はじめに
こんにちは。IT 本部 IT 基盤部第一グループの山本です。
2022 年に、 ProxySQL を使って Aurora MySQL の新規接続の遅延を解消した話 を書きました。今回はその続きで、当時 EC2 上に置いた ProxySQL1 を EKS2 へ移した際の話です。
ただし今回紹介したいのはその移行の手順ではありません。ProxySQL 自体の設定は 2022 年からほとんど変えていない一方で、周辺の実装は AI を利用して EKS 環境向けにすべて作り直しました。
先に結論を書きます。実装を任せられるかどうかは、仕様に確かめ方まで書けるか、そして検証の仕組みを先に作れるかで決まります。実装そのものより、確かめる仕組みのほうが効くというのが、今回いちばん実感した点でした。
2022 年も足りない部分は自分で書いていましたが、最小限のスクリプトで止めました。今回は Go で約 1 万行、テストと負荷試験の道具まで作っています。
確かめる仕組みを先に作るやり方は、 レガシー API を Go へ移植した話 の続きでもあります。あのときは移行元のコードが正解を教えてくれましたが、今回は何を正解とするかを決めるところから始めています。
以下、そのループの中身と、そこから出てきた具体的な穴を順に書きます。
2022 年に何を選び、何を諦めたか
2022 年に Aurora の前段へプロキシを置くと決めたとき、Amazon RDS Proxy と ProxySQL を比較しました。決め手はコストです。負荷に応じて台数を調整できるぶん、ProxySQL のほうが安く収まる見込みでした。
このときの前提は「製品はそのまま使う。足りない部分は運用で吸収するか、諦める」でした。実際には mysql_servers(接続先サーバーの一覧を持つテーブル)を同期するスクリプトを Python で書いています。とはいえ、Aurora のインスタンスを検出して登録し、weight を少しずつ増やすだけの最小限のものです。
それ以上を作らなかったのは、機能が要らなかったからではありません。書く時間とその後保守し続ける手間を考えると、割に合わないと判断したためです。インフラの担当者がミドルウェアの周辺の道具を内製すると決めるとき、たいていはこの見積もりで止まります。
今回の分岐: 組み込みの auto-discovery 機能が期待通りに使いこなせなかった
今回の移行では、アプリケーション(Rails on EC2)と Aurora はそのままに、間の ProxySQL だけを EKS に載せ替えました。台数を負荷に応じて増減させたかったのが動機です。
設計は、ProxySQL が持つ Aurora 用の auto-discovery 機能(mysql_aws_aurora_hostgroups)を使う前提で始めました。Writer と Reader の判別も、フェイルオーバーへの追従も ProxySQL 自身が行ってくれます。2022 年に自前で書いたスクリプトは不要になるはずでした。
しかし検証で問題が出ました。EKS のクラスターと Aurora は別の VPC にあり、VPC Peering を越えて接続します。この構成では check_timeout_ms(接続チェックのタイムアウト)の超過が頻発し、正常な Reader が SHUNNED(一時切り離し)のまま復帰しなくなりました。
さらに、auto-discovery は内部で mysql_servers を書き換えますが、その際に想定しないホストが hostgroup に入ることもありそのままでは利用できないと判断しました。
この時点で、選択肢は 2 つでした。
- auto-discovery の挙動に合わせて設計を妥協し、SHUNNED から戻らない状態を運用で拾い続ける
- Aurora の状態追従を自分で書く
今回は後者を選び、Kubernetes 上で動くコントローラーを Go で書きました。以下では Manager と呼びます。2022 年の見積もりなら前者を選んでいました。後者を選べたのは、AI で短期間に実装できるようになったからです。
仕様を書いて実装させ、負荷をかけて確かめる
これから並べる機能は、Manager というコントローラーと、ProxySQL Pod に同居させたサイドカーに入っています。あわせて Go で約 1 万行、145 commit、期間は約 2.5 か月でした。この期間は実装だけをしていたわけではなく、検証や本番構築、トラフィックの段階的な切り替えと並行しています。
2022 年に最小限のスクリプトで止めた判断と、今回すべて実装した判断の違いは、実装のコストです。実装を AI に任せられるようになったことで、検証で見つかった要件をすぐコードにして、次の検証に載せられるようになりました。負荷をかけるだけでなく、フェイルオーバーのような異常系も含めたテストパターンを、機能を足すたびに毎日流しています。
仕様 → 実装 → 検証を毎日回す
AI に任せているのは実装とテストの実行です。何を確かめるかを決めるところと、実行した結果を見て次を決めるところは自分でやっています。
- 1. 仕様を書く: 直すと決めたことを、要件とその確かめ方の形に落とします
- 2. 実装させる: 仕様を渡して、コードとテストコードを書かせます
- 3. 流す: 負荷をかけたままフェイルオーバーを起こすといった条件を渡して流させ、結果を確かめます
- 4. 次の仕様にする: 確かめた結果から、次に何を直すかを決めます
先に検証の仕組みを作る
最初に用意したのは、負荷をかける道具です。ProxySQL は MySQL プロトコルを話すので、HTTP の負荷試験ツールは使えません。k63 に SQL 用の拡張(xk6-sql4)と MySQL ドライバーを組み込んだイメージをビルドして利用しました。
このイメージを Argo Workflows5 のテンプレートから起動し、クラスター内から負荷をかけます。設定するパラメーターは Virtual User(VU)数、対象の AZ、1 接続あたりのクエリ数、書き込みを含めるか、実行時間です。
プロキシの前段では新規接続の比率が性能に効くため、本番に近い比率で負荷を徐々に増やして流しています。
スクリプトは 2 本用意しました。負荷試験用と、シナリオテスト用です。シナリオ用のテンプレートは 3 つの AZ に同時に負荷をかけられるようにしてあり、流している最中に Reader の追加やフェイルオーバー、Pod の停止を発火させます。判定はしきい値で行い、接続エラー率は 5% 未満、クエリのエラー率は 1% 未満を条件にしました。クエリの所要時間やエラー率はカスタムメトリクスとして Prometheus に送り、AZ 別に見ています。
冒頭で触れたレガシー API の移植でも、順番は同じでした。Perl 版と Go 版のレスポンスとデータベースの差分を比較するテスト基盤を先に作りました。7,195 行のテストコードと 185 件のテストケースで、本来なら後回しになる規模です。それを先に作れたのは、実装を AI に任せられたからでした。
違うのは、正解をどこから取るかです。前回は移行元の Perl 版が正解でした。同じリクエストを両方へ投げて差分が無ければ合格なので、判定は自動化できます。今回作った機能には比較する相手がいないので、何を正解として、どのように実現するかもこちらで決めました。仕様がそのまま合否の定義で、負荷をかけたまま事象を発火させ、エラー率をしきい値で見る形にしています。CI で回すのではなく、Argo Workflows から都度流します。
この道具があると、機能を足すたびに同じ条件で動作を確かめられます。
仕様の書き方
次に、任せるために書いたのは仕様書です。「実装すること」と「確かめ方」は、そのまま仕様書の見出しでもあります。たとえばピーク前に台数を増やしておく事前スケールでは、KEDA6 の cron トリガーが目標台数を絶対値でしか指定できません。そこで、窓に入った時点の台数(baseline)の何倍にするかで指定できるようにしました。この拡張のときは、以下のような内容を先に書いてから実装させました。
## 要件
- 窓の増分に倍率指定 xF と割合指定 +P% を追加する。どちらも内部では factor に正規化し、desired = ceil(baseline * factor) で計算する
- 既存の絶対値指定 +N は現行どおり維持する(後方互換)
- 値の範囲: 増分は必ず baseline を増やす方向のみ受理する。絶対値は [1, 20]、倍率は正規化後に (1.0, 5.0]。範囲外や no-op(+0 / x1 / +0%)は起動時に停止する
- レプリカ数の最終的な上下限は ScaledObject 側に集約する。上記の範囲チェックは打ち間違いの検知が目的で、上限による打ち切りとは別の層
実際の要求仕様はどこまで書けば足りるかは使うモデルによって変わると思います。ここに載せたのは今回の環境で落ち着いた粒度で、一般解ではありません。
指示で意識したこと
指示を出すうえで意識したのは、次の 3 点です。
- 要件と一緒に確かめ方を渡す: フェイルオーバー後の挙動を定義する場合に完了までの時間を測る、というところまで含めないと、動いたかどうかを判断できません
- 実測値は AI に決めさせない: ウォームアップの接続数のように環境ごとに上限が変わる値は、自分で計測した上で値を与えます
- ガードレールを用意する: シナリオテストと本番を超える負荷の試験の環境を作って検証しました
テストコードも AI に書かせました。weight の計算のように条件分岐が多い部分は、AZ ごとの台数や接続数を変えたケースを網羅的に書いてもらいました。
要件と合否基準さえ書ければ、実装そのものは以前ほどの障壁ではなくなりました。逆に言うと、「何が足りないのか」を洗い出す作業の比重が上がっています。
製品の外側で埋めた穴
以下は、そのループから出てきたものです。すべてを最初から設計していたわけではありません。2022 年から分かっていた課題もありますが、多くは検証で問題が出るたびに 1 つずつ足していきました。見つけ方はコードを読むことではなく、負荷をかけたまま事象を発火させることでした。
必要になった順に、「何が起きるか」「何を実装するか」「どう確かめるか」の形で挙げます。ProxySQL に限らず、同じ形で整理できると思います。
EKS 上の ProxySQL の全体構成
起動直後の接続の遅さ
Aurora は新規接続の認証に 2 〜 3 秒かかることがあります。ProxySQL 側もスレッドの生成にコストがかかります。起動した Pod がそのままトラフィックを受けると、この遅延を利用者が踏みます。
- 実装すること: 起動時に全ユーザーと全インスタンスの組み合わせへ接続し、認証キャッシュを温める。その後もアイドル接続を保持する
- 確かめ方: 投入直後のエラーとレイテンシーを見る
Aurora のトポロジー変化への追従
Aurora の Reader はオートスケーリングで増減し、フェイルオーバーで Writer が入れ替わります。追従できなければ、ProxySQL は居なくなったインスタンスにクエリを送り続けます。
- 実装すること: RDS API でインスタンスを検出し、ProxySQL の Admin API 経由で
mysql_serversを更新する。全体の同期は 10 秒周期 - 確かめ方: 実際にフェイルオーバーを起こし、反映までの時間を測る
Writer の切り替わりだけを速く見る
全体の同期に Writer の判定を含めると、フェイルオーバー時に書き込みが失敗し続ける時間が周期に引きずられます。Reader の増減は多少遅れても許容できますが、Writer は待てません。
- 実装すること: Writer の判定だけを 2 秒周期の別ループに分ける
- 確かめ方: フェイルオーバーから反映までの時間を測る。今回は約 22 秒
一時的な状態遷移での誤削除
Aurora のインスタンスは、メンテナンスやバックアップ、パラメーターグループの変更中に一時的に異なる status を返します。「見えなくなったから削除する」と実装すると、生きている Reader を消してすぐ戻す動きになります。
- 実装すること: 削除するかどうかの判定を、クラスターのメンバーシップに基づいて行う。削除の方法自体は変えなくてよい
- 確かめ方: 負荷をかけながら Reader の再起動とフェイルオーバーを発火させ、エラー数とサーバーの出入りの回数を見る
新しい Reader を入れたときの性能劣化
オートスケーリングで Reader が増えると、その 1 台に均等割りのトラフィックが流れ込みます。起動直後は InnoDB のバッファープールが温まっていないため、性能が落ちます。2022 年の記事でも触れた課題です。
- 実装すること: 新しく検出した Reader は weight 0 で登録し、段階的に上げる。今回は 60 秒ごとに 5 ずつ
- 確かめ方: 投入時のレイテンシーを段階ごとに測る
AZ 間の通信
ProxySQL を挟むと AZ を跨ぐ通信が増えます。2022 年の時点で、アプリケーションから ProxySQL への経路は同一 AZ に寄せていました。今回は ProxySQL から Aurora への経路も対象にしています。
- 実装すること: 同一 AZ の Reader を優先し、その AZ の台数が不足する場合は他の AZ に配分する
- 確かめ方: AZ ごとの台数と接続数を変えたケースで weight のテーブルを検算し、実機では ProxySQL の管理インターフェイスで配られた weight を見る
制御を集約したことによる単一障害点
Reader の同期と weight の制御を 1 つのコントローラーに集約すると、それ自体が単一障害点になります。
- 実装すること: Kubernetes の Lease を使った Leader Election で冗長化する
- 確かめ方: leader を落として引き継ぎまでの時間を測る。通常時は約 1 秒、クラッシュ時で約 15 秒
本番へ入れる前に確かめたこと
本番へ入れる前に、以下を確かめました。
- シナリオテスト: Reader の追加、フェイルオーバー、Leader Election、Pod の停止、ローリングアップデート、負荷増加
- 負荷試験: 単一 AZ に負荷をかけ、3 AZ 構成で本番のピークの 2 倍以上のクエリに耐えられること
- 段階移行: Route53 の加重ルーティングで 1%、10%、50%、100% と上げ、各段でエラー率とレイテンシーを見ること
移行して何が変わったか
同じものを載せ替えただけではないので、変わった点をまとめます。
コスト
EC2 では c7gn.2xlarge をオンデマンドで、各 AZ に 3 台ずつ、合計 9 台を常時起動していました。スケールアウトできない構成なので、ピークに合わせた台数を平常時も持ち続けることになります。EKS では負荷に応じて台数が動き、インスタンスも小さいものに変えて spot に載せました。インスタンスコストは月額でおよそ 8 割下がりました。
AZ 間の転送量も減っています。同一 AZ の Reader を優先する weight を入れたことで、DB read の cross-AZ 比率は 65% から 31.5% になりました(日次の使用量ベース)。
安定性
各 AZ 3 台という台数には、1 台落ちても残りで受けられるようにという意味もありました。落ちた 1 台の入れ替えは自分で行っていました。EKS では Pod が落ちれば作り直され、Node が落ちれば Karpenter7 が別のインスタンスを立てます。ウォームアップを入れてあるので、入れ替わった Pod が起動直後の遅さを持ち込むこともありません。
シナリオテストでは Pod の停止とローリングアップデートも流して確かめています。制御側の Manager も Leader Election で引き継ぐので、通常時は約 1 秒で戻ります。AWS からの中断やメンテナンスの通知にも自動で入れ替わるので、インスタンスの面倒を見る作業はなくなりました。
オブザーバビリティ
Manager は自分の状態をメトリクスとして出します。最終成功時刻、mysql_servers の online と shunned の数、Writer が切り替わった回数、同期の失敗数などです。
ダッシュボードも移行と同時に作りました。ノードの種別と中断の理由、AZ ごとの Pod の配置、クエリの所要時間とエラー率などです。これらは実装が終わってから足したものではなく、機能を足すのと同じループの中で一緒に作りました。
本番に入れてから、その効き目を確かめる機会がありました。ある時期、アプリケーション側の指標はすべて正常でした。CPU はスケールのしきい値 50% に対して 22 〜 30%、接続数にも余裕があり、クエリのレイテンシーも変化していません。埋まっていたのはノード側のカーネルのテーブルで、使用率は 90% 近くまで来ていました。満杯になれば新規接続がドロップし、アプリケーションからは原因不明の接続エラーとして見えます。そこから遡るのは相当きつかったはずです。
気づけたのは、ノード側の資源まで監視の対象に入っていたからです。80% の時点でアラートが上がり、枯渇の前にこの指標をスケールのトリガーへ追加しました。障害には至っていません。接続を集約する層では、アプリケーションのリソース指標とは別に、カーネル側の資源を明示的に見ておく必要があると思います。
なお、問題を見つけたのは AI ではありません。見つけたのは、負荷をかけたまま事象を発火させる道具と、こうした監視です。その手前を先に作れたことが、実装を任せられたことの効果でした。
まとめ: 割に合う範囲が広がった
要件と合否基準を書ければ、実装そのものは以前ほどの障壁ではありません。効いたのは仕様の書き方と、検証を先に作ったことでした。確かめ方を要件と一緒に渡す、実測で決まる値は自分で計測して与える、ガードレールを用意する、の 3 つです。前回のレガシー API 移行でも、後回しになりがちなテスト基盤を先に作れたことが効いていました。2 回続いたので今はここを最初に考えます。
2022 年も足りない部分は自分で書いていました。違うのは量です。あのときは最小限のスクリプトで止めましたが、今回は 1 万行とテストの道具まで作りました。製品が期待どおりに動かない部分を周辺の実装で埋める、という手はもともとあって、それが割に合う範囲が広がったという話です。
外から触れることは条件になります。今回は ProxySQL に Admin API があったので、サーバーの一覧も weight も外部から操作できました。本番のデータベース経路に自作のコードを常駐させられたのも、確かめる仕組みを先に作れたからです。
一方で、自作したものは自分で保守します。実装した側の判断ミスは、自分で見つけて直すしかありません。比較する相手は「コストゼロ」ではなく、「挙動が読み切れない既製機能を運用で支え続けるコスト」だと考えると、判断しやすいと思います。
-
ProxySQL は MySQL プロトコルを話すプロキシです。アプリケーションとデータベースの間に置いて、接続をまとめる役割を持ちます。 ↩︎
-
Argo Workflows は Kubernetes 上でワークフローを実行するためのツールです。 ↩︎
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。