コンテナは“起動してから”が勝負:コールドスタート/CPU/I/Oの遅さを分解して計測する実践ガイド

はじめに

「イメージは小さくした」「ビルドも速くなった」。それでも、実際に動かすと起動がもっさりしていたり、最初の1リクエストだけ遅いI/Oが妙に詰まることがあります。
これ、だいたい“アプリが遅い”というより、コンテナ実行時のオーバーヘッド+初回処理(コールドスタート)の合成で起きます。

ありがちな症状はこんな感じです。

  • docker run してからプロセスが立ち上がるまでが遅い
  • 最初のHTTPレスポンスだけ遅い(2回目以降は速い)
  • ファイル読み込みやログ出力が詰まる
  • 同じコンテナでもホストや環境で挙動が違う(ローカルは速いのにCI/本番は遅い 等)

この手の遅さは、闇雲に最適化すると沼ります。先にやるべきはひとつで、「どこが遅いか」を分解して数値で掴むこと。
本記事では、起動時間・I/O・コールドスタートを“層”に分けて、再現可能な計測手順に落とし込みます。


座学

1) 「起動が遅い」は4つの層に分解できる

起動を速くしたいなら、まず“起動”を言語化します。コンテナの体感起動は多くの場合、次の4つの合計です。

  1. 取得(Pull):レジストリからレイヤーを取ってくる時間
  2. 展開(Extract/Mount):レイヤーを展開し、overlayfs等でマウントしてrootfsを作る時間
  3. プロセス起動(Exec):PID 1 が立ち上がるまで(エントリポイント、シェル、ランタイム起動など)
  4. アプリ準備(Ready):依存読込、設定ロード、DB接続、JIT/キャッシュ、最初のリクエスト処理など

よくあるのは、(1)(2)を“起動”と勘違いしていたり、逆に(4)をコンテナ基盤のせいにしてしまうケース。
計測は、この層を分けて行うのがコツです。


2) コールドスタートを悪化させる定番要因

アプリ側の初回遅延には、原因のパターンがあります。

  • 動的ロードの集中:大量のimport/require、巨大な設定ファイル、テンプレートコンパイル
  • 初回だけ発生する準備:キャッシュ生成、DBマイグレーション、秘密情報取得(KMS/SSM)
  • ネットワーク待ち:DNS解決、外部API疎通、DB接続確立、TLSハンドシェイク
  • ランタイム特性:JVM/JIT、Nodeの初回モジュール解決、Pythonのimport、Goは比較的軽いが初回I/Oは効く
  • ストレージ特性:overlayfs越しの小さなファイル大量読み込み、ログの大量出力

“初回だけ遅い”のは、だいたいこのどれか(または複合)です。


3) I/Oは「どのI/Oか」を言い当てるのが重要

I/Oといっても、ボトルネックは別物です。

  • 読み込みI/O:設定/静的ファイル/依存の読み込み
  • 書き込みI/O:ログ、テンポラリ、キャッシュ、SQLite等
  • メタデータI/O:大量の小ファイルの stat()/探索(node_modules、python site-packages)
  • ネットワークI/O:外部接続やDNS、レジストリ通信

コンテナだとoverlayfsの影響で、小さいファイルを大量に触るワークロードが目立って遅くなることがあります。
なので、計測も「読み/書き」「小ファイル/大ファイル」「ネットワーク」を分けます。


ハンズオン

ここでは“速い/遅い”の感覚を、数字に変えるところまでやります。Linux/macOSどちらでも概ね可能ですが、Linuxの方が観測手段が豊富です。

ゴール

  • 起動を「Pull」「展開」「PID1」「Ready」に分けて測る
  • コールドスタート(最初の1回)とウォーム(2回目以降)を比較する
  • I/O負荷を“疑う”のではなく“証拠を出す”

1) 計測用の“Ready判定”を用意する

起動の最終ゴールは「プロセスがいる」ではなく「サービスとして使える(Ready)」です。
HTTPサーバなら、最初に 200 OK を返せるまでをReadyとします。

例:コンテナを起動して、curl が通るまでの時間を測る(擬似コードの流れ)

  • docker run -d ...
  • curl をリトライして成功した時刻を記録
  • docker stop して終了

以降の手順では、これを“Ready時間”として扱います。


2) Pull時間を測る(ネットワーク/レジストリ要因を切り出す)

Pullが遅いなら、起動の議論以前に配布が詰まっています。

# 1) いったんローカルから消す(注意:他用途のイメージは消さないこと)
docker image rm -f your-image:tag

# 2) Pull時間を計測
/usr/bin/time -p docker pull your-image:tag

ここで遅いなら、疑うべきは

  • レジストリ帯域、同時pull制限、ネットワーク
  • イメージサイズ/レイヤー数
  • 圧縮/展開コスト(次のステップへ)

3) “展開+起動(PID1まで)”を測る:run直後にすぐ終わるコンテナで見る

アプリ準備(Ready)を含めたくないので、まずは短命コマンドで測ります。

# すでにPull済み前提で、runのオーバーヘッドをざっくり測る
/usr/bin/time -p docker run --rm your-image:tag true

この時間はおおむね

  • rootfsの用意(overlayの組み立て)
  • PID1起動(今回は true なので最短)
    が中心です。

もしここが大きいなら、アプリ以前に「実行基盤での遅さ」があります(ストレージ・セキュリティ設定・Docker Desktopの仮想化など)。


4) Ready時間を測る:コールド vs ウォームを比較する

次に、実際のサービスを立ち上げてReadyまで測ります。
下は“汎用の測定ワンライナー”の例です(ポートは適宜変更)。

# コンテナ起動
cid=$(docker run -d -p 18080:8080 your-image:tag)

# Readyになるまで待つ(最大60秒)
start=$(date +%s%3N)
for i in $(seq 1 120); do
  if curl -fsS http://localhost:18080/health >/dev/null 2>&1; then
    break
  fi
  sleep 0.5
done
end=$(date +%s%3N)

echo "Ready(ms)=$((end-start))"

docker stop "$cid" >/dev/null

これを2回繰り返して比較します。

  • 1回目:コールドスタート(ページキャッシュもアプリ内部キャッシュも温まってない)
  • 2回目:ウォーム(ホストのページキャッシュが効きやすい、依存の読み込みが速くなりがち)

もし1回目だけ極端に遅いなら、アプリの初回処理(設定ロード、依存import、テンプレコンパイル、外部接続)が濃厚です。


5) 「最初の1リクエストが遅い」を測る(起動後と切り分け)

Readyになっても、最初の実リクエストだけ遅いことがあります。
それは「起動」ではなく「初回処理」問題です。

# 起動してReady確認後…
/usr/bin/time -p curl -o /dev/null -sS -w "HTTP:%{http_code}\n" http://localhost:18080/
# 2回目
/usr/bin/time -p curl -o /dev/null -sS -w "HTTP:%{http_code}\n" http://localhost:18080/

差が大きいなら、

  • 初回だけ重い処理(キャッシュ生成、コネクション確立、JIT等)
  • 初回アクセス時にだけロードされるモジュール/テンプレート
    が候補になります。

6) I/Oが原因かを“証拠で”詰める:ホスト側から観測する

(A) まずは超軽量に:docker stats で傾向を見る

起動直後にCPUやI/Oが跳ねていないかを見ます。

docker stats --no-stream "$cid"

CPUが100%に張り付くなら計算系、メモリが急増するならロード系、I/Oが増えるなら読み書きが疑えます。

(B) ログ出力が重いケースを疑う(意外と多い)

起動時に大量ログを吐くアプリは、stdout/stderrが詰まってReadyが遅くなることがあります。
試しにログ量を減らして比較できるなら、それが最短で確証になります。

(C) Linuxなら strace で「小ファイル地獄」を可視化

コンテナ内で strace を使う(またはホストからPIDに当てる)と、どんなsyscallが多いか見えます。
特に openat / stat が膨大なら“小ファイル大量アクセス”です。

例(コンテナ内で起動コマンドに被せるイメージ):

strace -f -tt -T -o /tmp/trace.log your_command

見るポイントは

  • openat/stat が大量か
  • 1回1回の T が長いか(I/O待ち)
  • DNSなら connect / recvfrom 周辺が伸びるか
    です。

7) “改善”は計測の後:よく効く方向性だけ押さえる

計測で原因の層が分かったら、対策はだいたい次の方向になります。

  • Pullが遅い → レイヤー数/サイズ、レジストリ帯域、配布経路、同時pull
  • 展開が遅い → ストレージ性能、Docker Desktop仮想化、レイヤー構成(巨大レイヤー)
  • PID1が遅い → エントリポイントでの重いシェル処理、不要な初期化
  • Readyが遅い → 依存ロード最適化、外部接続の遅延隠蔽、初回キャッシュ戦略、ログ量削減
  • 初回リクエストが遅い → JIT/テンプレコンパイル/コネクション確立の前倒し or 遅延ロードの制御

ポイントは、改善は必ず“どの数字がどれだけ変わったか”で判断することです。
体感ではなく、Ready(ms) と FirstRequest(ms) を並べるだけでも、最適化が前に進みます。


まとめ

コンテナの遅さは「イメージが重い」だけでは説明できません。起動は、

  • Pull
  • 展開
  • PID1起動
  • Ready
  • 初回リクエスト(コールドスタート)

に分解でき、さらにI/Oは「小ファイル/大ファイル」「読み/書き」「ネットワーク」に切り分けられます。

大事なのは、最初に層ごとの計測を置くこと。
time docker pulltime docker run --rm ... true、Readyまでのリトライ計測、初回/2回目curl比較。これだけで、どこに手を入れるべきかがかなり明確になります。

“速くする”の前に、“遅い場所を言い当てる”。
これができるようになると、コンテナのパフォーマンス改善はギャンブルではなく、再現性のある作業になります。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です