Docker 上級 #5 リソース制限と cgroups
ここまでコンテナのリソース使用量はほぼホストが面倒を見てくれるかのように扱ってきました。運用に行くとそれが通用しなくなります — 一コンテナがホストメモリを食って他のサービスが死んだり、CPU を独占して応答遅延を作ったりが普通です。この記事は リソース制限 を本格的に整理します。
Docker 上級 シリーズでこの記事の位置:
- #1 BuildKit と buildx
- #2 マルチアーキテクチャイメージ
- #3 イメージセキュリティ — non-root, distroless, scan(Trivy)
- #4 SBOM と署名 (cosign)
- #5 リソース制限と cgroups ← この記事
- #6 プロダクション運用 — restart 方針、healthcheck、graceful shutdown
cgroups — コンテナ隔離の一軸 #
#1 で短く触れた cgroups (control groups)。Linux カーネルのリソース会計 / 制限機能です。コンテナが軽い理由 が namespace なら — コンテナの安全な運用を可能にする仕組み が cgroups です。
cgroups は二世代あります。
| cgroups v1 | cgroups v2 | |
|---|---|---|
| リリース | 2007 | 2016 |
| 構造 | リソースごとに別階層 | 単一統合階層 |
| メモリ会計 | 部分的 | 正確 |
| Docker サポート | 長い | 20.10+ 安定 |
最近の Linux ディストリビューションはほぼ v2 がデフォルト です。Docker Desktop も v2。この記事は v2 前提で進みます。
確認:
stat -fc %T /sys/fs/cgroup
# cgroup2fs ← v2
# tmpfs ← v1 (古いシステム)
docker info | grep Cgroup
# Cgroup Driver: systemd
# Cgroup Version: 2メモリ制限 — --memory
#
最もよく使う制限です。
docker run -d --memory 512m myapp
docker run -d -m 512m myapp # 短縮services:
web:
image: myapp
mem_limit: 512m # または deploy.resources.limits.memory (Swarm)
mem_reservation: 256m # soft limitmem_limit vs mem_reservation
#
| オプション | 意味 |
|---|---|
mem_limit | ハード限界 — 越えると OOMKill |
mem_reservation | ソフト限界 — ホストがメモリ不足のとき優先回収対象にならないように |
運用では mem_limit だけ明示するのが普通。mem_reservation は一ホストに複数のコンテナを束ねて立ち上げるマルチテナント場面で意味が生きます。
単位表記 #
512 # バイト (デフォルト)
512b # バイト
512k # キロバイト (1024 バイト)
512m # メガバイト
2g # ギガバイトm はメガバイトです。K8s の 500m (0.5 cpu) と混同しないように。
スワップ (swap) #
docker run -m 512m --memory-swap 1g myapp
# RAM 512m + スワップ (1g - 512m = 512m) = 合計 1g
docker run -m 512m --memory-swap -1 myapp
# 無制限スワップ (ホスト限界まで)
docker run -m 512m --memory-swap 512m myapp
# スワップ使用禁止 (RAM 限界がそのまま全体限界)運用では普通 スワップなし にするか、ホスト自体でスワップを無効化します。スワップが入ると性能予測が難しくなります。
OOMKilled — 限界を越えると何が起きるか #
メモリ限界を越えたコンテナは OOMKilled 状態で終わります。
docker inspect myapp --format '{{.State.OOMKilled}}'
# true
docker inspect myapp --format '{{.State.ExitCode}}'
# 137 ← SIGKILL (128 + 9)exit code 137 がほぼ OOMKilled のシグネチャです。ホストの dmesg でも確認:
sudo dmesg | grep -i 'killed process'
# Memory cgroup out of memory: Killed process 12345 (python) ...運用で OOMKilled が頻発しているなら:
- 限界が小さすぎる — 計測して増やす
- アプリのメモリリーク — 時間とともに増えるかを追跡
- ランタイムが限界を認識していない — 次の節
コンテナのメモリ認識 — ランタイムの落とし穴 #
コンテナの中のアプリが free / /proc/meminfo を読むと ホストのメモリ を見ます。cgroups の限界は別の仕組みで適用されます。
docker run --rm -m 512m ubuntu free -m
# total used free
# Mem: 15920 542 14253 ← ホストメモリこれがなぜ問題かというと — 一部のランタイムが free または Runtime.maxMemory のような呼び出しで ホストサイズ基準で動作 して、限界を越えるメモリを使って OOMKilled されます。
Java (JVM) #
# 昔 (JVM 8 初期): ホストメモリ基準 → よく OOMKill
java -Xmx2g app.jar
# JVM 10+ : -XX:+UseContainerSupport (デフォルト) → cgroups 限界認識
java -XX:MaxRAMPercentage=75.0 app.jarJVM 10+ は UseContainerSupport がデフォルトで有効です。-Xmx を明示するより MaxRAMPercentage で限界の比率を渡す方がコンテナ親和的 です。
Node.js #
Node にも似た論点があります。V8 の old-space 限界がデフォルト 1.5~4GB ほどなのでコンテナ限界とずれることがあります。
node --max-old-space-size=512 app.jsコンテナ限界が 512m ならば Node の old-space 限界もその辺に合わせる方が安全です。
Python #
CPython はガベージコレクタが単純なので限界を明示的に与えるものはありません。ただし multiprocessing のような箇所で worker 数を自動で決めるとき — os.cpu_count() がホストのコア数を返します。コンテナの CPU 限界を認識しないので、worker 数は環境変数で直接 与える方が安全です。
CPU 制限 — --cpus / --cpu-shares
#
CPU も二つの形で制限できます。
# 1) 絶対限界 — 1 コア使用可能
docker run --cpus 1.0 myapp
# 2) 絶対限界 — 1.5 コア (1 コア 100% + 別コア 50%)
docker run --cpus 1.5 myapp
# 3) 相対重み — 他コンテナとの比較
docker run --cpu-shares 512 myapp| オプション | 意味 |
|---|---|
--cpus N | 一コンテナが使える CPU の絶対量 (N コア分) |
--cpu-shares | 相対重み (デフォルト 1024)。ホストが忙しいときの分配比 |
--cpuset-cpus 0-2 | 使えるコアを明示 (例: 0,1,2 番コアだけ) |
運用ではほぼ常に --cpus で絶対限界を明示する方が予測可能。--cpu-shares は一ホストの中で優先順位が違うコンテナを立ち上げるときに意味が生きます。
CFS quota の動作 #
--cpus 1.0 は内部的に CFS (Completely Fair Scheduler) quota で実装されます。100ms ごとに 100ms 分の CPU 時間を受け取る形。これがたまに意図しない throttling を作ります — 瞬間的な burst が阻まれるケース があります。
K8s では cpu.cfs_period_us / cpu.cfs_quota_us の動作が非現実的という意見があり、一部環境では CPU 限界を意図的に置かないこともあります (メモリ限界は常に)。Docker 単独環境では普通 --cpus で限界を置くのが一般的です。
コンテナの CPU 認識 #
JVM / Node / Go のようなランタイムは GC スレッド / worker 数 をコア数によって決めます。os.cpu_count() がホストコア数を返すとコンテナ限界とずれます。
docker run --rm --cpus 0.5 alpine nproc
# 8 ← ホストコア数、限界無視解決:
- JVM 10+:
UseContainerSupportが自動処理 - Node:
process.env.UV_THREADPOOL_SIZE環境変数で thread pool サイズを明示 - Go:
runtime.GOMAXPROCSがコンテナ限界を認識するようにautomaxprocsライブラリ使用 - Python: worker 数を環境変数で
# go.mod
require go.uber.org/automaxprocs v1.5.3
# main.go
import _ "go.uber.org/automaxprocs"import 一行追加すれば GOMAXPROCS が cgroups 限界に自動設定されます。
compose.yaml のリソース定義
#
Compose v2 では二つの形式が見えます。
services:
web:
image: myapp
mem_limit: 512m
mem_reservation: 256m
cpus: 1.5
pids_limit: 100services:
web:
image: myapp
deploy:
resources:
limits:
memory: 512M
cpus: '1.5'
reservations:
memory: 256M
cpus: '0.5'普通の docker compose up では mem_limit / cpus のような シンプル形式が動作 します。deploy.resources は Swarm モードでフル効果ですが、最近の Compose は単一ホストでも一部認識します。単一ホスト運用ならシンプル形式を使う方が混乱しません。
pids_limit — プロセス暴走防止
#
fork bomb / ゾンビプロセス蓄積の防止に効果的。
docker run --pids-limit 100 myappservices:
web:
pids_limit: 100ウェブアプリ一個が 100 個のプロセスを立ち上げることはほぼありません。限界を置けば意図しない暴走をコンテナ単位で防げます。
ulimit — ファイルディスクリプタなど
#
Linux ulimit もコンテナ単位で渡せます。最も一般的なのは 開けるファイルディスクリプタ (nofile) です。
docker run --ulimit nofile=65536:65536 myappservices:
web:
ulimits:
nofile:
soft: 65536
hard: 65536
nproc: 4096大きなトラフィックを受けるサーバ / 多くの接続を維持するワーカーはデフォルト 1024 では足りません。運用環境では一度増やしておくとよいでしょう。
IO 制限 — --device-write-bps など
#
ブロック IO も cgroups が制限できます。よく使う項目ではないですが — 一コンテナがディスク IO を独占して他サービスに影響を与えるマルチテナントホスト環境で意味があります。
docker run --device-write-bps /dev/sda:10mb myapp
# このコンテナの /dev/sda 書き込み速度を 10MB/s に制限運用コンテナ一個のリソース定義によく入るオプションではありません。
リソース計測 — docker stats 再び
#
中級 #6 で触れたコマンド。リソース限界の効果を見たいときによく回すコマンドです。
docker stats myapp
# CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
# myapp-web-1 24.5% 312MiB / 512MiB 60.93% 12kB / 8kB ...MEM % が限界の 70~80% を頻繁に超えるなら限界が小さいか漏れがあります。OOMKill は突然起きるので、普段から stats を見てマージンを確認 する流れが安全です。
prometheus / cAdvisor
#
運用環境では stats を人間が目で見るのではなく時系列 DB に積んで追跡します。cAdvisor が Docker の cgroups 会計を Prometheus メトリクスとして公開してくれます。
services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
ports:
- "8080:8080"Prometheus + Grafana と繋げばコンテナごとの CPU / メモリ / IO グラフが一箇所に集まります。Docker 単独運用の最初のモニタリングセットアップです。
OOMKilled の診断フロー #
OOMKilled が見えたら一度回す流れ:
# 1) 本当に OOMKilled か
docker inspect <c> --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
# 2) ホスト dmesg に記録
sudo dmesg -T | grep -i oom
# 3) 限界確認
docker inspect <c> --format '{{.HostConfig.Memory}}'
# 4) 普段の使用量 (運用中なら stats またはモニタリング)
docker stats <c> --no-stream
# 5) ランタイムが限界を認識しているか (JVM のような場合)
docker exec <c> java -XshowSettings:vm -version 2>&1 | grep MaxHeapSizeこのフローが手に馴染めばメモリ事故の 9 割は素早く絞り込めます。
まとめ #
この記事で掴んだ絵:
- コンテナのリソース制限は cgroups v2 の上で動くメカニズム — namespace と一緒に隔離の二軸
--memory/mem_limitが最も大事。運用コンテナに限界明示はほぼ必須- OOMKilled のシグネチャは exit code 137 +
State.OOMKilled: true - ランタイムがコンテナ限界を認識するか別途点検 — JVM
MaxRAMPercentage、Node--max-old-space-size、Goautomaxprocs - CPU は
--cpusで絶対限界、または--cpu-sharesで相対重み pids_limit、ulimit nofileも運用安定性のためによく入る設定- 計測は
docker stats→ cAdvisor + Prometheus + Grafana に拡張
次の記事 (#6 プロダクション運用) では Docker 上級シリーズを締めくくります。PID 1 の信号処理、SIGTERM グレースフル終了、restart 方針の深掘り、healthcheck の運用視点、liveness vs readiness の概念まで — 一コンテナを プロダクションで安定的に回す 細部です。