🦔

事業成長に伴うSchedulerのパフォーマンス改善

に公開

こんにちは、令和トラベルにて旅行アプリ「NEWT (ニュート)」のバックエンド開発をしている仙波です。先日、Schedulerのパフォーマンス改善を行ったため、抱えていた課題と行った改善について具体的な例をもとに紹介します。

はじめに

私たちが開発しているNEWTでは、メール送信やポイントやクーポンに関わる処理を定期的に実行しています。サービス規模の拡大に伴い、Schedulerの処理時間が長くなる課題が顕在化したため、その改善に取り組みました。

🤔 何が課題だったか?

NEWTのインフラはGoogle Cloudで構成されており、定期実行にはCloud Schedulerを利用し、workerのAPIはCloud Runで提供しています。カスタマー数やツアー数の増加に比例してSchedulerの実行時間が伸び、タイムアウトで処理が失敗する状況が発生していました。単純にCloud Schedulerのタイムアウトを伸ばすという対処法も考えられましたが、今後の事業成長を踏まえ、根本的な解決を目指して設計をもう一度見直す必要がありました。

architecture_before

問題のあるSchedulerの特定

まずは現状把握のため、各Schedulerにおける実行時間の実態を明らかにする必要がありました。Cloud Log Analyticsを使用して過去3ヶ月のScheduler実行状況を分析しました。

調査の手順:

  1. Schedulerログの洗い出し
  2. レスポンスタイムの分析(平均、90パーセンタイル、最大、最小)
  3. タイムアウトリスクのあるSchedulerの特定

最終的に以下のように、それぞれのエンドポイントごとにlatencyを算出し、タイムアウトリスクのあるSchedulerを特定しました。

sendpoint: /worker/aaa
  min latency    : 470.945584 s
  max latency    : 599.952203 s
  average        : 546.117115 s
  90 percentile  : 587.687826 s

endpoint: /worker/bbb
  min latency    : 64.766363 s
  max latency    : 600.000473 s
  average        : 502.013406 s
  90 percentile  : 600.000248 s
  
...

ボトルネックの特定

次に、特定した問題のあるSchedulerに対して、どの処理にどれくらい時間がかかっているのかを調べる必要がありました。Trace Explorerを使って、各処理の所要時間を可視化することで、処理状況が明らかになり、ボトルネックを特定することができました。

trace_explorer

分析の結果、処理時間が長くなる原因として以下のような要因があることが分かりました。

  • 処理するエンティティが多く、バッチ処理したとしても時間がかかってしまう。
  • メールやプッシュ通知の送信など、個別に内容が異なる場合にまとめて送信できない。
  • データ構造などの理由から、一部SQLをEntityごとに発行している箇所がある。

⚡ 改善施策

基本的なアプローチは、Schedulerの責務を分離し、Cloud Tasksを使った並列処理を導入することです。

具体例として、ツアー検索画面の「おすすめ順」表示機能でソートする際に必要となる、ツアーのスコア計算処理の改善を説明します。

おすすめ順について

NEWTでは、ツアー検索結果を「おすすめ順」で表示する機能があります。これは、ツアーの利用率など多くの指標を基にスコアを計算し、カスタマーに最適なツアーを表示するための機能です。このスコアは毎日更新する必要があり、全ツアーを対象とした処理をSchedulerで実行していました。

sort_by_recommendation_score

課題と改善アプローチ

課題: 全ツアーのスコア計算を1つのSchedulerで一括処理していたため、ツアー数の増加に比例して実行時間が長くなっていました。

改善: Schedulerの役割を再定義し、実際の計算処理はCloud Tasksによる並列実行に移行することで、スケーラビリティを確保しました。

改善後の設計

Schedulerの処理

Schedulerは更新対象ツアーを特定し、1,000ツアー単位でタスクを分割(例:10万ツアー→100タスク)してCloud Tasks (Queue) にEnqueueします。

Taskの処理

各Taskは割り当てられたツアー群(1,000ツアー)のスコア計算を行い、結果を更新します。

architecture_after

工夫したポイント:

  • 各Taskに共通して必要なデータ(e.g, 総ツアー数, etc)はキューイングされる前に、事前に取得して各Taskに渡すことで、各Taskでは必要最低限の処理のみを行うようにしました。
  • 各Taskでは、 WHERE IN (?, ?, …)bulk insert を使うことで、実行時間が処理するツアー数に依存しないような形でスコア計算できるようにしました。
  • Cloud Tasks側で maxConcurrentDispatchesmaxDispatchesPerSecond などの流量制限を行い並列度を調整することで、柔軟にシステム全体に対する負荷が調節できるようにしました。
  • データの処理数や処理時間、処理内容を考慮して、まずは悲観的に並列度を設定し、モニタリングを通して適切に流量調整するような戦略を取りました。

この改善により:

  • タスク単位でエラー処理が可能になり、効率的なリトライが実現
  • Cloud Tasksによる並列処理でスケーラビリティが向上
  • Cloud Tasksで流量調整を行うことにより、システム負荷調整の柔軟性を担保
  • タイムアウトの心配がなくなり、システムの安定性が大幅に改善

を実現することができました。

📊 結果どうなったか?

平均5~6分かかっていたスコア計算が以下のように改善しました。

  • Scheduler実行時間:約20秒
  • スコア計算タスク:約3~6秒 × 100タスク

これにより、大幅な処理時間の短縮と安定性向上を実現できました!

execution_log

運用改善

システム改善と並行して、運用面でも予防的な対策を強化しました。元々サーバーリソースや処理時間の監視・アラートは実施していたものの、突発的に増加する負荷に気付けない場合がありました。例えば、マーケティングキャンペーンで一時的に急増したカスタマーの処理が重くなるケースがありました。

そこで、処理負荷の先行指標となるデータ量の監視を行うようにしました。

  • カスタマー数やツアー数が閾値を超えた際のSlack通知
  • Redashダッシュボードでの継続的な監視

これにより、workerが実際に実行される前に潜在的な負荷に事前に気付けるような体制を構築しました。

redash_alert

おわりに

今回の取り組みにより、Schedulerの安定性が大きく向上しました。サービス規模の拡大を見据えた継続的な改善を通じて、より快適な旅行体験を提供していきたいと考えています。

令和トラベルでは、一緒に旅行の未来を創る仲間を募集中です!興味のある方はお気軽にご連絡ください。

https://www.reiwatravel.co.jp/recruit

令和トラベル Tech Blog

Discussion