はじめに
「イメージは小さくした」「ビルドも速くなった」。それでも、実際に動かすと起動がもっさりしていたり、最初の1リクエストだけ遅い、I/Oが妙に詰まることがあります。
これ、だいたい“アプリが遅い”というより、コンテナ実行時のオーバーヘッド+初回処理(コールドスタート)の合成で起きます。
ありがちな症状はこんな感じです。
docker runしてからプロセスが立ち上がるまでが遅い- 最初のHTTPレスポンスだけ遅い(2回目以降は速い)
- ファイル読み込みやログ出力が詰まる
- 同じコンテナでもホストや環境で挙動が違う(ローカルは速いのにCI/本番は遅い 等)
この手の遅さは、闇雲に最適化すると沼ります。先にやるべきはひとつで、「どこが遅いか」を分解して数値で掴むこと。
本記事では、起動時間・I/O・コールドスタートを“層”に分けて、再現可能な計測手順に落とし込みます。
座学
1) 「起動が遅い」は4つの層に分解できる
起動を速くしたいなら、まず“起動”を言語化します。コンテナの体感起動は多くの場合、次の4つの合計です。
- 取得(Pull):レジストリからレイヤーを取ってくる時間
- 展開(Extract/Mount):レイヤーを展開し、overlayfs等でマウントしてrootfsを作る時間
- プロセス起動(Exec):PID 1 が立ち上がるまで(エントリポイント、シェル、ランタイム起動など)
- アプリ準備(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 pull、time docker run --rm ... true、Readyまでのリトライ計測、初回/2回目curl比較。これだけで、どこに手を入れるべきかがかなり明確になります。
“速くする”の前に、“遅い場所を言い当てる”。
これができるようになると、コンテナのパフォーマンス改善はギャンブルではなく、再現性のある作業になります。
コメントを残す