☁️

非同期処理の設計原則と GCP パターン集 !

に公開

こんにちは!Software Engineer の @putcho01 です。

普段の業務では新規サービスの開発をしており、Go や GCP をよく触っています。

最近、業務の中で非同期処理が必要な機能を設計する機会がありました。新規サービスということもあり、非同期処理の技術選定や設計を一から行う必要がありました。ただ、非同期処理や GCP の各サービスの特徴をなんとなくでしか理解できていませんでした。

そこで、「非同期処理の設計指針」「GCPで非同期処理を実現する上での各サービスの特徴やアーキテクチャ」といった疑問を解消するため、一から調べ直してみました。この記事はその学びをまとめたものです。

この記事でわかること

  • 非同期処理の設計原則(冪等性、パラメータ設計、並行性など)
  • GCP 各サービスの特徴・違い
    • Cloud Tasks
    • Cloud Pub/Sub
    • Cloud Eventarc
    • Cloud Scheduler
  • ユースケース別の構成パターン

非同期処理の基本

まずは、非同期処理全般で押さえておきたいポイントを整理します。

そもそも非同期ジョブで作るべきか?

設計原則の前に、まず立ち止まって考えたいのが「本当に非同期ジョブで作るべきか?」という問いです。

非同期ジョブは便利ですが、複雑さが増します。バッチ処理や同期処理で済むなら、そちらのほうがシンプルです。

  • バッチ処理
    • 「毎晩まとめて処理」で OK なら Scheduler + Jobs で十分
  • 同期処理
    • 数秒で終わる処理なら、そのまま API で完結させたほうがシンプル
  • 非同期ジョブ
    • ユーザーを待たせたくない or 大量処理が必要なときに使う

非同期処理に限らずアーキテクチャ設計全般の話ですが、まずシンプルな方法を検討する のが重要です。

非同期処理の設計原則

非同期ジョブで作ると決めたら、次の原則を守ります。これは Sidekiq の Best Practices でも紹介されている、プラットフォームを問わない普遍的な考え方です。

1. ジョブは短く、シンプルに保つ

非同期ジョブは 短時間で終わるシンプルな処理 にします。これが最も重要な原則かもしれません。

// NG: 10,000件を1つのジョブでまとめて処理
func processAllOrders(orderIDs []string) {
    for _, orderID := range orderIDs { // 10,000件ループ
        processOrder(orderID)
    }
}

// OK: 1件ずつ別のジョブに分割
func processSingleOrder(orderID string) {
    processOrder(orderID)
}

// エンキュー側で分割
for _, orderID := range orderIDs {
    taskClient.CreateTask(ctx, &taskspb.CreateTaskRequest{
        Parent: queuePath,
        Task:   createOrderTask(orderID),
    })
}

理由

観点 短いジョブ 長いジョブ
スループット 高い(並列処理しやすい) 低い(リソース占有)
失敗時の復旧 リトライが簡単 途中からの再開が必要
可観測性 開始・終了だけ見れば十分にできるかも 途中経過のログ・監視の実装が必要になる

長時間ジョブがどうしても必要な場合は、中断・再開できる設計を検討します。

2. パラメータはシンプルに保つ

非同期ジョブに渡すパラメータは 小さく、シンプルにします。

// NG: オブジェクトをそのまま渡す
order, _ := orderRepo.Find(ctx, orderID)
enqueueTask(order) // 構造体ごと渡している

// OK: ID だけを渡し、ジョブ内で取得する
enqueueTask(orderID) // ID だけ渡す

理由

  • ジョブがキューで待機している間にデータが変わる可能性がある
  • シリアライズ/デシリアライズで予期せぬ問題が起きる
  • キューのサイズが膨らんでパフォーマンスが悪化する

渡すのは ID(文字列・数値)だけにして、処理時点で最新のデータを取得する のが鉄則です。

3. 冪等性を確保する

非同期処理は 「最低 1 回実行される(at-least-once)」 が基本です。しかし、ネットワーク障害やリトライで、同じジョブが複数回実行されるケースがあるため対策が必要です。

冪等性を確保する方法

  • 処理前に「処理済みかどうか」をチェックする
  • ユニーク ID でジョブの重複作成を防ぐ
  • DB 更新は UPSERT や条件付き更新で安全に

「完了したジョブも再実行されうる」 という前提で設計します。

4. 並行性を前提に設計する

複数のジョブが同時に走ることを前提に設計します。

考慮すべきポイント

  • 同じリソースへの同時アクセス(ロック、楽観的排他制御)
  • 外部 API のレート制限(キュー側で流量制御)
  • DB コネクションプールの枯渇

「このジョブは同時に 1 つしか動かない」という前提に依存しない設計が理想です。

5. 失敗を前提にリトライ戦略を決める

ネットワークが不安定だったり、外部 API が落ちたりして非同期処理が失敗する可能性があります。失敗は必ず起きるものとして、リトライ戦略を事前に決めておく必要があります。

エラーの種類 対処
一時的な障害 指数バックオフでリトライ
永続的なエラー DLQ に退避。手動確認
レート制限(429) バックオフ + キュー側の流量制御
バグ(500 系) リトライしても無駄なので DLQ へ→ 修正後再投入

リトライ回数や間隔は処理の特性に合わせて調整します。

GCP の非同期処理サービス

ここからは GCP が提供する各サービスを見ていきます。

各サービスの特徴

まずは各サービスの特徴を簡単に整理します。

Cloud Tasks

HTTP リクエストをキューに積んで、指定した先へ確実に叩きにいくタスクキュー。

  • 得意なこと
    • 遅延実行、細かいリトライ制御、レート制限、重複作成の抑止(タスク名指定)
  • リトライ方法
    • 指数バックオフ。最大回数やバックオフを柔軟に設定できる
  • 注意点
    • 「同じタスクが複数回実行されうる」前提で設計が必要。重複作成は taskName で抑止可能

Cloud Pub/Sub

疎結合なメッセージング基盤。メッセージを配信して、複数の処理へ広げるのが得意。

  • 得意なこと
    • ファンアウト、高スループット、Push/Pull
  • リトライ方法
    • 基本は at-least-once。失敗時は再配信される(= 冪等性が必須)
  • DLQ
    • Subscription に Dead Letter Topic を設定できる
  • 注意点
    • 重複を前提に、アプリ側で冪等性を担保する設計が必要

Eventarc

GCP のイベントを受けて、Cloud Run / Functions などへ ルーティングするハブ。

  • 得意なこと
    • Storage などのイベント、Audit Logs ベースのトリガー、CloudEvents 形式
  • 配信
    • at-least-once 前提(= 冪等性が必須)
  • 再試行
    • 内部的にメッセージング基盤を使っているので、失敗時は再試行される

Cloud Scheduler

マネージド cron。

  • 得意なこと
    • cron 式での定期実行 / タイムゾーン対応 / リトライ / HTTP or Pub/Sub への送信
  • 注意点
    • 前回が終わっていなくても次が走ることがある(長時間ジョブはロックやジョブ設計が必要)

比較表(ざっくり俯瞰)

各サービスを並べてみるとこんな感じです。

特徴 Cloud Tasks Cloud Pub/Sub Eventarc Cloud Scheduler
起点 API 呼び出し API 呼び出し イベント発生 時刻(cron)
配信モデル 1 対 1 1 対多 1 対 1 1 対 1 / 1 対多
遅延実行 × × ○(定期のみ)
リトライ ○(細かく制御可) ○(購読で調整)
順序保証 ○(同一キュー内) ○(Ordering Key) × N/A
レート制限 × × N/A

Cloud Tasks と Pub/Sub

各サービスの中でも、違いが悩ましかったのがCloud Tasks と Pub/Sub です。GCP 公式ドキュメントでも比較されていますが、ポイントを絞って整理します。

Pub/Sub と Cloud Tasks の主な違いは、暗黙的呼び出しと明示的呼び出しの概念にあります。

一言でいうと

  • Pub/Sub →「誰が処理するかを受け手が決める」
    • イベントをどの Subscriber がどう処理するかは関与しない
  • Cloud Tasks →「誰が処理するかを送り手が決める」
    • Client が Worker のエンドポイントを指定してタスクをキューイングする

Cloud Pub/Sub

Cloud Tasks

https://docs.cloud.google.com/tasks/docs/dual-overview?hl=ja

詳細比較

観点 Cloud Tasks Cloud Pub/Sub
呼び出しモデル 明示的(送り手が宛先を指定) 暗黙的(受け手が購読を選択)
レート制御 キュー側で設定(QPS 指定) サブスクライバー側でフロー制御
計画的な配信 ×
順序付けられた配信 ×(キューに登録されたタスクの順序はベスト エフォートで保持) ○(順序指定キーあり)
最大メッセージサイズ 1 MB 10 MB
最大配信レート 500 QPS / キュー 実質無制限
重複排除 ○ タスク名で重複作成を防止可能 × アプリ側で冪等性を担保
API による pull ×
バッチ挿入 ×
メッセージごとに複数のハンドラ / サブスクライバー ×

判断に迷ったら

「送り先を自分で決めたいか?」 を考えます。送り先を指定したいか、受け手に任せたいか。この違いが判断の軸になります。

  • 「このワーカーに、この処理を、このタイミングで」→ Cloud Tasks
  • 「この出来事を、関心あるサービスに届けたい」→ Pub/Sub

両者は排他ではなく、組み合わせて使うこともあります。例えば「Pub/Sub でイベントを受け取り、Cloud Tasks でレート制限付きの外部 API 呼び出しをキューイングする」といったパターンです。

アーキテクチャパターン集

ここからは、ユースケースに沿ったアーキテクチャパターンを 11 パターン紹介します。必要なところだけ拾い読みしてもらって大丈夫です。

1. Web API の重い処理を非同期化

ユーザーを待たせたくない重い処理(注文処理、画像変換、PDF 生成など)を API から切り離したいとき。

構成

処理の流れ

  • API は即時に 202 Accepted を返し、処理 ID を発行
  • Worker が非同期で処理し、結果を DB に保存

設計の勘所

  • 重複実行に備えて 冪等化(処理 ID で実行済みチェック)
  • リトライ前提で 外部 API / DB 更新は UPSERT などで安全に

注意点

  • 失敗してもユーザーには見えない → 監視・通知の仕組みが必須

2. 複数サービスへのイベント通知(ファンアウト)

1 つの出来事を複数のシステムで扱いたいとき(注文確定 → 在庫/決済/通知/分析など)。

構成

処理の流れ

  • Publisher は Topic にイベントを投げるだけ
  • Subscriber は独立して処理・スケール

設計の勘所

  • イベントは重複しうる前提で、各 Subscriber を冪等に
  • 失敗メッセージは DLQ に退避して、あとから回収できるように

注意点

  • Pub/Sub は at-least-once 配信のため、同じメッセージが複数回届くことがある。「在庫を 1 減らす」のような処理は、2 回届くと 2 減ってしまう → 冪等な設計が必須

3. ファイルアップロードをトリガーに画像処理(Storage → Eventarc)

目的: アップロードを起点に変換処理を走らせたいとき(サムネ生成、トランスコード、ウイルススキャンなど)。

構成

処理の流れ

  • Storage のイベントを Eventarc で受けて、Cloud Run にルーティング
  • 処理結果を別パス/別バケットへ保存

設計の勘所

  • 無限ループ防止(入力と出力のパス/バケットを分ける)
  • 冪等性(同じオブジェクトイベントが複数回届いても壊れないように)

注意点

  • 出力先への書き込みが再度イベントを発火させ、無限ループになることがある。入力バケット(uploads/)と出力バケット(processed/)を分けるか、ファイル名のプレフィックスでフィルタリングする

4. 定期バッチ処理(Scheduler → Pub/Sub → Cloud Run Jobs)

日次レポート、バックアップ、定期同期などを決まった時刻に回したいとき。

構成

処理の流れ

  • Scheduler が Pub/Sub にメッセージを投げる
  • Cloud Run Jobs がバッチを実行して結果を出力

設計の勘所

  • 長時間ジョブは Jobs を使う(最大 24 時間実行できる)
  • 必要に応じてロック or 同時実行制御を入れる

注意点

  • Scheduler は前回のジョブ完了を待たない。処理が長引くと二重実行になり、レポートが重複したりデータが壊れる。対策として、ジョブ開始時にロックを取得するか、処理時間に余裕を持った間隔を設定する

5. 外部 API へのレート制限付きリクエスト(Tasks のキューで流量制御)

レート制限のある外部 API に大量リクエストしたいとき(SNS、決済、SMS など)。

構成

処理の流れ

  • API は Tasks にタスクを積むだけ(すぐ返す)
  • Tasks が Worker を指定した流量で叩いてくれる

設計の勘所

  • maxDispatchesPerSecond / maxConcurrentDispatches で外部 API を守る
  • 429/5xx を想定して、指数バックオフで回復させる

注意点

  • Cloud Tasks でレート制限しても、Worker 側で goroutine を使って並列リクエストすると意味がない。Worker は 1 タスク = 1 リクエストを守り、並列化はキュー側に任せる
# キュー設定例
rateLimits:
  maxDispatchesPerSecond: 10 # 秒間10リクエストまで
  maxConcurrentDispatches: 5 # 同時実行は5つまで

6. リアルタイムデータ処理(ストリーム)

リアルタイム集計、異常検知、ログ処理などをやりたいとき。

構成

処理の流れ

  • Pub/Sub で取り込み、Dataflow で変換・集計
  • 条件に応じて Cloud Run で通知

設計の勘所

  • 遅延や重複を織り込んだ設計(ウィンドウ処理、再処理の考慮)

注意点

  • ストリーミング処理では、遅延(late data)重複配信 が必ず起きる。許容遅延を設定し、集計結果は「概算値」として扱うか、後から補正する設計にする

7. 監査ログベースの自動化(Audit Logs → Eventarc)

IAM 変更の通知、特定リソース作成時の自動化など(セキュリティ/コンプライアンス用途)。

構成

処理の流れ

  • 監査ログを Eventarc のトリガーにして Cloud Run へ送る

設計の勘所

  • フィルタで対象を絞る(ノイズ削減)
  • 重複通知に備えて冪等化(イベント ID をキーに)

注意点

  • Eventarc のフィルタが広すぎると、大量のイベントが通知されてノイズになる。例えば methodName で絞り込まないと、すべての API 呼び出しが通知対象になる

8. 順序保証が必要なメッセージ処理(Ordering Key)

ユーザー単位の状態遷移など、順序が重要な処理をしたいとき

構成

処理の流れ

  • 同じ Ordering Key のメッセージは順序が保たれる
  • 異なるキーは並列に処理される

設計の勘所

  • 順序が必要な単位(ユーザー/口座など)をキーにする

注意点

  • Ordering Key の粒度が粗いと並列性が犠牲になる。例えば全メッセージで同じキーを使うと、実質シングルスレッド処理に。「ユーザー ID」「注文 ID」など、順序が必要な最小単位をキーにする

9. 遅延実行(今すぐではなく「30 分後」)

リマインダー、期限切れ処理、仮登録の掃除など、「あとで実行」したいとき。

構成

処理の流れ

  • scheduleTime で未来時刻を指定して enqueue するだけ

設計の勘所

  • 一度きりの遅延は Tasks、定期は Scheduler、と割り切る

注意点

  • 「毎日 9 時に実行」のような定期処理を Cloud Tasks で実装しようとしないscheduleTime で未来時刻を設定し、処理完了後に次のタスクを作成…というパターンは運用が複雑になる。定期実行は Scheduler に任せる

10. 承認ワークフロー(人の判断を待つ)

経費申請、デプロイ承認など「人間の承認待ち」を挟みたいとき。

構成

処理の流れ

  • Workflows のコールバックで外部イベント(承認)を待つ
  • 最終アクションは Tasks などに委譲できる

設計の勘所

  • タイムアウト/期限切れの分岐を入れておく

注意点

  • 承認待ちのタイムアウト処理が必要。「3 日経っても承認されなければ自動キャンセル」「24 時間後にリマインド通知」といったルールを Workflows 内で定義しておく

11. 大量データの並列処理(分割して配る)

大量メール送信、大規模データ変換、インポートなどを水平分割で処理したいとき。

構成

処理の流れ

  • Coordinator がチャンクに分割して Pub/Sub に投げる
  • Worker が並列に処理して結果を集約

設計の勘所

  • チャンク ID で冪等化(同じチャンクが再処理されても大丈夫なように)

注意点

  • 並列処理は速いが、結果集約がボトルネックになりやすい。1 万件の Worker が同時に 1 つの DB に書き込むと詰まる。BigQuery へのストリーミング挿入や、中間ファイルを GCS に書いて最後にマージするなど、集約先の設計も重要

GCP での運用ポイント

前半で紹介した設計原則を GCP で実践するときのポイントをまとめます。

監視すべきメトリクス

サービス 監視すべき項目
Cloud Tasks キュー深さ、最古タスクの滞留時間、失敗率
Cloud Pub/Sub 未 Ack(バックログ)、最古未 Ack の滞留、DLQ 件数
Eventarc 配信失敗数、Dead Letter 件数
Scheduler ジョブ失敗率

Cloud Monitoring でダッシュボードとアラートを作っておくのが良い。

まとめ

この記事では、非同期処理の設計原則から GCP での実際の 4 サービスの使い分けまでを整理しました。

自分自身、「なんとなく」だった理解がかなりクリアになりました。

同じように「非同期処理よくわからない…」となっている方の参考になれば嬉しいです!

参考リンク

非同期処理の設計原則

GCP 関連

Discussion