MNTSQ Techブログ

「MNTSQ(モンテスキュー)」のTechブログです。

2千万件のログ分析から始めるRedis -> SQS移行とオートスケール

MNTSQ Tech Blog TOP > 記事一覧 > 2千万件のログ分析から始めるRedis -> SQS移行とオートスケール

はじめに

システムが成長し、扱うデータ量やトラフィックが増大してくると、非同期処理の安定性とスケーラビリティがサービス全体の課題となります。

弊社のサービスの根幹部分はRuby on Railsを採用しているため、長らく標準の非同期処理のキューとしてResque (Redis) を使用していました。しかし、サービス規模の拡大に伴い、Redisベースの運用では「ワーカーのオートスケール最適化」が困難であるという課題が浮き彫りになってきました。

本記事では、この非同期処理のバックエンドを Amazon SQS に移行した背景と、移行に伴って行ったキュー設計・オートスケール最適化の取り組みについて紹介します。

なぜSQSなのか

非同期処理でのオートスケール実現の課題

変化の激しいワークロードに対して、理想的なオートスケールを実現するためには、以下の要素が必要不可欠です。

グレースフルなシャットダウン : オートスケールインを行うということは、処理途中であっても中断が発生しうるということです。このようなイベントに対して、適切なハンドリング・リトライを行える必要があります。

細かなメトリクスを取得できる : オートスケールを運用に乗せるためには、実際のワーカー台数の変化を、メッセージの滞留数や滞留時間、同時実行数などと照らし合わせ、適切な設定になっているかを評価する必要があります。また、オートスケールの条件もこれらのメトリクスを参照することになります。

RedisからSQSへ ─ 移行のメリットと留意点 ─

Redisをバックエンドに使用していると、キューの滞留数やジョブの状態をリアルタイムで詳細に把握するために、独自のメトリクス収集の仕組みを構築・維持しなければなりません。また、エラー時のリトライ機構についても、アプリケーション側で慎重に設計・実装する必要がありました。

これらの課題を解決するため、バックエンドを Amazon SQS へ移行することを決断しました。

SQSの可視性タイムアウトを利用すれば、ジョブ実行中のエラーやオートスケールに伴う中断が発生しても、メッセージを安全にキューへ戻し、自動で再試行できます。これにより、複雑なエラーハンドリングを行うことなく、グレースフルシャットダウンを容易に実現できます。また、標準で提供される滞留数や滞留時間といった強力なメトリクスをそのままオートスケールのトリガーに利用できるため、独自のモニタリング基盤を維持するコストも不要になります。柔軟にスケールし、かつ壊れにくい基盤を作るには、SQSのマネージドな特性をフル活用することが最適解だと判断しました。

注意点として、SQSには"優先度"の概念がありません。Redisベースのジョブキュー(Resque/Sidekiqなど)では1つのキュー内でジョブの優先度を表現できますが、SQSではキューそのものを分割して、優先度の高いジョブが滞留しないような設計を取る必要があります。また、at-least-once配信を前提としたジョブの冪等化や、可視性タイムアウトを超える長時間ジョブの二重実行対策といった、アプリケーション側で事前に手当てすべきポイントもあります(これらの具体的な対応については後述します)。

つまりSQS移行においては、サービスのジョブ特性を正しく理解した上で、最初に適切なキュー構成を設計できるかが成否を分けます。

2千万件のログからサービス特性を分析する

最適な設計を行うため、ジョブ全体の傾向や特徴を把握する必要があります。

「ワーカーのオートスケールの最適化」とは、即ち「顧客体験」と「運用コスト」の最適化です。なんとなくワーカーが増えたり減ったりしているという状況はゴールではありません。弊社のサービスではアップロードした契約書の条項の分解や、検索用のインデックス作成など、顧客体験に関わる処理も非同期で行われます。このような処理が、特定のテナントや初期導入に伴う大量解析や、実行時間が比較的長時間にわたるジョブの影響(所謂ノイジーネイバー問題)を受けないような設計にしたいところです。また、無駄なスケールアウトはコスト観点から好ましくないです。

ということで、以下が弊社サービスでのジョブの特徴です。(見やすいように対象を絞って表示しています)

処理時間別 ジョブの実行数のグラフ(対数軸)

このグラフは、データベースのジョブの実行を管理するテーブルのここ半年分のデータ約2千万件を集計したものです。(ジョブの実行ログをデータベースに蓄積していれば、リードレプリカでSQLを叩いてExcelで集計するだけなので、特別な分析基盤がなくても気軽に行えます)縦軸がジョブの実行数、横軸がジョブの実行時間を表します。(横軸も対数チックな軸になっています)また、同じジョブでも実行時間にバラツキがあるため、同一のジョブは同じ色で表現しています。

このグラフから、以下のようなワークロードの特性が見えてきました。

  • 78%のジョブは1秒未満、99%のジョブは10秒以内に完了する。1秒未満のジョブは営業時間中に分間200件以上積まれる一方、10秒超えのジョブは分間1件未満しか積まれない
  • 実行時間に数秒から数時間のバラツキがあるジョブが存在する

また、弊社のサービスでは以下のようなオペレーションが発生する点も考慮する必要があります。

  • 初期導入: 顧客の運用開始の準備として大量のドキュメントをアップロードし、ファイル変換や解析を行う作業がある
  • 再インデクシング: 検索機能の拡張などで、大量の検索用インデックスを更新・再作成する作業がある

よって、上記を考慮しつつ、10秒未満のジョブをいかに滞留させずに捌けるかが設計における重要な課題でした。

キューの再設計

設計の出発点はシンプルです。99%を占める短時間ジョブを、長時間ジョブやバーストワークロードに巻き込まれず安定して捌くこと。これを実現するため、キューを「優先度ベース」と「機能ベース」の2軸で分割しました。

前提: アプリケーション側での事前整備

SQSのクライアントとしてはshoryukenを採用しました。

キュー設計の話に入る前に、SQSをバックエンドにする上でアプリケーション側で先に手当てした2点に触れておきます。これらは設計段階で必要になることが予想できたため、本格的なチューニングに入る前に済ませました。結果として、後段のチューニングフェーズではこの2点が問題になることはありませんでした。

ひとつめは、ジョブの冪等化です。SQSはat-least-once配信のため、同一メッセージが複数回配信される前提で実装する必要があります。移行に伴ってジョブの抽象クラスを見直し、すべてのジョブが冪等に動作するよう統一しました。

ふたつめは、長時間ジョブにおける可視性タイムアウトの動的延長です。SQSの可視性タイムアウトは、処理中のメッセージが他のワーカーに再配信されないようにするための仕組みですが、ジョブの実行時間が可視性タイムアウトを超えると、処理中にもかかわらず別ワーカーで二重実行されてしまいます。これを防ぐため、長時間ジョブに対してはアプリケーション側でハートビート的に可視性タイムアウトを延長する実装を入れました。これによりインフラ側では可視性タイムアウトのチューニングにシビアになる必要がなくなったのは設計上のポイントです。

優先度ベースのキュー: 短時間ジョブを守るための4段構成

通常業務のジョブは、実行時間と投入パターンの2軸で4つのキューに振り分けます。

キュー名 用途 想定される投入パターン
critical 顧客体験に直結し時間にシビアなJob(UIからのファイルアップロード・解析、メール発信など) 単発・低頻度
high 顧客体験に直結するが大量投入される可能性があるJob(Zip解凍やそれに伴う解析など) バースト
default 顧客業務に影響するが即時性不要なJob(メール連携・定時タスク起点、外部連携起点) 中頻度
low 実行時間が10秒を超える可能性のあるJob(台帳のexport/importなど) 不定

設計の肝は2つあります。

ひとつめは、実行時間 10秒 を境界に 短時間ジョブキュー(critical / high / default)と 長時間ジョブキュー(low)を分離すること(以下、10秒ルール)。ジョブ全体の99%は10秒以内に完了する一方、残り1%には数十秒〜数時間に及ぶジョブが混ざっています。これを同じキューに流すと、1本の長時間ジョブがワーカーを占有してしまいます。これでは滞留時間をオートスケール条件にしたとき、無駄にスケールアウトをしてしまいます。しかし、10秒ルールを導入することにより、短時間ジョブキュー側ではSQSの滞留時間メトリクスを直接オートスケールのトリガーに使えるようになります。

ふたつめは、短時間ジョブをさらに「UI起点で単発投入されるもの(critical)」と「UI起点だが大量投入されうるもの(high)」で分けたこと。たとえば「Zipアップロード後の一括解析」は、1回の操作で数百件のジョブが一気に積まれる可能性があります。これを critical に流すと、1人のユーザーが大きなZipをアップロードしただけで、他のユーザーの単発操作が裏で詰まる、という典型的なノイジーネイバー問題が発生します。バースト性のあるワークロードを high に隔離することで、critical は常に低い滞留数を保ち、最も厳しいSLOを当てられるようにしています。

機能ベースのキュー: 大規模オペレーションの隔離

優先度ベースの分割だけでは扱いきれないのが、冒頭でも触れた初期導入と再インデクシングです。あるテナントの大量処理が他テナントの通常業務を圧迫しないよう、このような特例のオペレーションには、通常業務とは完全に分離した専用キューを用意しました。

キュー名 用途
introduction 初期導入など、通常業務と分離したいJob(ファイルアップロード・解析・インデクシング)
reindexing 全件indexing / 権限変更時の大量indexing用

これらの機能ベースキューは普段はワーカー0台で待機し、必要なタイミングでのみ起動します。そのため、キュー区分が増えることによる定常的なコスト増は発生しません。隔離したい単位でキューを切る判断を、コストを気にせず行えるのがSQS + オートスケール構成の利点です。

キューごとのオートスケール戦略

各キューには、特性に応じたオートスケール設定を割り当てています。短時間ジョブキュー(critical / high / default)は滞留時間をトリガーにスケールアウトし、こまめにスケールインする運用にしています。「滞留時間数秒以内」という明確なSLOがあるため、滞留時間ベースで反応させるのが最もシンプルです。一方、長時間ジョブキュー(low)はメッセージ数がワーカー最小数を超えたらスケールアウト、キューが空になったらスケールインとしており、SLOを設けない代わりにMAX台数を絞ることでコストを抑制しています。

モニタリングと改善のループ

キューを再設計して移行が完了しても、それで終わりではありません。設計が想定通りに機能しているかを継続的に観測し、ズレを見つけて細かく修正していくフェーズこそが、オートスケール運用の本番です。設計時点で全てを正解にすることは不可能なので、動かしながら最適化する前提でモニタリング基盤を整えました。

Datadogによるモニタリング

まず、Datadog上にキュー運用のためのダッシュボードを構築しました。ダッシュボードでは主に以下の観点を一覧できるようにしています。

  • ワーカー台数の推移
  • キューごとの滞留数
  • キューごとの滞留時間
  • 処理中メッセージの数

これらを並べて眺めることで、「high キューだけ滞留時間が伸びているがワーカー台数が頭打ちになっている → スケールアウト上限が低すぎる」「default キューはスケールアウトしているのに、処理中のメッセージ数が常に少なく滞留時間が慢性的に長い → 10秒以上かかることがある長時間Jobが紛れ込んでいる」といった具体的な問題を即座に切り分けられるようになりました。

加えて、前述の2千万件分析にも使ったジョブ実行履歴テーブルに対し、enqueue時のキュー名の記録や検索用インデックスの追加といった改修を行い、ダッシュボードで気になった事象をSQLで即座に深掘りできるフローも整えています。

ダッシュボードが「能動的に見にいく」仕組みである一方、異常をプッシュで検知する仕組みとして、Datadogモニタも「キューの種類 × 指標」の組み合わせで網羅的に仕込みました。滞留時間、滞留数、ワーカーのCPU/メモリ、DLQのメッセージ数などをキューごとに監視することで、ダッシュボードを見ていない時間帯でもSLO違反や異常な振る舞いに即座に気付ける体制を作っています。キュー数 × 指標数で監視項目はそれなりの規模になりますが、AIエージェントの登場によってこれらを実現することが可能になりました。

モニタリング → 仮説 → 修正のループ

道具が揃ったあとは、ひたすら地道な改善ループを回しました。

  • 時間のかかっているジョブを発見: 実装を見直し、必要に応じてリファクタリング。low への移動で済むケースもあれば、ロジック自体に改善の余地があるケースもある。Datadog APMのトレースを仕込んでひたすら問題の処理を特定するなど、時にはアプリケーションの深い部分に踏み込んで改善を行った
  • オートスケールがうまくいっていない: メトリクスから原因を考察し、閾値や上限台数などオートスケール関連のさまざまな設定を何度も見直した
  • キュー配置のミスマッチ: 想定と異なる挙動をするジョブを適切なキューに移動した

ひとつの修正で全てが解決することは稀で、「直すと別の歪みが見える」を繰り返すのが実態でした。しかし、モニタリングの整備をしっかり行ったことによって、何が課題かが常に明確であり、継続的に改善活動を行えています。

結果

こうしたループを繰り返した結果、まだまだ課題はありますが、現在ではかなり安定してオートスケールが機能しています。

ブログ執筆時点でのオートスケールの様子

次の一手: SQS Fair Queue

本記事の設計を進めている最中、AWSから SQS Fair Queue という機能がリリースされました。これは、MessageGroupId をテナント識別子として設定することで、SQSがノイジーテナントを自動検出し、他テナントへのメッセージ配信を優先する機能です。本記事で扱ってきた「ノイジーネイバー問題」の一部を、マネージドな仕組みで解決してくれます。 ただしFair Queuesは「ジョブ特性ごとのキュー分離」(本記事の introduction / reindexing / low など) を代替するものではなく、短時間ジョブのキュー内で発生するテナント間の不公平を緩和する位置づけです。本記事で構築したキュー設計と組み合わせることで、よりきめ細やかなノイジーネイバー対策が可能になると期待しています。 弊社ではすでに本機能を導入済みで、本番ワークロードでの効果を観察しているフェーズです。結果についてはいつか別記事でレポートできればと思います。

docs.aws.amazon.com

おわりに

長くなりましたが、改めて今回の取り組みを通して得られた学びを整理します。

  • 設計の前にデータを見る: 2千万件のログから「99%が10秒未満」というワークロード特性を掴めたことが、10秒ルールやキュー分割という具体的な設計判断に直結しました。「なんとなくスケールしている」状態から脱却するには、定量的にサービス特性を把握することが出発点になります
  • マネージドサービスの特性に乗る: 可視性タイムアウトや標準メトリクスといったSQSの強みをそのまま設計の前提に組み込むことで、独自の監視・リトライ基盤を維持するコストから解放されました。「自前で頑張る」を減らし、マネージドな仕組みに乗っかれる箇所は徹底的に乗っかる方が、結局シンプルで壊れにくい構成になります
  • 設計は仮説、運用しながら最適化する: 設計時点で全てを正解にすることは不可能で、動かしながら細かく修正していくフェーズの方がむしろ重要でした。そして、そのループを高速に回すにはモニタリングを疎かにしないことが大切です

最後の点について補足すると、今回これだけ細かい粒度でダッシュボードやモニタを整備できたのは、AIエージェント(Claude Code)の存在が大きいです。キュー × 指標の組み合わせで大量のモニタを作成・保守する作業は、人の手では不可能に近いくらい大変です。(現実にはdev環境、staging環境など環境数分必要になりますし)

しかし、「Datadogリソースを定義するコード」を完全にブラックボックスにしても、AIエージェントの力を借りれば問題なく保守し続けられる、という確信が持てたことで、「観測したいものは全部観測する」という方針を恐れずに取れるようになりました。これは単なる開発効率の話ではなく、インフラ設計の意思決定そのものに影響を与える変化だと感じています。

次回は、このDatadogダッシュボード・モニタをIaCで運用するための工夫についても記事にしてみたいと思います。ここまで読んでくださり、ありがとうございました。

MNTSQ株式会社 SRE 西室