https://cloud-ace.jp/event/260929/
発生する問題
- アクセス急増
- Cloud Runが自動スケール
- Cloud Runの裏側にいるCloud SQLはスケールしない
- Cloud SQLに設定されている
max_connectionsの値を超過 Too many connectionsなどのエラーが発生- サービスダウン(500エラー)
アプリケーション側(e.g. Prisma)のコネクションプールの仕組みだと対応が難しい。例えばPrismaのコネクションプールの仕組みで管理できるのは、あくまで1コンテナ(1コネクションプール)あたりのコネクション数。Cloud Runでコンテナが爆増したら、その分Cloud SQLへの接続数も爆増してしまう。
関連: Google Cloud Managed Service for PrometheusでPrismaのコネクション数をチューニングする
どう対応する? 従来の方法
リードレプリカを大量に立てる- アクセス増からリードレプリカを立てるまでに時間がかる
- シンプルにコスト(お金)がかかる
- 静的IPのリスト管理が複雑になりがち
あまり筋は良くなさそう…
ではどう対応する? Cloud SQL Enterprise Plusを用いた方法
※前提: Cloud SQLのEnterpriseエディションとEnterprise Plusエディションは異なるプランである点に注意
Cloud SQL Enterprise Plusは、Managed Connection Poolingを標準サポートしているので、これを利用する。
Managed Connection Poolingは、要は今回のようなケースでいい感じにDBへの接続(コネクション)を管理してくれる機能。Cloud Runのそれぞれのコンテナから来る大量のコネクションを束ね、管理することで、Too Many Connectionsエラーを防ぐことができる。
参考: https://docs.cloud.google.com/sql/docs/postgres/managed-connection-pooling?hl=ja
Managed Connection Poolingには以下の2つのモードがある。
- Sessionモード
- Transactionモード
推奨アーキテクチャ
Cloud Run (containers scale out)
| |
write / update read
(many connections) (many connections)
v v
+--------------------------------+ +--------------------------------+
| Managed Connection Pooling | | Managed Connection Pooling |
| mode: Transaction | | mode: Transaction |
| many conns -> few conns | | many conns -> few conns |
+--------------------------------+ +--------------------------------+
| few connections | few connections
v v
+--------------------------------+ +--------------------------------+
| Primary (only 1 writer) | | Read Pool |
| | | read endpoint (single IP) |
| | | +-- node (read replica) |
| | | +-- node (read replica) |
| | | +-- node <- autoscale |
+--------------------------------+ +--------------------------------+
| replication ^
+----------------------------------+
※ 図では分けて描いたが、Managed Connection Poolingは別サーバーを立てるものではなく、各インスタンスで有効化する機能
2つの機能は別々の問題を解いているので、両方使う。
| 機能 | 解く問題 | これだけだと… |
|---|---|---|
| Managed Connection Pooling | 接続数(大量の接続を少数に束ねる) | 読み取りもPrimaryに集中し、CPUが詰まる |
| Read Pool | 読み取り負荷(複数ノードに分散) | 各ノードのmax_connectionsが溢れる |
- Transactionモードは、DBへの接続を「トランザクション(BEGIN〜COMMIT)の間だけ」貸し出す。アプリはDBと繋がっていても、実際にSQLを投げているのは一瞬で、残りはほぼ待ち時間。その空き時間に他のクライアントへ回すので、少ない本数で大勢をさばける。write/readに関係なくデフォルトかつ推奨。モードはインスタンス単位の設定なので、処理ごとには変えられない
例: Cloud Runのコンテナ A, B が、それぞれアプリ側(Prismaなど)で接続を2本ずつ持つ
→ a1, a2, b1, b2 の計4本がDBへ来る([##] = トランザクション実行中、. = 待ち)
Sessionモード: 1本ずつ占有 → DB側も4本(max_connectionsの枠を4つ消費)
DB接続1: a1 [##].......[##].....
DB接続2: a2 ....[##].......[##].
DB接続3: b1 ..[##].......[##]...
DB接続4: b2 ......[##].......[##]
Transactionモード: [##] の間だけ借りて返す
時間 → t1 t2 t3 t4
DB接続1: a1[##] b1[##] a1[##] b1[##]
DB接続2: a2[##] b2[##] a2[##] b2[##]
→ どの時点でも同時に実行中なのは2つ → DB側は2本(枠を2つ消費)
1本だと、もう片方は接続が空くまで待たされる
Sessionモードだと「DB側の接続数 = コンテナ数 × アプリ側の接続数」になり、Cloud Runが増えるほど掛け算で増える。Transactionモードなら、DB側は「同時に実行中のトランザクション数のピーク」だけで済む。
- readもTransactionモードで問題ない。むしろ単発の
SELECTは1クエリ = 1トランザクションなので、終わった瞬間に接続が返り、回転が速い - Sessionモードを使うのは、アプリが「接続に覚えさせた設定」に頼っているとき。Transactionモードでは、トランザクションごとに違う接続が割り当てられるので、前の接続で覚えさせた設定が次では消えている(共用PCで設定を変えても、次に座るのは別のPC、のイメージ)
Transactionモードで起きること
a1: SET timezone = 'Asia/Tokyo' → DB接続1 がこの設定を覚える
a1: SELECT now() → DB接続2 に回される → timezoneの設定がない!
同じ理由で、PREPARE(Prismaのprepared statementなど)や一時テーブルも使えない。こうした機能に頼るアプリを改修せずに載せたいならSession。まずTransactionで試し、動かなければSession、の順
- Read Poolの中身はPrimaryの読み取り専用コピー(Read Replica)なので、使えるのはreadだけ。writeは常にPrimary 1台で受ける(複数台に書くとデータの整合性が崩れるため)
- writeの負荷対策は、スペックを上げる以外にもある
- readをRead Poolへ逃がし、PrimaryのCPUを書き込みに専念させる(この構成自体がそう)
- 書き込みを減らす・ならす: まとめて書く(バッチINSERT)、キュー(Pub/Sub、Cloud Tasks)に溜めて一定ペースで書く
- それでも足りなければ、DBを機能ごとに分ける、または書き込みも台数で増やせるSpannerを検討する
- **
max_connections**は1台のDBが同時に受けられる接続の上限。Cloud Runが増えると接続も掛け算で増え、ノード追加も即時ではないので溢れる。だから接続プールで本数そのものを減らす
参考: Managed Connection Pooling / Configure Managed Connection Pooling / About read pools / Read pool autoscaling