
はじめに
こんにちは、プロダクト開発部の勝間田です。
非同期処理は、即時の応答が不要な処理をバックグラウンドで並行処理することでユーザー体験を向上させるものであり、私たちのサービス TUNAG(ツナグ)では主にSidekiqを利用しております。
即時の応答が不要な処理とはいえ、そこで大きな遅延(Latency)が発生してしまえば、ユーザー体験を損ねることにつながってしまいます。
Sidekiqの設定でキューの重みづけを行い、ユーザーへのインパクトが大きいジョブが優先的に処理されるよう工夫はしていましたが、優先度の低いキューとはいえ、その遅延が大きくなることがありUXに少なからず影響が出始めていました。
これまでスケーリングについてはECSタスクのCPU使用率ベースのオートスケーリングに任せていました。
それとは別で定期実行によりSidekiqのキューが一定数以上滞留してしまった場合はSlackにアラートを通知し、エンジニアがそれに気づいて手動でECSのタスク数を増やす、という運用を行なっていました。
こうした属人的な対応・検知から遅延解消までのタイムラグ・休日対応が難しい点などが課題となっており、これらを解消すべく今回の仕組みを導入するに至りました。
またこの記事では下記のことについては触れませんので、あらかじめご了承ください。
- CloudWatchカスタムメトリクス送信の具体的な実装
- 各AWSリソースの具体的な設定手順
余談ですが、最近サブサービスでSolid Queueを導入しました。導入の背景や手順について別のブログで紹介しておりますので、ご興味のある方はぜひご覧ください。
対応内容
これらの課題を解決するため、監視する指標をCPU使用率からSidekiqのキューLatency(遅延時間)そのものに変更することにしました。
やりたいこと:キューのLatencyが一定の閾値を超えたら、エンジニアの手を介さず自動でECSタスクをスケールアウトさせる
具体的な対応内容は以下の通りです。

1. LambdaでアプリのAPIよりSidekiqのLatencyを取得
Sidekiqには、キューやワーカーの状態を取得するためのAPIがあります。
github.com
以下のようなコードで簡単にキューの情報を取得することができます。
stats = Sidekiq::Stats.new stats.queues.keys.map do |key| queue = Sidekiq::Queue.new(key) { name: queue.name, size: queue.size, latency: queue.latency } end
このSidekiq APIを利用してLatencyを取得する方法として、「LambdaからSidekiqのデータストア(Redis)に直接接続する」のではなく、「アプリケーション(Rails)側に、Sidekiq APIを呼び出して結果をJSONで返す専用のエンドポイントを実装する」ようにしています。
このようにすることで以下のメリットがあると思います。
- Lambda側でRedisとの接続が不要になるので、あらゆる設定をする必要がない。
- 将来SidekiqのバージョンアップなどでAPIの仕様が変わったとしても修正はアプリケーションのみとなり、Lambda側のコードは一切変更する必要がなく返却されるLatencyに集中すればよくなる。
2. CloudWatchカスタムメトリクスにQueueのLatencyを送信
Latency情報をオートスケーリングのトリガーとして使えるよう、CloudWatchに送信します。
TUNAG(ツナグ)では、処理する機能が大きく異なる非同期処理についてそれぞれ異なるECS Serviceで動かしているため、個別にオートスケーリングさせる必要がありました。
そのためメトリクス名は同じ Latency としつつ、CloudWatchのディメンションを利用してECS Serviceごとに設定することで、CloudWatchアラームがLatencyを個別に監視し、対応が必要なECS Serviceのみをスケールアウトさせることができます。
3. EventBridgeをLambda実行のトリガーに設定
用意したLambdaを定期実行するために、トリガーとしてEventBridgeのスケジュールを設定しました。
スケールアウトが後手後手にならないようある程度短めにLambdaを実行するようルールを設定しています。
4. CloudWatch アラームによる監視と発火
CloudWatchに Latency メトリクスが送信されるようになったので、次はこのメトリクスを監視するCloudWatchアラーム を作成します。
アラームも個別に作成することができ、サービスごとに独立したスケーリングができるようにしています。
下の画像のように、ECS Serviceで分けたディメンション単位でアラームを設定しています。

5. ECSオートスケーリングポリシーの実行
作成したCloudWatchアラームが ALARM 状態になることをトリガーとして、ECSタスクを増やすスケーリングポリシーを設定します。これにより、Latencyが高くなったらECSタスクが設定した数だけ自動でスケールアウトするようになります。
ここまででやりたいことの「キューのLatencyが一定の閾値を超えたら、エンジニアの手を介さず自動でECSタスクをスケールアウトさせる」が実現できます。
ただスケーリングポリシーを設定する上で、コストとパフォーマンスのバランス調整に悩みました。「いつ、どのくらいタスクを増減させるか」という判断には、2つの課題があります。
-
スケールアウトから解消までのタイムラグによる「過剰スケールアウト」のリスク
ECSタスクを増やしてから、そのタスクが起動し、キューのLatencyが実際に解消されるまでにはタイムラグが発生します。
もし、アラームが発火し続ける間タスクを増やし続ける設定にすると、本来なら(起動中のタスクで)捌ききれる量だったにも関わらず、過剰にタスクを増やしてしまい、無駄なコストが発生する恐れがあります。 -
スケールインによる「ピーク時パフォーマンス低下」のリスク
非同期処理の特性上、平常時はLatencyが0に近い状態が続きます。もしLatencyの値だけを見てタスクを減らすと、ピーク時以外の時間帯でタスクが必要最低限まで減りすぎてしまいます。
その結果、次のジョブのピークが来た際に、スタート時点のタスク数が少なすぎるため、そこからスケールアウトを始めても処理が間に合わなくなる恐れがあります。
上記のような課題があったため、現状は下記のような対策によりバランスをとっています。
-
スケーリングポリシーに適度なクールダウン期間を設定
ECSタスクをスケールアウトさせ、新しいタスクが起動してジョブを処理し始めてもすぐにLatencyが解消されるわけではありません。このタイムラグを考慮しないと、Latencyがまだ高い状態を見てアラームが発火し続けることで必要以上にタスクを増やしてしまい、過剰なスケーリングが発生する可能性があります。
そのためスケーリングポリシー側に適切なクールダウン期間を設けることで、「一度スケールアウトしたら、次のスケーリング判断まで一定時間待機する」ように設定し、過剰なスケールアウトを防ぐようにしました。ただこの期間についても増やすタスクの数と同様でサービスによって適切な値が異なってくると思います。最初は短めに設定しつつ、Latencyの解消具合を見ながら微調整していくと良さそうに思いました。 -
キューのLatencyによる「スケールイン」をあえて設定しない
非同期処理の特性上、アラームがOK(Latencyが低い)状態の時間帯のほうが多いため、OK状態をトリガーにタスク数を減らしてしまうと、次のジョブのピーク(エンキューが急増する時間帯)が来た際に、タスク数が少ない状態からスケールアウトをすることになります。
Latencyによりスケールアウトした時間帯は、継続してジョブがエンキューされる可能性が高い時間帯です。クールダウン期間もあるため、少ないタスク数からのリカバリーでは間に合わなくなる恐れがあります。
そのため、Latencyベースでは「増やす」ことだけを担当させ、タスクを「減らす」処理については、従来から設定してあるCPU使用率ベースのスケーリング(CPU使用率が低ければ減らす)に引き続き任せる構成としました。
現状はこのような設定で運用しております。
これらの設定についてもサービスの特性やピーク帯の有無等で変わりそうです。より良い設定があればぜひ知りたいです。
結果
Latencyベースのオートスケーリングを導入した結果、これまでキューの滞留件数が増えるたびに飛んでいたSlackアラートが、ほとんど飛ばなくなりました。
手動対応についても導入後は一度も行っておりません。
これによりエンジニアの手動対応コストを削除し、処理遅延によるUXの悪化を未然に防ぐことができました。
まとめ
今回ご紹介したSidekiqのLatencyをトリガーにしたオートスケーリングは、AWSの標準的な機能(Lambda, EventBridge, CloudWatch, ECS Auto Scaling)を組み合わせることで実現できるため、導入自体のハードルは比較的低いと感じました。
今後はより細かなチューニングもしていければと考えています。現状では閾値を超えた際に決まったタスク数を増やしていますが、Latencyの値やキューの種類によって増やす数を変えたり、監視するキューをより厳選したりなどコストパフォーマンスとのバランスを考えながら改善していきたいです。
最後に
スタメンでは、Rubyエンジニアに限らず全技術領域で、プロダクトを成長させていくエンジニア、デザイナー、プロダクトマネージャーの方を募集しています。
ご興味いただけましたらぜひご応募いただけますと嬉しいです。
皆さんとお話できることを楽しみにしています。
コメント