Sidekiqをスマートにスケーリングする方法 ― AWS Auto Scalingでワーカーを優雅に終了させる
今年の初め、私たちは専用サーバーのホスティング環境からAWSへ移行しましたが、この決断は大正解でした。自動化の仕組みを数多く整えたことで、サーバーの細かい設定やメンテナンスにかける時間を大幅に削減できています。:) この記事では、処理するエラー通知の量が変動した際に、私たちのインフラストラクチャ内部で何が起こるのかを詳しく解説します。
スケーリングの仕組み
私たちの処理パイプラインはSidekiqに大きく依存しており、これまで非常に安定して稼働しています。パイプライン内の作業の大部分は、Auto Scalingグループに属するインスタンスによって処理されます。Sidekiqのバックログ(未処理ジョブ)が増加するとCloudWatchアラームが発火し、グループ内に新しいインスタンスが起動します。そしてトラフィックが落ち着き、バックログが解消されると、インスタンスは順次終了されていきます。
より洗練されたスケーリング手法
この構成における唯一の課題は、インスタンスが終了される前に、Sidekiqワーカーがすべての作業を完了させられるようにしたいという点です。Sidekiqには完了前に中断されたジョブを再スケジュールする機能がありますが、可能な限りそれを避けたいと考えています。ジョブを再スケジュールして再実行させるよりも、あと数秒かけてでも完了させる方が効率的だからです。
幸いにも、Sidekiqには「現在のジョブ完了後に新しいワークを受け付けない」ようワーカープロセスに指示するメソッドが用意されており、終了準備としてワーカーをドレイン(排出)するのが簡単になっています。ポイントは、ワーカーたちにドレイン開始をどう伝えるかです。そのために、私たちはLifecycle Hooks、CloudWatch Events + Lambda、EC2 Simple Systems Manager(SSM)、そして少々のコードを組み合わせた構成を採用しています。
この記事の残りの部分では、同じ構成を再現して、インスタンスを「静かに、そして優しく」終了させる方法をご紹介します。
前提条件
まず、定義したルールに基づいてインスタンスを終了するスケーリングポリシーを持つAuto Scalingグループが必要です。さらに、以下の要件も満たしておく必要があります。
- インスタンスがSSM Agentを実行するように設定されていること
- インスタンスがSSMへのアクセス権限を持つIAMロールで実行されていること
- インスタンスがAuto Scaling APIとやり取りするための権限を持っていること
以下は、インスタンスロール用のポリシードキュメントの例です:
コードの実装
準備が整ったら、各パーツをつなぎ合わせていきましょう。プロセスの最後尾、つまりインスタンス上に配置されSidekiqのドレインを担当するスクリプトから始めて、逆順に組み立てていきます。
1. ワーカーを停止させるRubyコード
まず、ワーカーを停止状態にし、作業完了を待機するRubyコードです:
このコードでは、Sidekiq APIを使って現在のインスタンスで稼働中のSidekiqプロセスの一覧を取得し、新しいワークの受け付けを停止するよう指示した後、すべてのプロセスでアクティブなジョブがなくなるまで待機します。
2. 終了時にスクリプトを呼び出すシェルスクリプト
次に、インスタンスの終了タイミングで上記スクリプトを呼び出すシェルスクリプトです:
セットアップ部分が多くありますが、このスクリプトの本体は8行目から始まります。まず、ライフサイクルフックの処理を開始したことをAuto Scalingグループに通知します。これにはガードとしての役割もあります。このインスタンスがAuto Scalingグループによって終了予定になっていない場合、このコマンドは失敗し、ワーカーシャットダウンスクリプトは実行されません。本来このスクリプトは、インスタンスが終了予定の場合にのみ呼ばれるべきですが、少し防御的にしておくに越したことはありません。:)
処理を進められることが確認できたら、ワーカーをシャットダウンし、その後Auto Scalingグループに対して終了を続行するよう指示します。このAPI呼び出しを行わない場合、インスタンスはフックのタイムアウト期限が切れるまで起動し続けることになります(詳細は後述します)。
SSMドキュメントの作成
次に、このシェルスクリプトをトリガーする仕組みが必要です。ここでSSM Agentの出番です。SSM Agentはバックグラウンドで常駐し、SendCommand経由でドキュメントが届くのを待ち受けています。私たちのドキュメントは、Agentに対して終了スクリプトの実行を指示します:
ドキュメントを作成するには、EC2コンソールのサイドバー下部にある「Systems Manager Shared Resources」セクションの「Documents」リンクをクリックし、「Create Document」ボタンを押して、JSONをコンテンツボックスに貼り付けてください。
Lambda関数の作成
ドキュメントが準備できたら、インスタンスの終了タイミングでターゲットインスタンス上でそれを実行するLambda関数を作成します。以下がそのコードです:
PrepInstanceForTermination の部分はご自身で付けたドキュメント名に置き換える必要がありますが、それ以外のコードはそのままコピー&ペーストで使えます。
このLambda関数には(通常のLambda権限に加えて)いくつかの追加権限が必要です。以下がそのIAMポリシードキュメントです:
こちらも同様に、ドキュメント名は必要に応じて置き換えてください。
イベントルールとライフサイクルフックの設定
もう少しです、あと2つのステップだけ! CloudWatchイベントとライフサイクルフックを作成しましょう。
CloudWatchイベントの作成
CloudWatch管理コンソールで、サイドバーの「Events」をクリックし、「Create rule」ボタンを押します。これが、先ほど作成したLambda関数をトリガーする仕組みになります。

左側では監視したいイベントとその追加条件(例えば、どのAuto Scalingグループがこのイベントをトリガーするかの制限など)を選択し、右側ではルールのターゲットを選択します。今回の場合は、先ほど作成したLambda関数です。Lambda関数をターゲットにする場合は、デフォルト設定を変更する必要はありません。
ライフサイクルフックの作成
これが完了したら、EC2管理コンソールに戻り、管理対象のインスタンスが属するAuto Scalingグループへ移動します。「Lifecycle Hooks」タブをクリックすると、フックを作成できます。私たちの設定は以下の通りです:

注目するのは「Terminate」の遷移です。600秒(Heartbeat Timeout)経過しても何らかの理由でシェルスクリプト内のAPI呼び出しが行われなかった場合でも、終了処理を続行させる(Default Result: CONTINUE)ようにしています。これにより、Sidekiqワーカーの処理が完了していなくても、10分後にはいずれにせよインスタンスが終了されることになり、インスタンスが無期限に残留する事態を防げます。
まとめ
お疲れさまでした! これで、Sidekiqインスタンスを安全かつ優雅な方法で終了できるAuto Scalingグループが完成しました。ぜひ皆さんの環境でも活用してみてください!
P.S.: これらのパーツの組み立て方については、awslabsが公開している非常に役立つチュートリアルを参考にさせていただきました。ありがとうございます!
-
Ubuntu 19.04の新機能まとめ:アップグレードする価値はある?
新しいソフトウェアが好きな方なら、CanonicalがUbuntuの新バージョンをリリースしたことをすでにご存じかもしれません。ワクワクするニュースではありますが、果たしてアップグレードする価値はあるのでしょうか?逆に、新しいものにあまり興味がない方にとっても同じ疑問が当てはまります。古いUbuntuでも十分に仕事はこなせています。しかし、バージョン19.04は今抱えている問題を解決してくれるのでしょうか? 本記事では、Ubuntu 19.04の主要な変更点をわかりやすく解説し、アップグレードすべきかどうかの判断材料を提供します。 GNOMEのパフォーマンス向上 GNOMEは品質、パフォーマ
-
Rubyで例外発生時にローカル変数とインスタンス変数をログに記録する方法
簡単には再現できないバグに悩まされたことはありませんか?アプリをしばらく使い続けたユーザーの環境でだけ発生し、エラーメッセージやバックトレースを見ても原因がさっぱり分からない——そんな経験を持つ開発者は多いはずです。 こうした場面で役立つのが、例外が発生する直前のアプリの状態をスナップショットとして記録しておく手法です。たとえば、すべてのローカル変数とその値の一覧を取得できれば、デバッグの強力な手がかりになります。実は、これは思ったより簡単に実現できるのです。 この記事では、例外発生時点でのローカル変数をキャプチャする方法を紹介します。ただし、最初に重要な注意点があります。ここで紹介するテ