ProxySQLのReader/Writer振り分けが引き起こすDB接続エラーを理解する
背景
ProxySQL 経由でDB(Aurora)に接続している環境で、散発的にDB接続エラーが発生していたのですが、アプリケーションのバグではなく、ProxySQL のルーティングロジックに起因する問題だと分かった話。
Aurora のアーキテクチャ
従来のDBとの違い
従来のDBアーキテクチャ(MySQLなど):
[コンピュート + ストレージ] ← 全部一体。接続先は1つ
Aurora:
[コンピュート: Writer] ─┐
[コンピュート: Reader] ─┼── [共有ストレージ]
[コンピュート: Reader] ─┘
従来のDB → コンピュートとストレージが一体で、接続先は1つ
Aurora → コンピュートが複数台に分かれ、ストレージを共有する
Auroraの場合コンピュートが複数台あるので「どれに繋ぐか」を指定する仕組みが必要に
→ エンドポイントの出番
Aurora のエンドポイント
| エンドポイント | 接続先 | 用途 |
|---|---|---|
| クラスターエンドポイント | Writer | 書き込み&読み取り |
| リーダーエンドポイント | Reader(ラウンドロビン) | 読み取り専用の負荷分散 |
| インスタンスエンドポイント | 特定の1台 | デバッグ等 |
Writer は1台、Reader は複数台
書き込みは Writer に、読み取りは Reader に振り分ければ負荷を分散できる
(一般的なWebアプリケーションでは読み取りがクエリの大半を占めるため、読み取り側を複数台にして負荷を分散している。Writer を1台に集約することで、書き込みの順序と一貫性を保証することもできる。)
ProxySQL による読み書き分離
なぜ ProxySQL を挟むのか
アプリケーションが Writer / Reader を意識してエンドポイントを切り替えるのは面倒だし、接続管理も複雑になる。
そこで ProxySQL を間に置く。
アプリ → LB → ProxySQL → Writer / Reader
こうすることで、アプリからは 1つの接続先 に見える。ProxySQL がクエリの種類を判定して、裏側で適切なエンドポイントに振り分ける。
振り分けのルール(簡略化)
-
INSERT/UPDATE/DELETE→ Writer -
SELECT→ Reader - トランザクション中 → Writer に固定(ロック)
この「トランザクション中は Writer に固定」が、今回のDB接続エラー問題のポイント!
エラーが起きる仕組み
トランザクション内での衝突
- アプリが
BEGIN(トランザクション開始)する - ProxySQL は「トランザクション中だから Writer に固定しよう」と判断
- トランザクション内で
SELECTが来る - ProxySQL は「SELECT だから Reader に送りたい」けど「この接続は Writer にロック済み」→ 衝突
具体例
こういうコードを考える:
ActiveRecord::Base.transaction do
user = UserService.update!( # ← INSERT/UPDATE (Writer)
user: user,
params: update_params
)
NotificationService.sync(
user: user, # ← user を読み取り (SELECT)
previous_email: previous_email
)
end
@permissions = user.permissions # ← さらに SELECT
更新処理のあとに、同じトランザクション内で読み取りが走っている。
SQL レベルで見ると:
BEGIN
UPDATE users SET ... → Writer
SELECT * FROM permissions WHERE ... → Reader に送りたいけどロック中 💥
COMMIT
という形でUPDATE(Writerに送る)の後にSELECT(Readerに送る)が来てDB接続が衝突してしまう。
なぜ「たまにしか」起きないのか
大半のリクエストでは問題なく通る。これが厄介なところで、エラーは ProxySQL の内部的な接続状態やタイミングによって発生する。
- 接続プールの状態
- 同時接続数
- クエリの到達タイミング
つまり再現性が低い。ログを見ても「たまに出る謎のエラー」にしか見えない。
まとめ
- Aurora はコンピュートとストレージが分離しており、Writer / Reader が別インスタンスになる
- ProxySQL はクエリの種類に応じて Writer / Reader に振り分けるプロキシ
- トランザクション中は Writer にロックされるが、SELECT の振り分けルールと衝突することがある
- このエラーはタイミング依存で再現性が低く、flaky test や散発的なエラーとして表面化する
MySQLのような従来のDBとクラウド向けに設計されたAuroraのアーキテクチャの違いや、ProxySQLの仕組みについて理解するいい機会になりました。
Discussion