はじめに
Cloud Runのサービスを運用していて、こんな会話をしたことはありませんか?
「コールドスタートが気になるからmin instancesを1にしておこう」
「これで常に1インスタンス稼働してるから、ダウンタイムは出ないはず」
私もずっとそう思っていました。ところが、公式ドキュメントとSLAを読み込んでいくと、min instances=1にしていても5xxエラーは仕様の範囲内で発生しうることがわかります。
この記事では、Cloud Runのインフラ仕様とSLAの定義から、なぜそれが起きるのかを整理します。
この記事は「Cloud Runは使えない」という主張ではありません。むしろ「想定通りに動いている上で、完全な可用性は保証されない」という前提で運用設計を考えるための整理です。
TL;DR
- Cloud RunのSLOは99.95%(東京含む通常リージョン)/ 99.9%(一部リージョン)
- SLAの定義上、月間で約21分までのダウンタイムは「SLA違反ではない」(さらに1分未満のスパイクはカウントされない)
- min instances=1にしていても、アイドル状態のインスタンス(Warm instance)自体がGoogle側のタイミングで入れ替わる
- 例外的にリクエスト処理中のコンテナにもSIGTERMが送られることがある
- アプリ側ではリトライ・冪等性・graceful shutdownの設計が前提
「常時1インスタンス稼働」のメンタルモデル
min instancesを1以上に設定する目的は、一般的に次のように説明されます。
アイドル状態のインスタンスを常に確保して、ゼロスケール状態になるのを防ぐ。コールドスタートの改善が見込める。
この説明自体は正しいのですが、ここから「特定の1インスタンスが永続的に稼働し続ける」と理解してしまうと現実とずれます。
実際は「最低でも1インスタンスは稼働している状態を維持する」であって、「そのインスタンスが死なない」ではありません。家のリフォームで例えると「常時1部屋は住める状態にしておく」みたいなもので、部屋(インスタンス)自体は入れ替わることが前提になっています。
このアイドル状態で待機しているインスタンスのことを、Cloud Runの公式用語ではWarm instance(または「アイドル状態のインスタンス」)と呼びます。本記事でも以降この用語を使います。
公式ドキュメントが明言している「インスタンスは入れ替わる」
Cloud Runのコンテナランタイムコントラクトには、明確に書かれています。
アイドルインスタンスはいつでもシャットダウンされる可能性があり、最小インスタンス数を設定して保温されているインスタンスも例外ではない。
つまり、min=1で立てているそのWarm instance自身が、Google側の都合(基盤メンテナンス、ホストの退役、リソース最適化など)で予告なく入れ替わる、ということです。
さらに同ドキュメントには:
例外的なケースでは、Cloud Runはリクエスト処理中のコンテナに対してもシャットダウンを開始しSIGTERMを送ることがある。
「例外的」とは書いてあるものの、仕様としては許容されている挙動です。
SLAの定義そのものが100%を保証していない
ここが個人的に一番の発見でした。Cloud Run SLAの定義を読むと、次の4つの要素から構成されています。
- SLO:99.95%(東京含む通常リージョン)/ 99.9%(一部リージョン)
- Downtime:エラー率が1%を超える状態
- Downtime Period:1分以上連続するDowntime。1分未満の散発的なエラーはカウントされない
- Monthly Uptime Percentage:月の総分数からDowntime Period分を引いた割合
99.95%のSLOというのは、月間で約21分のDowntime Periodまでは「SLA違反ではない」ということです(30日 × 1440分 × 0.05% ≒ 21.6分)。さらに1分未満のエラースパイクはカウントされないため、実際には21分よりも多くの瞬間的な5xxが仕様内に収まる計算になります。
つまり、ある一瞬に集中して5xxが大量発生しても、それが1分以内に収まればSLA上は問題なしという建付けです。「99.9%以上は完璧に動く」という前提で運用設計をしないと、SLAは守られているのにユーザーから苦情が来るという状況が起こりえます。
ダウンタイムが発生しうる4つの瞬間
具体的に、min=1でもエラーが発生しうるシナリオを整理します。
その前に、なぜインスタンスの入れ替わり時にエラーが起きるのかをタイムラインで見ると分かりやすいです。
新インスタンスの起動が完了する前にリクエストが入ると、503が返る。これがmin=1での典型的な失敗パターンです。以下、具体的な発生契機を見ていきます。
1. 基盤メンテナンス起因の強制シャットダウン
Google側の基盤都合(ホスト退役、ハードウェア交換、ゾーン内リソース再配置など)でインスタンスが入れ替わるパターン。これがアプリ側からは最も見えにくく、再現も困難です。
新インスタンスの起動が間に合わず、その間に来たリクエストが503になる可能性があります。
2. リビジョン切り替え(デプロイ時)
新リビジョンをデプロイしたとき、--no-trafficを使わない限り即座にトラフィックが流れます。新リビジョンのインスタンスが起動してヘルスチェックに合格するまでの間、起動が間に合わなければ503やレイテンシ増大が発生します。
Goのような起動の軽い言語でも、コンテナのpullからプロセス起動・ヘルスチェック合格までで数百ms〜数秒は必要です。FastAPI/DjangoのようなPythonフレームワークやJVM系になるとさらに重くなり、CPU boostを使っても5〜15秒かかるケースもあります。デプロイのたびに小さなレイテンシスパイクが出るのは構造的に避けられません。
3. スケールアウト遅延
min=1の状態で急なトラフィックスパイクが来ると、2インスタンス目以降を起動する間に1インスタンス目のconcurrencyを超えたリクエストが保留されます。on-demandスケーリングが間に合わない場合は503になります。
公式ドキュメントには次のように書かれています:
Cloud Runはスケールアウト時に最大10秒までリクエストを滞留させる。10秒でインスタンスが用意できない場合は429エラーが発生する可能性がある。
ここで重要なのは、429エラーが出る前の「10秒間のレイテンシ増大」自体が、ユーザー視点では実質的なサービス停止と変わらない点です。本来100ms以内に返るAPIが10秒待たされたら、フロントエンドは普通にタイムアウトしますし、ユーザーは「動かない」と判断します。SLA上はカウントされないとしても、UX上は明確な障害です。
4. メモリ超過によるOOM kill
Cloud Runコンテナがメモリ上限を超えた場合、インスタンスは終了する。処理中のすべてのリクエストはHTTP 500エラーで終了する。
これはアプリ側の責任範囲ですが、メモリ上限ギリギリで運用していると「いつか発生する」種類のエラーです。
SIGTERMハンドラの限界
「ちゃんとgraceful shutdownを実装すればいいのでは?」という発想は正しいのですが、公式ブログの解説に重要な但し書きがあります。
graceful terminationのシグナルは主にトラフィックのないインスタンスのスケールダウン時に送られるため、シグナルハンドラでin-flightリクエストのドレイン処理をする必要は基本的にない。ただし、基盤インフラ起因のシャットダウンの場合は、in-flight接続が残った状態でこのシグナルを受け取ることがあり、graceful terminationが常に保証されるわけではない。
整理すると:
| シャットダウン契機 | in-flightリクエスト | graceful対応 |
|---|---|---|
| 通常のスケールダウン | なし | 不要 |
| 基盤メンテナンス起因 | あり得る | 試みるが保証なし |
| SIGTERM後10秒経過 | - | SIGKILLで強制切断 |
つまり、SIGTERMハンドラを書いていても、「それで全リクエストが守られる」と思ってはいけない、ということです。
では、どう設計すべきか
仕様としてダウンタイムが発生しうると認めた上での設計ポイントを整理します。ただしここから先の対策の多くは、実はCloud Run固有ではありません。コンテナが終了させられうる環境(Kubernetes、ECS/Fargate、Heroku等)すべてで共通する、分散システムの一般論に近いです。
1. クライアント側のリトライ
Cloud RunのSLA本文にもbackoff要件が明記されています。
最初のエラー後、最低1秒のbackoffインターバルを設け、連続するエラーごとに指数関数的に最大32秒まで増やすこと。
逆に言うと、Googleはクライアント側でリトライしてくれる前提でSLAを設計しています。サーバー間通信ならexponential backoffを実装し、フロントエンドからの呼び出しなら5xx時のユーザー視点でのリトライ動線を用意します。
2. APIの冪等性
リトライが前提になる以上、同じリクエストが2回流れても問題ないようにAPIを設計します。POSTでも冪等キー(Idempotency-Key)を受け取って重複処理を防ぐ、などが現実解です。
3. graceful shutdown
SIGTERMを捕捉して、進行中のリクエストを完了させてから終了する。これはKubernetes・ECS・Heroku等でも同じパターンで、Cloud Run固有なのは「SIGTERM後10秒でSIGKILL」という具体的な数値だけです。
タイムアウトは10秒未満(8秒程度)に設定して、SIGKILLが来る前に確実に抜けるようにします。
補足:Cloud Runに特化して対策するなら
ここまでの対策は分散システム全般の話でした。Cloud Runのパラメータレベルで踏み込めるものを挙げると次のような選択肢があります。それぞれ前述の4つの失敗モードのどれかに効きます。
Startup probeの調整(→ リビジョン切替・スケールアウト遅延)
デフォルトのTCP probeはポートのlistenだけを見ているため、依存先(DB・外部API)の接続がまだの状態でもトラフィックが流れる可能性があります。HTTP probeで/healthzを立て、依存先の生存確認まで含めてreadyを返す設計にしておくと、起動が間に合わない問題を減らせます。
spec:
template:
spec:
containers:
- image: ...
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 1
failureThreshold: 30 # 最大30秒待つ
timeoutSeconds: 1
/healthzは単に200を返すだけでなく、DBへの疎通や必要な初期化が済んでいることを確認してから200を返す実装にしておくのが理想です。
min instancesを2以上にする(→ 基盤メンテ起因・スケールアウト遅延)
「min=1で落ちる」問題の最も直接的な対策です。1台が入れ替わっても残り1台が捌く。コスト2倍だが可用性はオーダーで上がります。
ただし注意点として、min=2でも単純に安全になるわけではありません。1台がメンテナンスで落ちた瞬間、トラフィックは残り1台に集中します。Concurrency上限が低いと、その1台がパンクして連鎖的に落ちるリスクがあります。
そのため、min=2にするなら「1台あたりのConcurrencyに余裕を持たせる」がセットの設計になります。常時使用率が60〜70%程度に収まるよう、1台で2倍のトラフィックを一時的に捌けるキャパシティを確保しておくのが定石です。
--no-traffic + gradual rollout(→ リビジョン切替)
新リビジョンを先にwarmしてから徐々にトラフィックを流す。
gcloud run deploy my-service --no-traffic --tag=canary
gcloud run services update-traffic my-service --to-tags=canary=10
gcloud run services update-traffic my-service --to-tags=canary=100
Concurrencyのチューニング(→ スケールアウト遅延)
デフォルト80は汎用値。I/O boundなら上げる、CPU boundなら下げる。実アプリのプロファイリングを元に決めます。
CPU boost有効化(→ リビジョン切替・スケールアウト遅延)
起動時のみCPU割り当てを増やす機能。コスト増は限定的なので、ほぼ全サービスで有効化していい設定です。
メモリ上限の余裕(→ OOM kill)
実使用量のピーク1.5〜2倍を設定し、OOM killを回避します。
マルチリージョン + Serverless NEG(→ 基盤メンテ起因の根本対策)
99.95%以上のSLOが必要な場合の最終手段。複数リージョン分のコストを払うことになるので、ここまで来るとGKEとの損益分岐の検討対象に入ります。
失敗モードと対策のマッピング
| 失敗モード | 主な対策 |
|---|---|
| 基盤メンテ起因 | min≥2(+ Concurrency余裕)、マルチリージョン |
| リビジョン切替 | Startup probe、--no-traffic、CPU boost |
| スケールアウト遅延 | Concurrency調整、CPU boost、min≥2 |
| OOM kill | メモリ上限の余裕、監視 |
まとめ
- min instances=1は「コールドスタートを減らす」機能であって「ダウンタイムをゼロにする」機能ではない
- アイドル状態のWarm instance自体がGoogle側のタイミングで入れ替わる
- SLAの定義は「99.95%」かつ「1分未満のスパイクはカウントされない」など、ある程度のエラーが仕様として織り込まれている
- アプリ側の対策はリトライ・冪等性・graceful shutdownが基本(これらは分散システム全般の話)
- Cloud Run固有のチューニングとしてはStartup probe・min≥2・Concurrency・CPU boost等が効く
- 99.95%以上のSLOが必要ならマルチリージョン構成を検討する
「Cloud Runで落ちた!」となる前に、SLAと仕様を理解した上で「何を保証してくれて、何を自分で担保する必要があるか」を整理しておくと、運用設計の見通しがよくなります。