Cloud Run コールドスタート改善記録
数ヶ月前に部内向けの Discord 認証 + アカウント情報提供サービス「jyogi-discord-auth」を提供しました。
このサービスは 部員情報などを取り扱っていて他の部内用アプリの基盤になっています。
使用したスタックは以下のとおりです。
- 言語: Go
- フレームワーク: net/http(標準ライブラリ)
- DB: TiDB Serverless(MySQL 互換)/ GORM
- インフラ: Google Cloud Run(min-instances 0)
- コンテナ: Docker(distroless)
- CI/CD: GitHub Actions
このサービスを使用した別プロダクトを作成している部員から以下の Issue がリポジトリに作成されました。
[FEATURE] 初回起動早めてほしい
🚀 何をするのか?
初回起動もうちょい早められたりしませんか
認証画面への遷移10秒くらいかかるときあるんですよね
とのことで、調査・改善しました。
原因の切り分け
Cloud Run アプリのレスポンスが遅くなる原因はいくつか考えられます。
- Cloud Run のコールドスタート(インスタンスが起動していない)
- アプリの起動シーケンス(起動時に重い処理をしている)
- クライアント側の処理(認証フローの実装)
どこが原因なのかを切り分けるために、まず /health エンドポイントへの応答時間を計測しました。
curl -s -o /dev/null -w '%{time_total}' https://<PROJECT_ID>.run.app/health
# → 0.127 秒(3回平均)
/health は DB 接続・テンプレートレンダリングが不要な最軽量エンドポイントです。これが速かったことで「コンテナ起動自体は速い」という判断を一旦しようとしましたが、この判断は誤りでした。
Issue に対して書いたコメント
🔍 jyogi-auth のURLを取得しています... === Cloud Run コールドスタート計測 === 対象: https://<PROJECT_ID>.run.app/health 試行回数: 3 [1/3] インスタンスがアイドルアウトするまで待機中 (180秒)... [1/3] 初回応答時間: 0.155778 秒 [2/3] インスタンスがアイドルアウトするまで待機中 (180秒)... [2/3] 初回応答時間: 0.113652 秒 [3/3] インスタンスがアイドルアウトするまで待機中 (180秒)... [3/3] 初回応答時間: 0.113441 秒 平均初回応答時間: .127 秒 (3回平均) 思ったより遅くない説が...
/health が速かった理由
min-instances 0 の設定ではリクエストが来ないとインスタンスがアイドルアウトしますが、アイドルアウトには数分〜十数分かかります。計測タイミングによってはウォームスタートを計測していた可能性があります。
そこで Cloud Logging からインスタンスが実際に起動したタイミングのログを確認しました。
gcloud logging read \
'resource.type=cloud_run_revision AND resource.labels.service_name=<SERVICE_NAME>
AND (textPayload:"Starting" OR textPayload:"AutoMigrate" OR textPayload:"listening")' \
--limit 20 --format 'value(timestamp, textPayload)'
2026-05-27T10:42:29.308Z Starting new instance. Reason: AUTOSCALING
2026-05-27T10:42:29.777Z Starting メンバー認証システム...
2026-05-27T10:42:30.830Z Starting AutoMigrate for TiDB ...
2026-05-27T10:42:35.251Z AutoMigrate completed successfully for TiDB ...
2026-05-27T10:42:35.254Z Server listening on port 8080
起動シーケンスをタイムスタンプで分解すると、次のとおりです。
| フェーズ | 所要時間 |
|---|---|
| コンテナ起動 → Go プロセス開始 | 約 0.5 秒 |
| DB 接続(CREATE DATABASE 用の一時接続 × 2往復) | 約 1.0 秒 |
| AutoMigrate(全 6 テーブルのスキーマチェック) | 約 4.4 秒 |
| 合計(インスタンス起動 → リクエスト受付) | 約 5.9 秒 |
AutoMigrate が起動時間全体の約 75% を占めていました。 最大で 15 秒に達したケースもあり、これがユーザー報告の「認証画面への遷移 10 秒」の正体でした。
つまり原因は ① ではなく ②:アプリの起動シーケンス でした。Cloud Run のコールドスタート(コンテナ起動)自体は速く、問題はアプリが HTTP リクエストを受け付け始めるまでの間にブロッキングで走っていた重い処理にありました。
改善内容
費用を増やさない範囲(min-instancesを1にするなど)で以下を改善しました。
学生団体なのでお金をかけたくない...
1. 起動シーケンスの最適化
AutoMigrate を本番環境でスキップ
毎コールドスタートで全テーブルのスキーマチェッククエリを走らせていた AutoMigrate を、環境変数 DISABLE_AUTO_MIGRATE=true でスキップできるようにしました。本番は DB が作成済みのため、スキーマ変更のないかぎり不要です。
CREATE DATABASE 用の一時接続をスキップ
起動時に「DB 名なしで接続 → CREATE DATABASE → 接続クローズ → 再接続」という 2 往復が走っていました。環境変数 SKIP_DB_CREATE=true で本番ではこの一時接続をスキップできるようにしました。
# Cloud Run デプロイコマンド例
--set-env-vars "SKIP_DB_CREATE=true"
--set-env-vars "DISABLE_AUTO_MIGRATE=true"
2. Docker イメージの軽量化
本番ステージを alpine から gcr.io/distroless/static-debian12:nonroot に変更しました。distroless はシェルや不要なツールが含まれないためセキュリティリスクを最小化でき、イメージサイズの削減によりコールドスタート時のイメージプル時間も短縮できます。distroless にはシェルや wget が含まれないため、ヘルスチェックをバイナリ自身で実行するサブコマンドとして内蔵しました。
# 改善後
FROM gcr.io/distroless/static-debian12:nonroot
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app/server", "-healthcheck"]
3. 起動時間の計測基盤
前後比較できるよう、起動フェーズごとの所要時間ログを追加しました。
[boot] config loaded (+2ms)
[boot] database initialized in 1.2s (+1.2s)
[boot] ready to serve requests (+1.3s)
また Cloud Logging から起動時間を自動算出するスクリプトを追加しました。
bash scripts/measure-coldstart.sh
計測結果
改善前のベースライン
最初は curl で /health を叩いて計測していました。しかし前述のとおりウォームスタートを計測していた可能性があったため、計測スクリプトを Cloud Logging のログベースに切り替えました。
PR にコメントとして残した実測結果がこちらです。
gcloud のログベースで取ったらダメだったよ...
scripts/measure-coldstart.sh === Cloud Run コールドスタート計測 (ログベース) === サービス: jyogi-auth (asia-northeast1) 対象: 直近 5 回の起動 (過去 30d) [1] 2026-05-27T08:17:38.595315Z 起動時間: 15.244 秒 [2] 2026-05-27T08:46:10.546916Z 起動時間: 3.024 秒 [3] 2026-05-27T09:32:07.313593Z 起動時間: 5.116 秒 [4] 2026-05-27T10:25:19.767308Z 起動時間: 2.929 秒 [5] 2026-05-27T10:42:29.777087Z 起動時間: 5.478 秒 平均起動時間: 6.358 秒 (5回平均)
最大 15.2 秒、平均 6.4 秒。「10 秒かかる」という報告が数字として裏付けられました。
改善後
デプロイ後の起動を同スクリプトで計測しました。
[5] 2026-05-27T16:37 起動時間: 0.961 秒 ← 改善後
| 改善前 | 改善後 | |
|---|---|---|
| 起動時間 | 平均 6.4 秒 | 0.96 秒(実測値 n=1) |
| 最大起動時間 | 15.2 秒 | — |
| 改善率 | — | 約 6.6 倍高速化 |
注記: 今回は「AutoMigrate スキップ」「CREATE DATABASE 用一時接続スキップ」「distroless 化」を同一デプロイで適用したため、各変更の個別の寄与は未計測です。改善前のログから AutoMigrate が約 4.4 秒・DB 接続 2 往復が約 1.0 秒と推測でき、AutoMigrate スキップが主因と考えられますが、厳密に分離したい場合は変更を 1 つずつデプロイして計測します。
注意点
AutoMigrate を本番で使い続けるリスク
小規模・開発初期であれば本番でも AutoMigrate を使うケースは珍しくありません。ただし GORM の公式ドキュメントでは、以下のように述べられています。
"at some point you may need to switch to a versioned migrations strategy"
(ある時点でバージョン管理されたマイグレーション戦略への切り替えが必要になります)
「開発初期は AutoMigrate でよいが、本番が成熟したら専用ツールへ移行すべき」というスタンスです。また、使い続けると以下のリスクがあります。
- カラムの型・サイズ変更は自動で行われるため、意図しないスキーマ変更が起きうる
- 不要になったカラムは削除されず残り続ける(データ保護のための仕様)
- 毎起動でスキーマチェッククエリが全テーブルに対して走る ← 今回のパフォーマンス問題の直接原因
公式が推奨する代替手段は Atlas で、GORM との公式インテグレーションが提供されており、バージョン管理されたマイグレーションファイルを自動生成できます。
Atlas は公式の GORM プロバイダーを使用して、データベーススキーマのマイグレーションを自動的に計画できます。プロバイダーを設定した後、以下のコマンドを実行するだけでマイグレーションファイルが生成されます。
atlas migrate diff --env gorm
今後の対応
DISABLE_AUTO_MIGRATE=true にしたため、スキーマ変更がデプロイで自動反映されなくなりました。今後スキーマを変更する際は手動でのマイグレーションが必要です。手順の整備は今後対応予定です。Atlas を導入してマイグレーションファイルを管理する形になる予定です。
まとめ
原因のまとめ
「認証画面への遷移 10 秒」の正体は Cloud Run のコールドスタートそのものではなく、アプリが毎起動で実行していた重い処理でした。
- AutoMigrate(全テーブルのスキーマチェック)が起動時間の約 75% を占めていた
- CREATE DATABASE 用の一時 DB 接続(2 往復)が別途約 1 秒かかっていた
- これらが HTTP リクエストを受け付ける前にブロッキングで実行されていたため、ユーザーには「遷移が遅い」として現れていた
AutoMigrate は開発中のスキーマ変更を自動で反映してくれる便利な機能ですが、本番環境でスキーマが安定してからも毎起動で走らせ続けていたのが根本的なミスでした。
これからやること
-
マイグレーション手順の整備
DISABLE_AUTO_MIGRATE=trueにした以上、スキーマ変更時の本番への適用手順を明文化する必要がある。 -
改善後のデータを蓄積する
改善後の起動データがまだ 1 回分しかない。scripts/measure-coldstart.shで定期的に計測し、改善効果を継続確認する。 -
各変更の寄与を分離計測する
今回は複数の変更を同時デプロイしたため個別の効果が不明である。厳密に知りたい場合は 1 つずつ計測する。
教訓
「開発時に便利だった機能」を本番でも無条件に使い続けない
AutoMigrate はローカルや開発環境では非常に便利ですが、本番では「スキーマが安定したら外す」という判断が必要でした。開発フェーズの都合でつけた設定が、ユーザー体験に直結するコールドスタートを 6 倍以上遅くしていました。
計測しないと正しい原因にたどり着けない
最初に curl で /health を測ったとき「速いのではないか」と誤認しかけました。軽量エンドポイントの応答時間とウォームスタートの見落としという 2 つの罠があり、Cloud Logging でインスタンス起動ログを直接確認して初めて正しい原因を特定できました。
Cloud Run でパフォーマンス問題が起きたときは、まずログで「インスタンスの起動シーケンスに何秒かかっているか」を確認するのが近道です。