遅いDockerコンテナのトラブルシューティング:ステップバイステップのパフォーマンスガイド

CPU、メモリ、ディスクI/O、ネットワーキング、制限、マウント、ログをチェックして、Dockerコンテナが遅い理由を見つけましょう。

遅いDockerコンテナのトラブルシューティング:ステップバイステップのパフォーマンスガイド

Dockerコンテナが遅いと感じたとき、まずイメージを再構築したり、ランダムなランタイムフラグを変更したりしないでください。最初に「遅い」の意味を決めましょう。APIの応答時間が長いのですか?ワーカーが遅れていますか?起動が遅いのですか?ビルドが遅いのですか?ホストが過負荷ですか?それぞれが異なる修正方法を示しています。

コンテナは物理法則からの魔法の隔離ではありません。それでもホストのCPU、ホストのメモリ、ホストのストレージ、ホストのネットワーキング、そしてあなたが出荷したアプリケーションコードを使用します。Dockerはこれらのリソースの周りに制御と名前空間を追加しますが、遅いクエリを高速にしたり、飽和したディスクをアイドル状態にしたりはしません。

まず、簡単なライブビューから始めましょう:

docker stats

スローダウンを再現しながらコンテナを監視します。単一のスナップショットは、負荷の下で何が変化するかを見るよりも役立ちません。CPUが跳ね上がり、高いままなら、CPUが原因です。メモリが上昇し続け、コンテナが停止するなら、メモリのパスをたどります。BLOCK I/Oが大きく動き、リクエストが停止するなら、ストレージに注意が必要です。コンテナが穏やかに見えても、ユーザーがレイテンシを感じるなら、アプリ、ネットワークコール、データベース、または上流のサービスを調べます。

最初に、コンテナとホストの健全性を比較する

遅いコンテナは単に遅いホスト上に存在するかもしれません。両方のレベルを確認します。

docker stats <container>
top
free -h
df -h

Linuxでは、利用可能であればiostat -xz 1が役立ちます。高いディスク使用率や長い待機時間は、遅いデータベース、パッケージインストール、ログが多いサービスを説明できます。Docker Desktopでは、Docker VMに割り当てられたCPUとメモリも確認します。十分なメモリがあるMacでも、Docker Desktopが低く制限されているとコンテナが飢える可能性があります。

すべてのコンテナが遅い場合、ホストが疑わしいです。1つのコンテナだけが遅く、隣接するコンテナが正常な場合、そのワークロード、その制限、マウント、依存関係に焦点を当てます。

CPUのボトルネック

docker statsでは、CPUは100%を超えることがあります。これはDockerがコア全体での使用量を報告するためです。200%を使用するコンテナは、おおよそ2つのコアを使用しています。重要な質問は、それがワークロードにとって予想されるかどうかです。

ランタイム制限を確認します:

docker inspect <container> --format 'NanoCPUs={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} Cpuset={{.HostConfig.CpusetCpus}}'

サービスが--cpus=0.5で開始された場合、通常のトラフィックの下でスロットルされる可能性があります。KubernetesやComposeでは、同じ問題がCPU制限に隠れていることがあります。ラップトップでジョブを迅速に処理していたワーカーが、CIでは半分のCPUしか得られないため遅くなる可能性があります。

アプリケーションレベルのCPUについては、推測する代わりにプロセスをプロファイリングします。Nodeの場合は、組み込みのCPUプロファイリングやclinicスタイルのツールを使用します。Pythonの場合は、許可されている場所でpy-spyでサンプリングします。Javaの場合は、JFRまたはasync-profilerを使用します。プロダクションイメージ内にツールをインストールできない場合は、ステージング環境で同じイメージを実行するか、デバッグコンテナパターンを使用します。

一般的なCPUの原因には、タイトなポーリングループ、高価なJSONシリアル化、正規表現のバックトラッキング、画像処理、圧縮、そして少なすぎるコアをめぐって争うワーカースレッドの多すぎが含まれます。CPUを増やすことは、アプリがそれを使用でき、ホストに容量がある場合にのみ役立ちます。

メモリプレッシャーとOOMキル

メモリの問題は、メモリ使用量の上昇、頻繁なガベージコレクション、ホスト上のスワップアクティビティ、または突然の終了として現れます。OOMステータスを確認します:

docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}}'

OOMKilled=trueの場合、コンテナはメモリ制限を超えました。それは明示的な--memory制限、Docker Desktop VMの制限、またはホスト全体のプレッシャーである可能性があります。

実際のトラフィックを送信しながらdocker statsを使用します。メモリが平坦化せずに成長する場合、リーク、無制限のキャッシュ、キュー蓄積、または一度に多すぎるデータをロードするワークロードを疑います。起動中にメモリが跳ね上がり、その後落ち着く場合、制限がランタイムに対して単に低すぎる可能性があります。

言語のデフォルトが重要です。Java、Node、および一部のアプリケーションサーバーは、バージョンと構成に応じて、コンテナ内でメモリを異なる方法で予約または使用する場合があります。予測可能な動作が必要な場合は、明示的なヒープまたはメモリオプションを設定します。たとえば、Javaサービスはコンテナ対応のヒープパーセンテージが必要な場合があります。Nodeサービスは--max-old-space-sizeが必要な場合があります。データベースは、プロセスとファイルシステムのための余地を残すキャッシュ設定が必要です。

アプリがすべての時間をガベージコレクションに費やすほどメモリ制限を厳しく設定しないでください。決してクラッシュしないが、常に一時停止するコンテナは依然として壊れています。

ディスクI/Oと遅いバインドマウント

ストレージの問題は、CPUとメモリのグラフが正常に見えるため、見逃されやすいです。Dockerでは、ディスクの遅さはしばしば4つの場所のいずれかから来ます:重いアプリケーションI/O、過剰なログ、ストレージドライバー、またはDocker Desktop上のバインドマウント。

Dockerのビューを確認します:

docker stats <container>
docker logs --tail 20 <container>

ログが非常にノイズが多い場合、ロギングドライバーは作業をしなければなりません。JSONファイルのログは、ローテーションが設定されていない限り、急速に成長する可能性があります。ビジーなサービスでは、すべてのリクエストボディやデバッグ行をログに記録することは、実際のパフォーマンス問題になる可能性があります。

ロギング設定を検査します:

docker inspect <container> --format '{{json .HostConfig.LogConfig}}'

ローカルおよび小規模サーバー設定では、デーモン設定またはComposeファイルでログローテーションを検討します。プロダクションプラットフォームでは、ログをプラットフォームのロギングシステムに送信し、アプリケーションのログボリュームを意図的に保ちます。

バインドマウントは、macOSとWindowsで特別な注意が必要です。ホストからLinuxコンテナにマウントされたソースツリーは、仮想化レイヤーを横断します。これは開発には便利ですが、依存関係フォルダ、データベース、または書き込みが多いディレクトリに対しては、名前付きボリュームよりもはるかに遅くなる可能性があります。

たとえば、Node開発コンテナは、node_modulesがバインドマウント上にある場合、遅くなる可能性があります。より良いパターンは、ソースコードをバインドマウントするが、依存関係を名前付きボリュームに保持することです:

services:
  app:
    volumes:
      - .:/app
      - node_modules:/app/node_modules
volumes:
  node_modules:

データベースの場合、ホストパスを必要とする特定のバックアップまたは検査ワークフローがない限り、バインドマウントよりも名前付きボリュームを優先します。

ネットワークレイテンシと依存関係の遅さ

コンテナは、別のサービスを待っているために「遅い」場合があります。ローカルプロセスは正常でも、DNS、データベース、Redis、API、またはプロキシが遅い場合があります。

コンテナ内からテストします:

docker exec -it <container> sh
curl -w '
lookup:%{time_namelookup} connect:%{time_connect} start:%{time_starttransfer} total:%{time_total}
' -o /dev/null -s http://service:8080/health

このcurl -w出力は、DNSルックアップ、TCP接続、最初のバイト、合計時間を分離します。DNSルックアップが遅い場合、/etc/resolv.confとDockerデーモンのDNS設定を検査します。接続が遅いか失敗する場合、ネットワーク、ファイアウォール、サービスバインディングを確認します。最初のバイトまでの時間が遅い場合、上流のサービスは接続を受け入れたが、応答に時間がかかりました。

コンテナ間トラフィックの場合、ユーザー定義のブリッジネットワークを使用して、コンテナが名前で互いを解決できるようにします:

docker network create appnet
docker run -d --name api --network appnet my-api
docker run --rm --network appnet curlimages/curl http://api:8080/health

実際のトラフィックがコンテナ間である場合、公開されたホストポートを介してベンチマークしないでください。プロダクションが使用するパスをテストします。

起動パフォーマンスは別の問題

遅い起動は、多くの場合、イメージのプル時間、コンテナ起動時の依存関係のインストール、データベースマイグレーション、またはアプリケーションのウォームアップから来ます。

コンテナは毎回起動するたびにパッケージをインストールすべきではありません。エントリポイントがnpm installpip installapt-getを実行したり、毎回バイナリをダウンロードしたりする場合、強い理由がない限り、その作業をイメージビルドに移動します。

アプリが提供する場合、タイムスタンプ付きの起動ログを確認します。そうでない場合、デバッグ中にエントリポイントのステップの周りに簡単なタイムスタンプを追加します:

date; echo 'starting migrations'
# migration command
date; echo 'starting server'
# server command

ネットワーク経由でプルされるイメージの場合、イメージサイズが重要です。マルチステージビルド、.dockerignore、およびより小さなランタイムベースは、コールドスタートとデプロイメント速度を向上させます。しかし、イメージがすでに存在し、コンテナが実行されている場合、イメージサイズは通常、CPU、メモリ、I/O、およびアプリケーションの動作よりも重要ではありません。

ビルドパフォーマンスはランタイムパフォーマンスではない

遅いDockerビルドはイライラしますが、それらは異なるクラスの問題です。コード変更が依存関係のインストールを毎回強制する場合、レイヤーの順序を修正します:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

依存関係レイヤーを無効にするソース変更ごとに、依存関係をインストールする前にリポジトリ全体をコピーしないでください。

また、ビルドコンテキストを小さく保ちます:

.git
node_modules
coverage
dist
*.log

BuildKitキャッシュマウントは、繰り返される依存関係のダウンロードに役立ちますが、最初にDockerfileが正しく順序付けられていることを確認します。キャッシュマウントは、キャッシュを早く無効にするDockerfileを完全に保存することはできません。

リソース制限はホストを保護し、アプリを傷つける可能性がある

CPUとメモリの制限は、1つのコンテナがホストをダウンさせないようにするために有用です。また、ワークロードを測定せずに例からコピーされた場合、人為的な遅さを作り出すこともあります。

制限を検査します:

docker inspect <container> --format '{{json .HostConfig}}' | jq '{Memory, NanoCpus, CpuQuota, CpuPeriod, BlkioWeight}'

jqが利用できない場合、コンテナを通常通り検査し、HostConfigを検索します。

Composeの場合、実際にレンダリングされた設定を確認します:

docker compose config

これにより、オーバーライドファイルや環境変数から継承された制限がキャッチされます。よくある驚きは、低い制限を設定する開発オーバーライドファイルが誤ってテスト環境で使用されることです。

実用的な診断フロー

「コンテナが遅い」という苦情がある場合、このフローを使用します:

  1. 遅い動作を再現し、再現中にdocker statsを実行します。
  2. ホストのCPU、メモリ、ディスク、およびDocker Desktop VMの制限を確認します。
  3. コンテナのCPUとメモリの制限を検査します。
  4. ログを読み、リトライ、接続タイムアウト、マイグレーション、デバッグログ、またはOOMのヒントを探します。
  5. コンテナ内からcurldig、または目的に合わせたデバッグイメージで依存関係をテストします。
  6. マウントを確認します:書き込みが多いパスを適切な名前付きボリュームに移動します。
  7. リソースグラフがコードに戻る場合、アプリケーションをプロファイリングします。

最善の修正は具体的である傾向があります:低すぎるメモリ制限を上げる、巨大なペイロードのログを停止する、データベースデータをバインドマウントから移動する、遅いDNSパスを修正する、Dockerfileレイヤーを再順序付けする、またはアプリケーションランタイムを調整する。一般的な「Dockerを最適化する」アドバイスは、実際にどのリソースが遅いかを証明することよりも役立ちません。