背景
- Heroku の Hobby Plan で運用していたサービスがテレビの取材を受け、ひるおびで放送された
- 放送1週間前に「サイトが落ちないように何か対策できないか」と相談され、そこから調べたり準備したりしたことについて書く
事前調査
テレビ放映時のアクセス数を見積もり
下記のブログを読み、1分間に450アクセス程度という見積もりで始めました。
現在の構成のスペックについて
元々の構成は下記の通りでした。
- アプリケーションサーバー: Rails アプリケーションを Heroku Hobby プランで運用
- 非同期処理: Sidekiq を Heroku Hobby プランで運用
- DB: Heroku Postgres の Hobby Basic プランで運用
- キャッシュストア: Heroku Redis の Hobby Dev プランで運用
- フロントエンド: React SPA を AWS CloudFront で配信
次に大量のアクセスが集中した場合にボトルネックになりそうな指標を洗い出しました。
- Database Connection
- Redis Connection
- CPU Usage
- Memory Usage
対応方針の検討
フロントエンドは CloudFront なのでまず落ちないと考え、バックエンドの対策のみに集中できました。
本来は本番と同じ構成で負荷試験を行い、現在の構成で秒間何アクセスまで耐えられるのか調べるべきなのですが、相談を受けたのが1週間前だったためそこまでの対策はできず。
まずは現状のアプリケーションの状態を調べるため、また当日何か異常が発生した時に速やかに原因を特定するため、サーバーのモニタリングツールとして NewRelic を導入することを決めました。
バックエンドの CPU Usage, Memory Usage 等についてはアプリケーションサーバーをスケールアウトすればいくらでも対応できますが、その際にDBへのコネクション数が上限を超えて死ぬケースが容易に想像できました。
そのため、アプリケーションサーバーのスケールアウトおよびDBのスケールアップを基本方針としました。
最後に、恐らくテレビを見てアクセスするユーザーの多くはトップページだけざっと見て去っていくだろうと考え、トップページにアクセスするだけで発生する API アクセスの中で DB アクセスを抑えられるように Redis を使ってキャッシュする方針としました。
NewRelic の導入
Heroku に NewRelic を導入する場合、 Heroku のアドオンに NewRelic APM を導入した上で Gemfile に newrelic_rpm を追加するだけで済みます。
導入後に Heroku ダッシュボードのアドオン一覧から NewRelic APM をクリックすると、このようなダッシュボードを閲覧することができます。
また今回の場合 API 通信は GraphQL を用いていたため、デフォルト設定だと NewRelic のエンドポイント別の response time が全て Graphql#execute エンドポイントにまとまってしまっていました。
そのため GraphqlController 上で operation name を transaction 名として上書きする処理を追加しました。
class GraphqlController < ApplicationController
before_action :set_transaction_name
def set_transaction_name
NewRelic::Agent.set_transaction_name(params[:operationName])
end
end
これで特にどこのページ・処理に問題が発生しているかすぐ特定できるようになりました。
DB のスケールアップ
DB のスケールアップに当たり、まずはどれだけのコネクション数上限を確保するべきなのか検討しました。
Heroku Postgres のプランでは、 Hobby Basic が上限20、次の Standard 0 が上限120となっています。
Heroku のダッシュボードを見ると、現在のコネクション数は12でした。
Heroku の Dyno (サーバー1台に相当する Heroku の概念) 1台あたりいくつのコネクションが発生するのか調べてみたところ、
- Rails アプリケーション1つにつき puma のスレッドが5つ立ち上がる
- puma のスレッドと同じ数のコネクションプールが確保される
ということだったので、 dyno 1台あたりコネクションプール5つ程度ということで、上限120なら worker dyno の接続と合わせても20台程度までスケールアウトできそうなことが分かりました。
Heroku 上でデータベースの移行をする場合、次の手順で行います。
- Heroku Postgres のデータベースを1つ追加する
- アプリケーションをメンテナンスモードに切り替える
- 旧DBから新DBにデータをコピーする
- 新DBをメインDBに指定する(ここで環境変数 DATABASE_URL が切り替わる)
- メンテナンスモードを解除する
- 旧DBを削除する
Heroku CLI を使うと下記のコマンドで作業が可能です。
# create a new DB
$ heroku addons:create heroku-postgresql:standard-0 -a your-app-name
$ heroku pg:wait -a your-app-name
$ heroku pg:info -a your-app-name
# turn on maintenance mode
$ heroku maintenance:on -a your-app-name
# replace old database by new one
$ heroku pg:copy DATABASE_URL HEROKU_POSTGRESQL_XXX -a your-app-name
$ heroku pg:promote HEROKU_POSTGRESQL_XXX -a your-app-name
# turn off maintenance mode
$ heroku maintenance:off -a your-app-name
こうして DB を1つ上の Standard 0 プランに移行することができました。
Heroku Dyno Type のスケールアップ
次にアプリケーションサーバーをスケールアップします。
現在の Hobby プランでは水平スケーリング機能が提供されていないため、次の Standard 1X に切り替えることにします。
注意点としては下記の2点があります。
- 自動スケーリング機能は Performance プラン以上でないと提供されない
- web dyno を Standard プランに移行したい場合、 worker dyno も合わせて Standard プランに移行しなければならない
Dyno Type のスケールアップ・Dyno 数のスケールアウトはダッシュボードから手軽に操作可能なので手順は省略する。
トップページから呼ばれる API のキャッシュ
特にトップページにアクセスが集中すると想定して、トップページを表示するだけでコールされる API の中で DB アクセスが発生しないよう Redis に結果をキャッシュするという実装を行いました。
- クエリパラメータが存在しない時のみキャッシュを参照する
- 1時間に1回リフレッシュして最新のデータを反映する
といった要件を考慮したので多少時間がかかりましたが、何とか放映当日までに実装を完了することができました。
なお放映後しばらくしてアクセス数が落ち着いた後は revert して元に戻しました。
結果
その日の11:00頃からサービスの紹介が始まり、 Google Analytics や NewRelic のメトリクスをリアルタイムで見ていましたが、結果的にアクセス数は下記のようなボリュームに収まりました。
- ユーザー数の累計は600名程度、瞬間最大風速は1分間に 192 人程度
- API コール数の瞬間最大風速は10分間に 3,200 件程度
- レスポンスタイムは 99 percentile 地点で概ね 350ms 以内
特にエラーを返すこともレスポンスタイムが極端に悪化することもなく、無事に山場を越えました。
反省点
概ね問題なく終わったものの、反省点を1つ挙げるなら Redis の connection 数を甘く見積もっていた点でしょうか。
当初 Rails アプリケーション1つにつき Redis の connection は1つになるのかなと思っていましたが、ダッシュボードで確認した限りでは Redis connection 数が最大で 18 になっていた瞬間があり、 plan limit が 20 だったため焦りました。
1件だけ Sentry に Redis 接続関連のエラー通知が届きましたが、内容を見ると僕自身が Sidekiq のジョブ処理状況ダッシュボードを見ていた時のエラーだったので、管理画面をそっ閉じしました。
それ以外は Sentry のエラーログも特に無く、平穏なまま終わりました。
まとめ
今まで主に Rails と React を書くばかりの野良エンジニアだったため、初めてまともにインフラ的な部分に注力する1週間を過ごすことができました。
スケジュールがタイトで寝不足に苦しむこともありましたが、結果的には非常に勉強になる経験を積むことができたなとポジティブな気持ちです。
お昼にサービスを紹介されるとだいたい600人くらいの人が見に来るということが分かったので、この知見がまた別の誰かの役に立てば良いなと思います。