tail my trail

作るのも使うのも、結局は、人なのだ

HAProxy & socat を使ったオンライン切り離しと Aurora Reader Endpoint における一考

f:id:uorat:20171201173258p:plain

前置き

随分前に RDS Read Replicas や Aurora Replica に対する振り分けを HAProxy で振り分ける方法を投稿した。

この後に、 Aurora Reader endpoint が実装され、 RDS for Aurora を使っている場合に限り、 readers への振り分けに HAProxy などのバランサーをかますことは必須ではなくなっている。 ただ、 Aurora Reader Endpoint も万能ではなく HAProxy を介しておくと小回りが利くことが多いので紹介しておく。

注意

ここで紹介している挙動は 2017 年 5 月時点の自プロダクトで見たものである。 環境固有の要因あり、かつ確認した挙動も古かったり見解が間違っているかもしれないので、あくまで参考程度に留めてほしい。

また、この記事を書いているちょうど今 re:Invent 2017 が開催中で、それに乗じて Aurora に関する Updates も多く出ている。 ちょっと前、2017年11月17日に Aurora Replica の Auto Scaling が可能 にもなっており、当時と今とでは結構状況が変わっている可能性が高い点を留意してほしい。

私と RDS for Aurora

今関わっているプロダクトは 2017 年の 5 月頃にバックエンド DB を RDS for MySQL から RDS for Aurora に移行した。

進め方は概ねデファクトだったと思う。

一通りのテストを終えたあとで、事前に移行元の RDS for MySQL Cluster から Aurora Read Replicas を生やしておき、HAProxy の weight で割合制御して本番トラフィックの参照系クエリの一部を流すようにして、まず様子を見た。 徐々に Aurora に散らすクエリの割合を増やしていき、最終的に 旧 Replicas の weight を 0 した時に まず RDS for MySQL Read Replicas を一掃した。 あとは Master を RDS for MySQL から Aurora writer に切り替える移行作業で、これは他のサービスアップデートのためのデプロイと兼ねて、サービスのピーク帯を避けて2時間程度のメンテナンス時間を設定し書き込み停止点を確保して進めた。

事前にステージング環境で入念なシミュレーションを行っていたこともあり、移行はあっけなく終わった。 細かいところを述べると、その後で本番トラフィックの洗礼を受け、幾つかのクエリチューニングが追加で必要となったりもした。Aurora のバッドノウハウである大量レコードのシーケンシャルリードを行うスロークエリが移行後に顕在化したりして、その手のクエリは抹消したりトランザクション分離レベルを変えたりした。

並列性に優れ、コンピューティングリソースを有効活用してくれる Aurora は RDS for MySQL 時代と比べて 最大で 5 倍のスループット が出るということだが、当然ワークロードによって性能向上率は変わる。 うちの場合だと Aurora の性能を存分に活かしきれるほどのクエリチューニングやアプリケーションリファクタをしまくったわけではないので スループットで言うと多少上がった程度で収まっている。 ただ、設計上 Replica Lag が起きにくいアーキテクチャとなっており、今までだと Read Replicas がサチった場合に Replica Lag が膨れるという副次リスクを問題視していたので、この恩恵を存分に受けることができた。これだけでも移行するメリットがあると感じている。

話が逸れるが、 Aurora Storage の Quorum Model についてまとまった解説がなされており、読むととても面白い。

Aurora における HAProxy の使いどころ

Reader Endpoint がなかった頃は結局 すべての Node Endpoints で振り分けざるを得なく、 また Writer に参照クエリを散らさないためには read_only (innodb_read_only) から識別するしか手がなかった。

以下、参考記事。

なので、このもやもや感を取っ払ってくれた Aurora Reader Endpoint はとても響いたリリースだった。

先程も述べたとおり、今は Aurora Reader endpoint があるので、参照クエリは Read Only なエンドポイントに向けてあげれば基本的には活性化している Aurora Replica にクエリが分散される。 各 Node の障害時は切り離されるし、 Writer 障害時に ある Reader が Writer に昇格すれば、その元 Reader には Reader endpoint 経由でクエリが流れてくることはなくなる。

ただ、いざ使ってみると必ずしも万能ではなく 2017 年 5 月当時に挙動を見る限りいくつか痒いところがあった。 なので、うちでは Reader Endpoint に身を委ねるのではなく、 HAProxy を引き続き間に挟むようにした。

背景と対処方法を紹介しよう。

1. Aurora readers の縮退時、しばらく削除した Reader Node にもクエリが振り分けられることがある

Cluster Endpoint や Reader Endpoint は DNS Endpoint で、ランダムに Reader Node IP を返却することで Readers の分散を実現している。 基本的には非活性な Node の IP は返さないし Reader から Writer に Failover した Node も返さない挙動だが、 DNS record となると DNS Cache や TTL が絡んでくるので、クエリを流すその瞬間に活性な Reader であることを保証してくれるものではない。 かつ、 Replica Node 削除時に実行中のクエリの終了を保証してくれるものでもないと思う。

うちの場合だと、Aurora 移行が一段落して性能もある程度恩恵を受けれるようになったので Aurora Readers を縮退 (削減) させるべく縮退時の挙動を当時見たのだが、 Reader を 幾つか Delete すると実際に何発か DB 接続エラーが出てしまった。

イレギュラーイベントな Writer 障害時の Failover であればまぁ仕方ないのだが、定型的なオペレーション時まで接続エラーが出るのは運用上ちょっと気持ちが悪い。 アプリケーションアラートが飛ぶので精神衛生上もよろしくない。

2. Replica Node の再起動をしたい時に困る

あまり頻度は高くないが、 Replica Node を再起動したい時がある。

例えば、DB Parameter Group を別のものに変更するような時。 特に、Aurora Replica の増設を画面 (RDS Console) で行った場合、Amazon Aurora DB クラスターの作成手順 に従って Cluster Indentifier を指定して Replica Node を追加することになるが、ここで DB Parameter Group を指定が出来ないのでデフォルトの DB Parameter Group (default.aurora) があたった形でReplica Node が起動してしまう。その場合、再起動を伴う DB Parameter Group の変更が必要となる。皆が AWS CLI や CloudFormation, Terraform などで操作していればこの点は困らないのだろうが。

また、Rebooting な Replica Node の IP を Reader Endpoint が返さないことを保証してくれるのか、当時ドキュメントを漁っても見あたらず、少なくとも当時軽く確認した限りでは再起動中の Replica の IP が変えるように見えた。 そして、いずれにしても再起動する以上、処理中のプロセスは kill される。

3. 回避策

なので、結局 HAProxy を間に挟んだ。ポイントは HAProxy の重み付けバランシングを利用するようにしつつ以下のように Backend Node を登録したこと。

  • 通常は HAProxy 経由で Reader Endpoint に流す
  • あわせて weight 0 な全 Node Endpoint を仕込んでおく
  • socat を使ってソケット越しで設定変更できるように操作レベルを admin にしておく

例えばHAProxy の設定はこんな感じ。

こうすると、通常は weight が 1 : 0 : 0 : 0 : 0 なので Reader Endpoint 以外には振り分けられない。 縮退や増設などの構成変更を行う場合は、HAProxy の weight をオンラインで変更して HAProxy から各 Node Endpoint に round-rogin させ、 一方で Reader Endpoint 経由で振り分けないようにする。 socat を使った重み変更なので HAProxy プロセスの再起動/再読込は不要、処理中の接続が中断されることもないのでオンラインで作業できる。

具体的にはこのような流れとなる。

  1. 継続利用する Node Endpoint の weight を 1 にする
  2. Reader Endpoint 経由でクエリが流れないように Reader Endpoint の weight を 0 にする
  3. (縮退する場合は)削除予定の Replica Node にクエリが流れてこないことを確認する
  4. 構成変更作業を進める
  5. 構成変更作業が終わったら、 Reader Endpoint の weight を 1 に戻す
  6. さらに、 Node Endpoint の weight を 0 に戻す

HAProxy Node がいくつもあると操作が面倒なので、Ansible のようなオーケストレーションツールを使うと比較的ラクにオペレーションできると思う。 以下、read4 を削除するためのオンライン weight 変更操作を Ansible で実行する例を載せておく。

最後に

この半年ほどこのオペレーションで、Aurora Replica の台数変更を何回か行っており上手く回っている。 時間も経ってしまったのでいい加減記事におこしておこうと思っていたところで、Aurora の Updates が立て続けに出てきた。

特に Aurora Replica の Auto Scaling サポートは大きい。 増強縮退を前提にしているのであればクエリを中断することもなく上手く処理中のタスクを吐き出してくれる気がするので、今回紹介したような HAProxy を挟む必要性は薄れるかもしれない。 まだ動作確認していないのであくまで希望的観測だが。

それにしても、今年も re:Invent 行きたかった。。

Amazon Web Services完全ソリューションガイド

Amazon Web Services完全ソリューションガイド