DBプロキシによるクエリルーティング
アプリのSQLは変えず、プロキシ層が読み書き振り分けとシャード解決を肩代わり。透過的フェイルオーバーまで含めた内部動作が分かります。
- 1.Vitess/ProxySQL的なプロキシは、SQLを構文解析してSELECTかDML/DDLかを判定し、読みはレプリカへ、書きはプライマリへ自動で振り分ける。
- 2.シャーディング構成では、プロキシがシャードキーからクエリを解析し単一シャードへ直送するか、複数シャードへファンアウトして結果をマージする。
- 3.プライマリ障害時はヘルスチェックとレプリケーショントポロジ監視で新プライマリを検出し、コネクションを再接続してアプリ側の再設定なしにフェイルオーバーを完結させる。
プロキシ層が解決する問題
シャーディングやレプリケーションを導入すると、アプリは「どのシャードに書くか」「どのレプリカから読むか」「プライマリが落ちたらどう繋ぎ直すか」を自前で判断する必要が出てきます。これをアプリコードに埋め込むと、シャード数が変わるたびに全サービスを改修する羽目になります。
DBプロキシ(Vitess の VTGate、ProxySQL、MySQL Router など)は、アプリと実DBの間に挟まる中間層です。アプリからは単一のMySQL/PostgreSQL互換エンドポイントに見え、内部でクエリを解析してどのノードへ送るかを決めます。シャーディング方式がデータの分割方式そのものを扱うのに対し、この記事は「その分割をプロキシがどう隠蔽し、実行時にどうルーティングするか」に焦点を当てます。
読み書き振り分けの仕組み
プロキシは受け取ったSQL文をパースし、以下のように分類します。
- 読み取り専用(
SELECT、コメントで明示されたヒントなし)はレプリカ群へ - 更新系(
INSERT/UPDATE/DELETE/DDL)はプライマリへ - トランザクション内のクエリは原則プライマリに固定(後述)
BEGIN 後の SELECT を素朴にレプリカへ流すと、直前の自分の書き込みが反映されていない可能性があります(read-your-writes が崩れる)。多くのプロキシはトランザクション開始を検知すると、その接続のクエリを全てプライマリへ固定するか、アプリ側のヒント(コメントや専用API)で明示的に読み先を指定させる方式を取ります。
レプリカが複数ある場合、プロキシは単純なラウンドロビンだけでなく、レプリケーション遅延を監視して遅れが大きいレプリカを一時的に除外する、負荷(アクティブコネクション数)で重み付けするといった選択ロジックを持ちます。遅延の許容値を超えたレプリカへ読みを送ると stale read(古いデータの読み取り)が発生するため、読み取り一貫性の要求が強いクエリはプライマリに固定するオプションを用意するのが一般的です。
シャード間ルーティング
シャーディング構成では、プロキシはさらに一段複雑な判断をします。
- 単一シャード直送:
WHERE句にシャードキーの等価条件(user_id = 123など)が含まれる場合、キーからシャードを一意に特定し、そのシャードへ直接転送する。ネットワークホップが1回で済み、レイテンシは非シャーディング構成とほぼ変わらない。 - ファンアウト(scatter-gather): シャードキーを含まない集約クエリやレンジスキャンは、該当しうる全シャードへ並列にクエリを転送し、返ってきた結果をプロキシ側でマージ・再ソートしてアプリへ返す。
| 観点 | 単一シャード直送 | ファンアウト |
|---|---|---|
| 条件 | シャードキーで一意特定できる | シャードキー条件がない/範囲指定 |
| レイテンシ | 1シャード分(低い) | 最も遅いシャード律速(高い) |
| 負荷 | 対象シャードのみ | 全シャードに負荷が波及 |
| 典型例 | 主キー検索、ユーザー単位のCRUD | 集計、管理画面の全件検索 |
ファンアウトが必要なクエリが多いスキーマ設計は、シャードを増やすほどファンアウト対象も増えるため、スケールしても改善しにくいという構造的な弱点があります。プロキシのクエリログでファンアウト比率を監視し、シャードキー設計(シャーディング方式参照)にフィードバックするのが実務上の定石です。
JOIN や集計関数を含むクエリは、プロキシ側で分割実行してから再計算する必要があります。例えば COUNT(*) は各シャードから部分カウントを集めて合算し、ORDER BY ... LIMIT は各シャードでソート済み結果を取得してからプロキシ側でマージソートし直します(分散結合とシャッフルで扱うシャッフル処理の縮小版に相当)。シャードをまたぐ JOIN は多くのプロキシでサポート外か、片方のテーブルを全シャードへ複製する設計(reference table)で回避します。
透過的なフェイルオーバー
プライマリ障害が発生した際、プロキシは以下の手順でアプリから障害を隠蔽します。
- 検出: ヘルスチェック(定期的な軽量クエリ)とレプリケーショントポロジの監視(合意アルゴリズムベースの管理層やオーケストレータからの通知)でプライマリ不達を検知する。
- 昇格の確認: 新プライマリへの昇格が完了し、書き込み可能になったことを確認する。昇格前のレプリカが古いデータのまま書き込みを受けるとスプリットブレインになるため、旧プライマリを確実に締め出すフェンシングが前提になる(同期/準同期/非同期レプリケーション参照)。
- 再接続: プロキシは保持しているコネクションプールの向き先を新プライマリへ切り替え、インフライトだったクエリはエラーを返すかリトライキューへ回す。アプリ側はDNSやIPを変える必要がなく、プロキシへの接続を維持したまま処理を継続する。
アプリケーションが認識するのは「一瞬のコネクションエラーまたはリトライ」だけで、どのノードがプライマリになったか、シャードがいくつあるかは一切知る必要がありません。この抽象化こそがプロキシ層の存在意義であり、コネクションプーリングがアプリ-プロキシ間で行う接続の使い回しと、プロキシ-DB間でプロキシ自身が行うプール管理は別レイヤーとして重なっています。
プロキシ層のコストとトレードオフ
プロキシは万能ではありません。全クエリがプロキシを経由するため、パース処理とネットワークホップの追加で数百マイクロ秒〜数ミリ秒のレイテンシが乗ります。またプロキシ自体が単一障害点にならないよう、プロキシプロセスを複数台配置してロードバランサでラウンドロビンさせる、あるいはアプリのDBドライバに軽量なルーティングロジックを埋め込んでプロキシへの依存を減らす(Vitess の VTGate をサイドカーとして各アプリPodに配置するパターンなど)構成が取られます。
シャード再配置(リシャーディング)中も、プロキシは移行済みキー範囲と未移行範囲を把握し、移行境界にあるキーへのクエリを新旧どちらのシャードに振るか判定する役割を担います。これによりリシャーディングをアプリの無停止のまま進められますが、判定ロジックの実装難度は高く、多くの実装ではダブルライト(新旧両方に書く)期間を設けて安全に倒します。
DBプロキシは、シャーディングとレプリケーションが生む「どこに聞けばいいか」という判断をアプリから完全に切り離すレイヤーです。読み書き振り分け、シャード解決、フェイルオーバーのいずれも、プロキシがSQL解析とトポロジ管理を通じて肩代わりすることで、アプリコードはシャーディングを意識しない単純なクエリだけを書けばよくなります。
データベース Article
DBプロキシによるクエリルーティングを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データベース
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
シャーディング構成では、プロキシがシャードキーからクエリを解析し単一シャードへ直送するか、複数シャードへファンアウトして結果をマージする。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「データベース / シャーディング」に近いか確認する。
- 強みである「Vitess/ProxySQL的なプロキシは、SQLを構文解析してSELECTかDML/DDLかを判定し、読みはレプリカへ、書きはプライマリへ自動で振り分ける。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。