事業成長に伴うSchedulerのパフォーマンス改善
こんにちは、令和トラベルにて旅行アプリ「NEWT (ニュート)」のバックエンド開発をしている仙波です。先日、Schedulerのパフォーマンス改善を行ったため、抱えていた課題と行った改善について具体的な例をもとに紹介します。
はじめに
私たちが開発しているNEWTでは、メール送信やポイントやクーポンに関わる処理を定期的に実行しています。サービス規模の拡大に伴い、Schedulerの処理時間が長くなる課題が顕在化したため、その改善に取り組みました。
🤔 何が課題だったか?
NEWTのインフラはGoogle Cloudで構成されており、定期実行にはCloud Schedulerを利用し、workerのAPIはCloud Runで提供しています。カスタマー数やツアー数の増加に比例してSchedulerの実行時間が伸び、タイムアウトで処理が失敗する状況が発生していました。単純にCloud Schedulerのタイムアウトを伸ばすという対処法も考えられましたが、今後の事業成長を踏まえ、根本的な解決を目指して設計をもう一度見直す必要がありました。

問題のあるSchedulerの特定
まずは現状把握のため、各Schedulerにおける実行時間の実態を明らかにする必要がありました。Cloud Log Analyticsを使用して過去3ヶ月のScheduler実行状況を分析しました。
調査の手順:
- Schedulerログの洗い出し
- レスポンスタイムの分析(平均、90パーセンタイル、最大、最小)
- タイムアウトリスクのある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を使って、各処理の所要時間を可視化することで、処理状況が明らかになり、ボトルネックを特定することができました。

分析の結果、処理時間が長くなる原因として以下のような要因があることが分かりました。
- 処理するエンティティが多く、バッチ処理したとしても時間がかかってしまう。
- メールやプッシュ通知の送信など、個別に内容が異なる場合にまとめて送信できない。
- データ構造などの理由から、一部SQLをEntityごとに発行している箇所がある。
⚡ 改善施策
基本的なアプローチは、Schedulerの責務を分離し、Cloud Tasksを使った並列処理を導入することです。
具体例として、ツアー検索画面の「おすすめ順」表示機能でソートする際に必要となる、ツアーのスコア計算処理の改善を説明します。
おすすめ順について
NEWTでは、ツアー検索結果を「おすすめ順」で表示する機能があります。これは、ツアーの利用率など多くの指標を基にスコアを計算し、カスタマーに最適なツアーを表示するための機能です。このスコアは毎日更新する必要があり、全ツアーを対象とした処理をSchedulerで実行していました。

課題と改善アプローチ
課題: 全ツアーのスコア計算を1つのSchedulerで一括処理していたため、ツアー数の増加に比例して実行時間が長くなっていました。
改善: Schedulerの役割を再定義し、実際の計算処理はCloud Tasksによる並列実行に移行することで、スケーラビリティを確保しました。
改善後の設計
Schedulerの処理
Schedulerは更新対象ツアーを特定し、1,000ツアー単位でタスクを分割(例:10万ツアー→100タスク)してCloud Tasks (Queue) にEnqueueします。
Taskの処理
各Taskは割り当てられたツアー群(1,000ツアー)のスコア計算を行い、結果を更新します。

工夫したポイント:
- 各Taskに共通して必要なデータ(e.g, 総ツアー数, etc)はキューイングされる前に、事前に取得して各Taskに渡すことで、各Taskでは必要最低限の処理のみを行うようにしました。
- 各Taskでは、
WHERE IN (?, ?, …)やbulk insertを使うことで、実行時間が処理するツアー数に依存しないような形でスコア計算できるようにしました。 - Cloud Tasks側で
maxConcurrentDispatchesやmaxDispatchesPerSecondなどの流量制限を行い並列度を調整することで、柔軟にシステム全体に対する負荷が調節できるようにしました。 - データの処理数や処理時間、処理内容を考慮して、まずは悲観的に並列度を設定し、モニタリングを通して適切に流量調整するような戦略を取りました。
この改善により:
- タスク単位でエラー処理が可能になり、効率的なリトライが実現
- Cloud Tasksによる並列処理でスケーラビリティが向上
- Cloud Tasksで流量調整を行うことにより、システム負荷調整の柔軟性を担保
- タイムアウトの心配がなくなり、システムの安定性が大幅に改善
を実現することができました。
📊 結果どうなったか?
平均5~6分かかっていたスコア計算が以下のように改善しました。
- Scheduler実行時間:約20秒
- スコア計算タスク:約3~6秒 × 100タスク
これにより、大幅な処理時間の短縮と安定性向上を実現できました!

運用改善
システム改善と並行して、運用面でも予防的な対策を強化しました。元々サーバーリソースや処理時間の監視・アラートは実施していたものの、突発的に増加する負荷に気付けない場合がありました。例えば、マーケティングキャンペーンで一時的に急増したカスタマーの処理が重くなるケースがありました。
そこで、処理負荷の先行指標となるデータ量の監視を行うようにしました。
- カスタマー数やツアー数が閾値を超えた際のSlack通知
- Redashダッシュボードでの継続的な監視
これにより、workerが実際に実行される前に潜在的な負荷に事前に気付けるような体制を構築しました。

おわりに
今回の取り組みにより、Schedulerの安定性が大きく向上しました。サービス規模の拡大を見据えた継続的な改善を通じて、より快適な旅行体験を提供していきたいと考えています。
令和トラベルでは、一緒に旅行の未来を創る仲間を募集中です!興味のある方はお気軽にご連絡ください。
令和トラベルのTech Blogです。 「あたらしい旅行、あらゆる人へ。」をミッションに、旅行におけるあたらしい体験や、あたらしい社会価値の提供を目指すデジタルトラベルエージェンシーです。旅行アプリ「NEWT(ニュート)」を提供しています。(NEWT:newt.net/)
Discussion