非同期処理の設計原則と 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

詳細比較
| 観点 | 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 サービスの使い分けまでを整理しました。
自分自身、「なんとなく」だった理解がかなりクリアになりました。
同じように「非同期処理よくわからない…」となっている方の参考になれば嬉しいです!
参考リンク
非同期処理の設計原則
- Sidekiq Best Practices — Ruby のジョブキューだが、設計原則は言語問わず参考になる
GCP 関連
Discussion