Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Amazon AuroraにAuto Scalingを導入してCPU高負荷を乗り切った話

こんにちは、SREチームの菅原(id:ksugahara08)と栗山(id:shepherdMaster)です。

新型コロナウイルスが流行して世の中では外出自粛になる中、Studyplusは例年以上に多くのユーザー様に利用していただく機会を得ました。それに伴って4月からサーバーへのアクセスが増えていき、5月にはコロナ前と比べて約2倍のリクエスト数がAWS ALBに来るようになりました。

リクエスト数が増えた結果何が起きたかというと、タイトルからお察しの通りAmazon AuroraのCPU負荷が上がり、頻繁にアラートが発砲されるようになりました。

EC2に関してはAutoScalingを設定していたため負荷に応じてスケールアウトしていたのですが、Auroraに関してはインスタンス数固定で運用してきたため、このようなことが起きるようになりました。

そこでサーバーサイド+SREチームでは他の作業を全てSTOPして、Auroraの負荷対策を行うことにしました。

ちなみにこれが当時(ピークタイムだけじゃなく日中にもCPUアラートがなりだした時)の様子です。 f:id:ksugahara08:20200525161342p:plain

ネタバレになりますが、各エンジニアの尽力によりラグナロクは回避されました。

そもそもなぜAuto Scalingさせる必要があったのか

AuroraのCPU使用率が高騰するようになったと言っても、すぐにスケールアウトを行ったわけではありません。DBのインスタンス数を増やす前にスロークエリ等の改善を行いました。というのも負荷が上がるのはピークタイムの数時間のみで、Auroraが高負荷状態になってもユーザー問合せが来るほどレスポンスタイムも大きく劣化していなかったためです。

スロークエリの改善やN+1問題の解消等の改善を行ってみたのですが、リクエスト数の増加による負荷高騰の要素が大きく、結果としてAutoScalingを入れて負荷分散をさせることにしました。

AutoScalingを選択したのはAuroraのインスタンスは高価で常に起動させているとAWS利用料金が大きく上がってしまうことを避けたかったからです。

弊社のAmazon Aurora構成

話の前提として弊社のAamazon Aurora構成について話しておきます。 特殊な構成では無いのですが、以前は以下のような構成になっていました。

Aurora-cluster
├── db-1 (Master)
├── db-2 (Read Replica)
├── db-3 (Read Replica)
└── analysis-db (分析用)

BIツールのために分析用のanalysis-dbを入れています。APサーバーからはカスタムエンドポイントを使って読み込み先DBインスタンスを分散させています。常時Master 1インスタンスとRead Replica 2インスタンスが起動している状態でした。

カスタムエンドポイント(読み込み先)の対象は分析用以外全て含めています。これはMasterにも読み込み処理を行ってもらい、Read Replicaを増やさなくて済むようにしているためです。弊社のアプリは読み込み系の負荷が大きくなることも背景にあります。

こちらの構成だったclusterを今回以下に変更しました。

Aurora-cluster
├── db-1 (Master)
├── db-2 (Read Replica)
├── db-3 (Read Replica managed by AutoScaling)
├── db-4 (Read Replica managed by AutoScaling)
・・・
├── db-n (Read Replica managed by AutoScaling)
└── analysis-db (分析用)

不意なフェイルオーバーに対応できるように常時MasterとRead Replicaをそれぞれ1インスタンス起動し、CPU負荷をトリガーにしてRead ReplicaをAuto Scaleさせるように設定します。弊社のリクエストの傾向として夜間はリクエスト数が減るため、台数を2インスタンスに減らして夕方のピークタイムに増やすようにしたいという意図がありました。

また、弊社のリスエストピークは毎日同じ時間に起き、ゲームアプリのような急なリクエスト増加も滅多に起こらないため、Auto Scalingの設定で用件が満たせるだろうと予想しました。

Aurora Auto Scalingの設定

この記事ではAurora Auto Scalingで出てくる用語については説明を省略して弊社での設定を中心に話します。もし用語がわからなければ公式ドキュメントを参照してください。

結論から書いてしまうと、弊社では以下のような設定値で落ち着きました。 f:id:ksugahara08:20200525162948p:plain

ターゲットメトリクスを44%にしているのは、Read Replica全てのインスタンスにおけるCPU使用率の平均値で判定されるため、分析用DBが入っている分ターゲット閾値を(しかたなく)低く設定しました。この値は何度も試行を繰り返しながらこの値に落ち着いた形です。

スケールインクールダウン期間は夜間にスケールインを想定しており、負荷の下がり方が急なので5分に設定しました。 スケールアウトクールダウン期間は30分に設定しました。DBがclusterに追加されてから、利用可能になるまで5分半かかり(何度も作り直し平均値を取りました)、追加されたRead Replicaがエンドポイントに追加されて、負荷が分散されるまでに時間がかかるためこれくらい長くすることにしました。

カスタムエンドポイントの変更

公式のAuto Scalingでは読み込みエンドポイントを推奨しているのですが、分析用DBインスタンスをがいることと、料金的事情でMasterにも読み込みリクエストを処理して欲しいという理由でカスタムエンドポイントを継続して使うことにしました。

カスタムエンドポイントには追加設定で今後追加されるインスタンスをこのクラスターにアタッチするという項目があるのでこれを有効にしました。これでAuto Scalingによって追加されたRead Replicaをカスタムエンドポイントで接続させることができます。

f:id:ksugahara08:20200525163021p:plain

サービス停止を伴うメンテナンスをしたくなかったので、10分程度の接続断が発生するカスタムエンドポイントの変更は行わず、カスタムエンドポイントを2つ作って付け替えることにしました。

アプリケーション側の対策

弊社ではRuby on Railsを使っており、コネクションプールを有効にしています。 そのため、DBがオートスケールすると以下の問題が発生します。

  • DBがスケールアウトしたあと、追加されたDBをRailsが認識できない(=追加されたDBにSQLが実行されない)
  • DBがスケールインしたあと、削除されたDBへ接続しようとしてエラーになる

なかなか悩ましい問題なのですが、なんとかこれらの対応をしました。

DBがスケールアウトしたとき

DBを定期的に自動的にデプロイされるように、Jenkinsの設定をしました。 (最初はCPU使用率トリガーでオートスケールさせていたので、いつオートスケールされるかわからないためオートスケールされそうな時間帯に30分に1回デプロイしてましたが、決まった時間でオートスケールさせるようにしてからは、その時間よりちょっとあとにデプロイされるようにしました。) デプロイしなおせばコネクションプールが作り直されるので、スケールアウトして追加された新しいDBに接続がされるようになります。 かなりアドホックなやり方なのですが、他にいい方法が思い浮かばなかったのでこうしています。何か他にいい方法があれば教えて下さい…。

その他には、コネクションプールを無効にしようかと思い、 activerecord-refresh_connection gemの導入を考えましたが、こちらのgemはpumaでは使えないため諦めました。 またpuma worker killerの導入もしましたが、puma worker killerの実行のタイミングでたまにNo connection poolエラーが発生するという未解決の問題があり、最終的に定期デプロイに落ち着きました。

DBがスケールインしたとき

RailsではDBに接続できなくなった場合、エラーを投げ、コネクションを削除するようです。そのため次に接続するときは生きているDBに接続がされます。 なのでスケールイン後ずっとエラーが出続けるわけではないですが、エラーが出ないようにしたいです。 色々探した結果、 activerecord-mysql-reconnect gemを導入しました。 こちらを入れるとDB接続エラーが発生した場合、再接続をしてくれます。ありがたいですね。

ScheduledActionの設定

Auto Scalingを設定してみたものの、ターゲットメトリクスの閾値設定に苦慮するようになりました。当初CPU使用率が55%でスケールアウト・インするように設定をしていたのですが、CPU使用率の値は小刻みに変動するため、不要なスケールアウト・インが繰り返されるという悩みです。これが起きると上記で上げたアプリケーション側での対応も必要になり設定がより複雑化します。

そこでScheduledActionを設定することにしました。ScheduledActionは時刻指定で最大、最小のインスタンス数を変えることができます。

今回はインスタンス数の最小値を定時に変えることで、閾値付近をCPU使用率の値が超えたり下がったりしないようにしました。 具体的には以下のような設定をTerraformで入れています。

# JSTの20:20にスケールアウト
resource "aws_appautoscaling_scheduled_action" "scaleout-nighttime" {
  name               = "scaleout-nighttime"
  service_namespace  = "rds"
  resource_id        = "cluster:my-aurora-cluster"
  scalable_dimension = "rds:cluster:ReadReplicaCount"
  schedule           = "cron(20 11 * * ? *)"

  scalable_target_action {
    min_capacity = 3
    max_capacity = 5
  }
}

# JSTの0:30以降にスケールイン
resource "aws_appautoscaling_scheduled_action" "scalein-nighttime" {
  name               = "scalein-nighttime"
  service_namespace  = "rds"
  resource_id        = "cluster:my-aurora-cluster"
  scalable_dimension = "rds:cluster:ReadReplicaCount"
  schedule           = "cron(30 15 * * ? *)"

  scalable_target_action {
    min_capacity = 2
    max_capacity = 5
  }
}

※こちらの設定ファイルはあくまで例です。

AWS CLIで設定する場合は以下になります。

# JSTの20:20にスケールアウト
aws application-autoscaling put-scheduled-action \
  --service-namespace rds \
  --schedule "cron(20 11 * * ? *)" \
  --scheduled-action-name 'scaleout-nighttime' \
  --resource-id 'cluster:my-aurora-cluster' \
  --scalable-dimension rds:cluster:ReadReplicaCount \
  --scalable-target-action 'MinCapacity=3,MaxCapacity=5'

# JSTの0:30以降にスケールイン
aws application-autoscaling put-scheduled-action \
  --service-namespace rds \
  --schedule "cron(30 15 * * ? *)" \
  --scheduled-action-name 'scalein-nighttime' \
  --resource-id 'cluster:my-aurora-cluster' \
  --scalable-dimension rds:cluster:ReadReplicaCount \
  --scalable-target-action 'MinCapacity=2,MaxCapacity=5'

# 結果確認
aws application-autoscaling describe-scheduled-actions \
  --service-namespace rds \
  --resource-id 'cluster:my-aurora-cluster'

AuroraのScheduledActionはAWSマネジメントコンソール上からは設定できないためAWS CLIかTerraformでの設定で変更をかけることができます。そのためこの設定項目があることに最初は気がつくことができませんでした。ScheduledActionを入れてからはスケールがかなり安定したように思います。

クエリ改善

DBに負荷をかけているクエリ(より正しくはDBを長く使用したクエリ)を探すには、パフォーマンスインサイトが有用でした f:id:ksugahara08:20200525163309p:plain このように具体的なSQLがわかるので上位のSQLを改善(クエリチューニングしたりインデックスを作成したり)することでCPU使用率がかなり改善しました。

特に、弊社の中では一番大きなテーブルである勉強記録テーブルにインデックスを作成したことで、CPU使用率が大幅に下がりました。

改善後はピークタイムでもスケールアウトする必要がなくなりました 🎉

逆にサービス利用率が低い夜中はスケールインさせてDBの台数を減らすことでコストカットを実現することができました。

スマートフォンアプリの改善

スマートフォンアプリチームにも今回の負荷高騰対策に協力してもらいました。 サーバーへのリクエスト数を減らす改善を入れたことで、負荷が上がる以前のリクエスト数と同じ水準まで下がりました。圧倒的感謝っ・・・・!

みんなで総力戦をした結果

SREとサーバーサイドチームのAurora Auto Scaling対応、SQLチューニング、N+1解消、DBのインデックス作成。クライアントアプリチームのリクエスト数削減対応。 これらの対応を総力戦で行った結果、ラグナロクは回避され平和が訪れました。 それどころか負荷が大幅に下がりAuroraのインスタンス数を以前より減らすことさえできるようになりました 🎉

今後の課題

今回改善が進みましたが、新しい課題にも直面しました。例えば以下のようなものです。

  • Auroraがスケールアウトした際にはコネクション再生成のためにデプロイが必要になっています。RDS Proxyが正式リリースされればそれで解決されないだろうか 🤔
  • 分析用DBインスタンスはcluster内にいるとAuto Scalingのターゲット値が決めづらいのでclusterから外す形にするか検討したいです。
  • スケールインしたAuroraをMackerelから退役させるには手動で行わないといけませんでした。AWSインテグレーションを使って入れているので、メトリクスを取得してそのメトリクスがなければpower offにして退役というシェルも使えなさそうでした。

もし良い方法をご存知の方が入ればコメント等で教えて頂ければと思います。

まとめ

4月から負荷が高騰して緊急対応が必要になりましたが、大きな障害にならず、エンジニアみんなで乗り越えられたことは事業部内での成果にもなりました。また、このような事態に直面したことで、今まで優先的に対応できてなかったパフォーマンス改善タスクが一気に進んだように思います。

Aurora Auto Scalingを導入している事例をあまり見かけませんが、非常に便利なものでしたので皆さんも導入検討してみてはいかがでしょうか?