Let's write β

プログラミング中にできたことか、思ったこととか

最小構成サーバーでは負荷に耐えられなくなってきたので、SidekiqのJob処理を別サーバーに移動した

NewAWSArchitecture.png

背景

サービスが成長しのユーザー数が増加してきたことによって、初期のサーバーの最小構成では ここ数日24時前後になるとロードアベレージが異常に上昇してしまって、 APIサーバーの応答時間が増加してしまうという問題がありました。

LoadAverageError.png

ロードアベレージがこの時最大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ロールにだけ実施されるようなりました。

Tips: どのロールに何を割り当てるかはdeploy.rbでまとめておくと楽

上記の話はプロダクション環境のみに関係のある話でステージング環境は以前すべて同じサーバーで問題ありません。なので、config/deploy.rbset :whenever_roles, :batchを書いておいてやって、具体的なbatchロールに属するサーバーはproduction.rbstaging.rbで設定する形にしておくと楽です。staging.rbではたとえばメインのサーバーと同一の接続情報にしておくことで同じサーバーですべて実施されます。

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サーバー及びバッチサーバーを見張ることにしました。

実施直後ロードアベレージが一気に低下

まず、上記の施策を実施した時点でロードアベレージががくんと下がりました

スクリーンショット 2017-07-30 15.25.46.png

施策の実施直前は色々とサーバー内で必要なプロセスを止めたり、色々と作業をしていたので一時的にロードアベレージは上がっていますが、実施後に、一気に平均のロードアベレージが下がりました。負荷の高い時間帯でなくても平均してロードアベレージは0.4程度あったのですが、施策を実施した直後0.04程度に一気に下がりました。

サービス時間帯中もロードアベレージが異常増が解決

スクリーンショット 2017-07-30 15.33.29.png

実施して一晩たった記録ですが、最近発生してきていた異常な状態がなくなったことがわかります。やはりサービス時間野メイン時間には多少ロードアベレージが上がっていますが、一番高いもので1.99です。

レスポンスタイムの上昇も解決

スクリーンショット 2017-07-30 15.35.33.png

ここしばらくでユーザー数が一気に伸びたこともあり、 2日連続で通信エラーに悩まされていて眠れない夜が続いていたのですが、今回の施策を実施して、一気に異常なピークが発生しなくなりました。

バッチサーバーのインスタンスタイプは妥当かどうか?

バッチサーバーはCloudWatchで見たところCPU負荷はピーク時にCloudWatch上で15%程度平常時には12%程度で、 メモリ使用率は30%程度です。t2.microはコアが2つなので、実際は24%~30%程度ですので、ベースラインパフォーマンスの40%は常に下回っている状態となりました。 参考リンク - AWS T2インスタンス

T2インスタンスはベースラインパフォーマンスを下回っていると自動的にCPUクレジットが溜まっていき、必要なときに頑張ってくれるのですが、現状のインスタンスタイプだとピーク時にもクレジットはたまり続けて居ますね。とはいえ、まだ一日しか様子は見ていないので、ここで小さくするのは早計だと思うので、一週間から一ヶ月程度様子を見た上で判断したいと思います。

今後の検討事項

デプロイをDocker化

開発環境はDockerでビルドできているので、本番環境でもDockerに移行してECS等で管理したい。 Dockerイメージにするとエントリポイント等から柔軟に何を実行するか調節できそうで、オートスケール等でも 用意に対応できると思う。