VercelでSupabaseの「max clients reached in session mode」が出たときと、かえって悪化させた修正
エラーは(EMAXCONNSESSION) max clients reached in session mode - max clients are limited to pool_size: 15です。当社のAPIがこれを返し始めたとき、マーケティングサイトはまったく正常でした。マーケティングサイトはPostgresを使わないからです。この種の障害が見過ごされるのは、まさにそのためです。正常に見える部分は、データベースを必要としない部分なのです。
このエラーの意味、修正するときに犯した2つの失敗、そしてVercelから実際に動く接続文字列を説明します。
2つのプーラー、2つのポート
サーバーレスがセッションモードを使い切る理由
ウォーム状態の関数インスタンスは、それぞれ小さなプールを持ちます。当社の場合は3本でした。セッションモードのプールは、インスタンスがウォームな間は接続を返しません。ウォームなインスタンスが5つで15本になり、上限に達します。どこから来るものでも、6つめのリクエストは失敗します。
フリートのデーモンが3つのエンドポイントを数秒ごとにポーリングしていたので、インスタンスは一日中ウォームなままでした。どれも珍しいことではありません。セッションモードが負ける、単純な計算です。
失敗その1:違う層から先に直した
応急処置は、セッションモードのまま、インスタンスごとにmax: 1とし、アイドルタイムアウトを20秒にすることでした。これは効きました。静かになったインスタンスが枠を返し、APIは数分で回復しました。続いて接続文字列をトランザクションモードに切り替えました。これは正しい判断でした。しかしmax: 1をそのまま残しました。これは間違いでした。
失敗その2:トランザクションモードでプールが1本
トランザクションモードでインスタンスごとに接続が1本だけだと、そのインスタンスのクエリはすべて1本のパイプの後ろに直列に並びます。デーモンのポーリングがそこで詰まり、リクエストは20秒の期限を超え、症状は500エラーから応答なしに変わりました。マーケティングサイトは相変わらず速いままでした。APIはまったく応答しなくなりました。
トランザクションモードは、トランザクションごとに接続を返します。ですから普通の大きさのプールが適しています。8にしたところ、タイムアウトは止まりました。
事前に気づくには
プーラーは、拒否するまで何も教えてくれません。Postgres自身は、尋ねれば教えてくれます。
select application_name, state, count(*) from pg_stat_activity where datname = 'postgres' group by 1, 2 order by 3 desc;
アプリに普段どおりの負荷がかかっているときに、セッション接続でこれを実行してください。数が上限に近づいていて、ほとんどの行がidleなら、忙しい1分が来ればこのエラーが出ます。トランザクションモードでは、同じクエリで接続が抱え込まれずに再利用されている様子がわかります。
実際に動く接続文字列
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
- ホストはプーラーです。
db.<ref>.supabase.coではありません。Supabaseのダッシュボードでは、トランザクションプーラーの見出しの下に直接接続のホストが表示されることがあります。それでも、それは直接接続のホストです。 - プーラーではユーザー名は
postgres.<project-ref>です。ただのpostgresは直接接続のホストでしか使えません。 - IPv4アドオンを購入しない限り、直接接続のホストはIPv6のみです。AAAAレコードがあり、Aレコードはありません。Vercelの関数からは到達できません。しかも、そこではポート6543は開いていません。
- postgres.jsではクエリ文字列は不要です。
prepare: falseを設定します。?pgbouncer=trueフラグはPrismaの慣習です。 - マイグレーションは5432のままにします。Drizzleなどのツールはセッション接続を必要とします。デプロイしたアプリは6543に、ローカルのマイグレーション環境は5432に向けてください。
$ dig +short A db.<project-ref>.supabase.co # (nothing) $ dig +short AAAA db.<project-ref>.supabase.co # 2600:1f18:... $ nc -z aws-0-us-east-1.pooler.supabase.com 6543 # succeeded
サイズ設定を1つの表で
最後に、知っておく価値のあることをひとつ。この間、APIに届かなくなったデーモンは、管理しているマシンがすべて死んでいると判断しました。そして電源を再投入しようとしました。その話はデータベースの接続数上限で、お客様のMacを危うく再起動しかけた話に書きました。
よくある質問
- VercelのアプリはSupabaseのどのポートを使うべきですか?
- トランザクションモードのプーラーである6543です。5432のセッションモードは、クライアント1つにつきサーバー接続を1本保持し、プロジェクト全体で15本が上限です。サーバーレスではすぐに使い切ります。
- Vercelからdb.<project-ref>.supabase.coに接続できないのはなぜですか?
- 直接接続のホストには、IPv4アドオンを購入しない限りIPv6アドレスしかありません。そしてVercelの関数はIPv6でそこに到達できません。プーラーのホスト、aws-0-<region>.pooler.supabase.comを使ってください。
- プーラーではどのユーザー名を使いますか?
- postgres.<project-ref>です。末尾にプロジェクトのrefを付けます。ただのpostgresは直接接続のホストでしか使えません。
- 接続文字列に?pgbouncer=trueは必要ですか?
- Prismaなら必要です。postgres.jsでは、代わりにクライアントのオプションでprepare: falseを設定します。トランザクションモードはプリペアドステートメントに対応していません。
- 各モードのプールサイズはいくつにすべきですか?
- セッションモードでは、上限が15なので、インスタンスごとに1か2にして、アイドルタイムアウトを短くします。トランザクションモードでは、接続はトランザクションごとに返却されるので、5〜10の普通のプールで構いません。