ProxySQL の導入を検討して見送った話
はじめに
近年では、AWS などのクラウドサービスを利用することで、複数ノード構成のリレーショナルデータベースを容易に構築できるようになりました。クラウドサービスが登場する以前は、自前でクラスタ構成を組んだり、フェールオーバーの仕組みを実装する必要があり、複数ノードを扱うデータベースは非常にハードルが高いものでした。しかし現在では、マネージドサービスのおかげで、ノード数の増減からフェールオーバーまでを簡単に扱えるようになっています。
一方で、インフラ側でノードを増やしたとしても、アプリケーション側が対応していなければ、その恩恵を十分に受けることはできません。適切に読み込み・書き込みを振り分けられなければ、費用に見合う性能向上が得られず、実質的には単一ノードと大きく変わらない利用方法になってしまいます。
今回、サービスのリプレイスにあわせてインフラを再構築し、データベースノードを追加したうえで負荷試験を行ったのですが、読み取り専用として用意したノードがまったく利用されていないことが分かりました。原因は採用しているウェブフレームワーク側にあり、たとえば WordPress であれば HyperDB プラグインを導入することで読み書きを自動的に振り分けてくれますが、今回のフレームワークでは、コード上で利用するコネクションを明示しない限り、自動的な振り分けが行われない仕様でした。
この問題をアプリケーションだけで解決するのは工数がかかるため、インフラ側で吸収できないか検討した中で候補に挙がったのが ProxySQL です。結果的には採用を見送りましたが、このサービスがどのような機能を提供しているのか、そしてどのような理由で利用を見送ったのかを本記事で紹介します。
ProxySQL とは
ProxySQL は、アプリケーションとRDB (MySQL や PostgreSQLなど)のあいだに挟んで利用する「データベースプロキシ」です。アプリケーションから見ると、接続先は ProxySQL という 1 つのエンドポイントだけになり、その裏側でどのデータベースノードにクエリを送るかを ProxySQL が判断してくれます。
ProxySQL の大きな特徴は、発行された SQL 文の内容に応じて接続先を振り分けられる機能を有している点です。たとえば、SELECT 文は読み取り専用ノードのホストグループに、INSERT / UPDATE / DELETE などの書き込み系の文はプライマリノードのホストグループに送る、といったルールを設定できます。こうしたルーティングルールは ProxySQL の設定で指定でき、SQL のパターンやユーザー名などに応じた柔軟なクエリルーティングが可能です。
この仕組みによって、アプリケーションコード側では接続先を切り替える処理を書かなくても、ProxySQL の設定だけで読み書き分離や簡単な負荷分散を実現できる可能性があります。今回のケースでも、フレームワークが読み書き分離に対応していない問題を、SQL ベースのルーティングで吸収できないかという観点から ProxySQL を検討しました。
また、ProxySQL は内部に接続プールを持っており、多数のアプリケーションプロセスからの接続をまとめて扱えるようになっています。アプリケーション側は毎回データベースに新規接続しているつもりでも、実際には ProxySQL が裏側の接続を再利用するため、接続確立のオーバーヘッドを抑えられるというメリットもあります。さらに、バックエンドのデータベースノードに対してヘルスチェックを行い、障害が発生したノードを自動的に切り離したり、フェールオーバー時に接続先を切り替えたりする機能も備えています。
採用を見送った理由
今回は、SELECT 文をリーダー(読み取り専用ノード)へ、それ以外のクエリをライター(書き込みノード)へ振り分ける、単純なルールを設定して動作検証を行いました。その結果、ウェブアプリケーションが正常に動作しないケースがあることが分かりました。
原因はトランザクションの扱いです。単純なルールベースの振り分けを行っていたため、同一トランザクション内で発行されたクエリがリーダーとライターにまたがって送信されてしまい、トランザクション内の整合性をうまく保てていませんでした。ProxySQL には、同一トランザクション中のクエリを同じバックエンドに固定するためのオプションも用意されていますが、今回利用しているフレームワークに対する知見がまだ少なく、内部の動きがブラックボックスに近い状態でした。
また、バックエンドのレプリケーションは非同期であり、どうしても遅延レプリケーションが発生し得ます。例えば、書き込み直後に同一リクエスト内で SELECT が実行された場合でも、状況によってはリーダー側にまだ変更が反映されておらず、「書き込んだ直後の値が読めない」ケースが将来的に顕在化する可能性があります。フレームワーク側がどのような単位で接続やトランザクションを張り替えているかが不透明なままでは、この種の一貫性問題を完全にコントロールすることが難しいと判断しました。
最終的には、アプリケーション側で読み取り専用ノードを適切に利用するようコードを修正できたこともあり、ProxySQL を本番環境で採用することは見送りました。
RDS Proxyとの比較
今回、ProxySQL 自体は本番採用を見送ったものの、その機能を確認する過程で AWS の RDS Proxy との比較も行いました。結論として、当たり前ではありますが、本番ワークロードに対してはマネージドサービスである RDS Proxy を利用するのが望ましいと判断しています。
一方で、検証環境や小規模なシステムにおいては、RDS Proxy の代わりに EC2 上で ProxySQL を動かす構成も選択肢になり得ると感じました。たとえば、Lambda などサーバーレス環境から RDS へ接続する際のコネクションプーリング用途や、RDS for MySQL(非 Aurora)でリーダーインスタンスを用意した場合に、その読み取りトラフィックを分散するためのエンドポイントとして活用できる可能性があります。
さらに、アプリケーションサーバー上でアプリケーションコンテナと ProxySQL を並列に動かす、いわゆるサイドカーパターンで稼働させる構成も考えられます。この場合、RDS Proxy の追加コストを発生させることなく、複数の RDS for MySQL のリードレプリカを効率的に活用できる余地があります。
AWS Lambda で RDS を利用する場合、基本的には RDS Proxy の利用が推奨されますが、ProxySQL の概要を知っておくことで、「フルマネージドな RDS Proxy を選ぶか」「自前で EC2 + ProxySQL を構築するか」という別の選択肢も見えてきたと感じました。
なお、RDS Proxy は SQL ベースの自動読み書き振り分けは行わず、 主に接続プーリングとフェイルオーバーの高速化を目的としたサービスです。 読み書き分離が必要な場合は、アプリケーション側での対応が基本となりますので、全く同じサービスではないという点に注意が必要です。
まとめ
本記事では、関係データベースをインフラレベルでうまく活用するためのアプリケーションである ProxySQL を取り上げ、その概要と最終的に採用を見送った理由について紹介しました。Docker イメージも提供されているため、細かな設定はともかくとしても、導入そのもののハードルはそれほど高くありません。
ProxySQL という選択肢を知っておくことで、データベースをより効率的に活用するための設計や、RDS Proxy 以外の構成を検討する際のヒントになっていれば幸いです。
















