背景
サービスが成長しのユーザー数が増加してきたことによって、初期のサーバーの最小構成では ここ数日24時前後になるとロードアベレージが異常に上昇してしまって、 APIサーバーの応答時間が増加してしまうという問題がありました。
ロードアベレージがこの時最大16とかです。 CPUが1つの仮想マシンでこれは要するに全くちゃんと処理が追いついていない状況です
調査手順
サーバーからのレスポンスが遅くロードアベレージが高いということは、 ネットワークの問題というよりは何らかのハードウェア資源が不足してるという状況が考えられます。(ポートも資源だという話はおいておいて)
CPU負荷の確認
幸いCloudWatchのダッシュボードでCPUのUsageを監視していたので、すぐに見られたのですが このときCPU負荷は常に90%以上を推移していてCPUが何らか圧迫されているように見えました。
sarでCPU負荷の内訳を確認
sysstat
というパッケージを入れておくと定期的にCPUが何に取られているかログを取ってくれるで便利です。sar -P ALL | head
してみたら、%sysや%iowaitは低かったのですが、%userが40%近い値になっていました。これはつまり何らかユーザープロセスがCPUを圧迫しているということです。
topで負荷の高いユーザープロセスの概要を把握
top
コマンドは常識だと思いますが、topでしばらくCPUの負荷を観察したところ、定期的にbundle
コマンドの何かが3つほど呼び出されてそれら個別に20%ずつほどCPUを食っているようでした。
psでユーザープロセスの詳細を把握
ps aux --sort -pcpu | head -n4
で、全プロセスをCPU処理を食っている割合で降順にソートし、トップ3を取り出してみました。
すると、サーバー側でwheneverというgem経由で登録されている
定期的にユーザー端末のヘルスチェックを行ったり、定期的なログを吐き出すようなジョブたちがCPUの大部分を占めていました。
このあと他のプロセスも見てみたのですが、 他のプロセスは基本的にCPU負荷は5%にも届かないような状態が多く、圧倒的にこれらのプロセスがCPUを食っていました。
これらの状況から、 以前から気がかりだった定期的な処理とAPIサーバーが同じところに乗っているという問題は早急に解決しないと行けないレベルになったと判断しました。
対応手順
Job処理をバッチサーバーに移動
とりあえずAPIサーバーからこれらの処理を追い出したかったので、ActiveJobを実行しているSidekiqを APIサーバーから追い出そうと決め、とりあえずGoのスクリプト等がcronで呼び出されていたサーバー(以下バッチサーバー)に移動しようと決めました。
ソースコードをバッチサーバにもデプロイ
デプロイ処理を現在はCapistranoで行っていたこともあり、rbenvの登録や必要なライブラリのインストール等を適宜バッチサーバーに実施してやり、デプロイ対象にバッチサーバーを登録してやったところすんなりCapistranoでソースコードをバッチサーバーにデプロイできました。
具体的には、capistranoの環境ごとの設定ファイルconfig/deploy/production.rb
等に新たなrole
として
role :batch, %w(<サーバー接続情報>)
を登録してやりました。これで本番環境のデプロイ時にバッチサーバーにもソースコードがアップロードされるようになりました。
Wheneverによるcrontabの登録をバッチサーバーのみにする
こちらもwhenever_roleという値を
set :whenever_roles, :batch
という感じで設定してやればwheneverのcron登録は先程のbatchロールにだけ実施されるようなりました。
Redisをバッチサーバに移動
SidekiqではJobのバックエンドのキューはRedisが利用されているので、 ジョブを正しくキューするにはRedisもどこかに移さなくてはなりません。 一旦APIサーバーはAPIのみに注力しようと考え、暫定的にバッチサーバーにRedisも移動することにしました。 (本来はどこか専用のインスタンスに置くべきだと思います)
バッチサーバー上にRedisをインストールして起動して、 適宜ポートの開放やAWS上でのセキュリティーグループの設定等を済まして、 redis接続可能になったらOKです。 このとき、APIサーバーからもRedisにつなげておくようにしておくと、Sidekiqに付属しているWebコンソール経由で見張れるので便利です。
Sidekiq.configure_server do |config| if Rails.env.production? config.redis = { url: 'redis://<redisのホスト>:<redisのポート>', namespace: 'sidekiq' } else config.redis = { url: 'redis://127.0.0.1:6379', namespace: 'sidekiq' } end end Sidekiq.configure_client do |config| if Rails.env.production? config.redis = { url: 'redis://<redisのホスト>:<redisのポート>', namespace: 'sidekiq' } else config.redis = { url: 'redis://127.0.0.1:6379', namespace: 'sidekiq' } end end
..... mount Sidekiq::Web, at: "<どこか適切なパス>"
この段階で、cron経由やAPIサーバー経由でジョブがバッチサーバー上のredisに登録される様になりました。 Jobの実行自体はSidekiqが実行されているAPIサーバー上で実施されているので、 最後にSidekiqもバッチサーバー上に追い出す必要があります. (こちらも本来はSidekiq用の専用サーバーを立ててやってAutoScalingとかするのが良いと思います)
Sidekiqをバッチサーバー上でのみ実施する
こちらもCapistranoのお陰で超簡単です。
... set :sidekiq_role, :sidekiq
とsidekiq_role
を設定してやって、wheneverの時と同様に各環境ごとにサーバーを設定してやれば完了です。これで、デプロイ時にバッチサーバー上でSidekiqが実行されるようになります。
動作を確認しながら、段階的にAPIサーバー上でのcronを消し、sidekiqもkill -USR1 <pid>
で動きを止めて様子を見ながらkill -TERM <pid>
で止めて、Webの監視画面からsidekiqが正常に動いていることを確認したら無事完了です。
Fluentdをバッチサーバーにインストール
Jobの中にはFluentd経由でログを収集しているインスタンス向けてRails内部からログを送信するタイプのものもありました。バッチサーバー上のsidekiqのログを見ていたらFluentdとの接続エラーが出ていたので、適宜Fluentdの設定をしました。これはAPIサーバー上にFluentdを設定したときのItamaeのレシピが残っていたので、それを適宜再利用してすんなり完了しました。
バッチサーバーをスケールアップ
バッチサーバーは本当に1日に一回程度Goのちょっとしたスクリプトが動くだけだったのでt2.micro
で運営していても問題がなかったのですが、
今回の移行で負荷が増大する懸念があったので、インスタンスタイプの変更を検討しました。
本番サーバー上でのメモリ使用率はそれなりに80%程度と高かったのですが、ps aux --sort -rss
の結果を確認したところこれらはunicorn
のワーカーが使用していもので、バッチジョブとは無関係でした。一方でCPUの消費はほぼバッチによるものでした。
また、バッチの定期的なものだけでなく、オンラインで実施されるJobも含めて検討するとサービスの実施時間帯中のスポットで一気にユーザーの需要に合わせて高まる傾向が多く、ずっとCPUを消費するというよりは必要なときにパワーの出るタイプが望ましいと判断しました。
また、サービス拡大の時期であり、需要の上限が用意には見積もれず、またオートスケールを導入してしまうよりは一旦サービス中の負荷を確認できたほうが良いだろうと判断し、一旦大きめのインスタンスタイプで余裕を持つことにし、インスタンスタイプはt2.medium
を選択しました。
結果
CloudWatchで監視
その晩は果たして上記の判断が正しいものだったのか不安だったので、CloudWatchでAPIサーバー及びバッチサーバーを見張ることにしました。
実施直後ロードアベレージが一気に低下
まず、上記の施策を実施した時点でロードアベレージががくんと下がりました
施策の実施直前は色々とサーバー内で必要なプロセスを止めたり、色々と作業をしていたので一時的にロードアベレージは上がっていますが、実施後に、一気に平均のロードアベレージが下がりました。負荷の高い時間帯でなくても平均してロードアベレージは0.4
程度あったのですが、施策を実施した直後0.04
程度に一気に下がりました。
サービス時間帯中もロードアベレージが異常増が解決
実施して一晩たった記録ですが、最近発生してきていた異常な状態がなくなったことがわかります。やはりサービス時間野メイン時間には多少ロードアベレージが上がっていますが、一番高いもので1.99
です。
レスポンスタイムの上昇も解決
ここしばらくでユーザー数が一気に伸びたこともあり、 2日連続で通信エラーに悩まされていて眠れない夜が続いていたのですが、今回の施策を実施して、一気に異常なピークが発生しなくなりました。
バッチサーバーのインスタンスタイプは妥当かどうか?
バッチサーバーはCloudWatchで見たところCPU負荷はピーク時にCloudWatch上で15%程度平常時には12%程度で、
メモリ使用率は30%程度です。t2.micro
はコアが2つなので、実際は24%~30%
程度ですので、ベースラインパフォーマンスの40%
は常に下回っている状態となりました。
参考リンク - AWS T2インスタンス
T2インスタンスはベースラインパフォーマンスを下回っていると自動的にCPUクレジットが溜まっていき、必要なときに頑張ってくれるのですが、現状のインスタンスタイプだとピーク時にもクレジットはたまり続けて居ますね。とはいえ、まだ一日しか様子は見ていないので、ここで小さくするのは早計だと思うので、一週間から一ヶ月程度様子を見た上で判断したいと思います。
今後の検討事項
デプロイをDocker化
開発環境はDockerでビルドできているので、本番環境でもDockerに移行してECS等で管理したい。 Dockerイメージにするとエントリポイント等から柔軟に何を実行するか調節できそうで、オートスケール等でも 用意に対応できると思う。