Docker 基础体系 · 第 11/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 资源治理:CPU、内存、PID、I/O、cgroups 与 OOM
Docker 容器的“资源限制”不是容器运行时凭空实现的一组参数,而是 Linux 内核提供的 cgroups(control groups,控制组)、命名空间、调度器、内存回收器和块设备 I/O 控制器共同作用的结果。
本文讨论 Linux 容器边界内的资源治理,示例以现代 Docker Engine、BuildKit 和 Compose 规范为基础。Windows 容器使用另一套资源与隔离机制,不能直接套用本文对 Linux cgroups、内核 OOM 和 /sys/fs/cgroup 的描述。
一、先建立整体模型:容器资源限制在哪里生效
一个容器至少涉及三类边界:
- 命名空间(namespace):决定进程能看到什么,例如进程号、网络接口、挂载点。
- cgroups:决定这些进程最多能使用多少 CPU、内存、PID、块设备 I/O 等资源。
- Linux 内核资源管理器:真正执行调度、内存回收、I/O 排队和 OOM 处理。
Docker Engine 通常负责:
- 创建或复用一个 cgroup;
- 将容器主进程及其后代进程加入该 cgroup;
- 根据
docker run、Compose 或 API 参数写入 CPU、内存等控制器; - 启动容器进程;
- 在容器退出后读取退出状态、OOM 标记和日志。
典型的数据流可以简化为:
flowchart LR
CLI[Docker CLI / Compose] --> Engine[Docker Engine]
Engine --> Runtime[runc 或其他 OCI Runtime]
Runtime --> CG[cgroup 层级]
Runtime --> NS[Linux namespaces]
CG --> CPU[CPU controller]
CG --> MEM[Memory controller]
CG --> PID[PIDs controller]
CG --> IO[I/O controller]
CPU --> K[Linux kernel]
MEM --> K
PID --> K
IO --> K
NS --> K
K --> P[容器内进程]
这里最重要的因果关系是:
Docker 参数只是配置入口;资源限制最终是否生效,取决于宿主机内核、cgroup 版本、控制器是否启用、运行时实现以及底层设备是否支持相应控制。
因此,“命令接受了参数”不等于“参数一定按预期限制了进程”。
二、cgroups:资源治理的内核边界
2.1 cgroup 是什么
cgroup 将一组进程放入一个内核管理的层级中,并对这一组进程应用资源控制策略。
例如,一个容器的进程树可能是:
容器 cgroup
├── PID 1:应用进程
├── 工作线程
├── 子进程
└── 应用启动的辅助进程
只要子进程仍然属于该 cgroup,CPU、内存、PID 等限制通常都会作用于它。资源限制不是只针对 Docker 记录的那个“主进程”。
cgroup 与进程命名空间解决的问题不同:
- PID namespace 让容器内进程看到自己的 PID 视图;
- pids controller 限制该 cgroup 能创建多少个进程或线程;
- memory controller 限制该 cgroup 的内存使用;
- CPU controller 控制调度时间;
- I/O controller 控制或统计块设备 I/O。
2.2 cgroup v1 与 cgroup v2
Linux 目前存在两种主要 cgroup 层级:
- cgroup v1:不同资源控制器可以挂载在不同层级;
- cgroup v2:使用统一层级,控制器通过
cgroup.subtree_control启用。
查看当前系统:
stat -fc %T /sys/fs/cgroup
常见结果:
cgroup2fs
表示当前主要使用 cgroup v2;若看到 tmpfs,还需要继续查看挂载信息:
mount | grep cgroup
查看当前 cgroup v2 的控制器:
cat /sys/fs/cgroup/cgroup.controllers
cat /sys/fs/cgroup/cgroup.subtree_control
前一个文件表示当前层级可用的控制器,后一个文件表示当前层级向子 cgroup 开放的控制器。Docker、systemd 和宿主机管理员可能共同管理这些层级,因此不应随意手工改写生产环境的 cgroup 树。
Docker 对 cgroup 版本的选择还受到以下因素影响:
- Linux 发行版和内核;
- Docker Engine 配置;
- systemd 是否作为 cgroup 管理器;
- rootless 模式;
- 用户会话是否拥有对应控制器权限。
可以使用以下命令查看 Docker 侧信息:
docker info
重点关注:
Cgroup Driver: systemd
Cgroup Version: 2
Cgroup Driver 是 Docker 如何组织 cgroup 的方式;Cgroup Version 是内核采用的 cgroup 版本。二者不是同一个概念。
三、CPU 资源治理:时间配额、相对权重与 CPU 集合
CPU 限制至少有三种不同语义,不能混为一谈:
- 绝对时间配额:一个周期内最多使用多少 CPU 时间;
- 相对权重:竞争时谁获得更多调度时间;
- CPU 集合:允许在哪些逻辑 CPU 上运行。
3.1 --cpus 的绝对上限
最常见的参数是:
docker run -d \
--name cpu-demo \
--cpus=1.5 \
alpine \
sh -c 'while :; do :; done'
--cpus=1.5 的含义是:在足够长的观察窗口内,该容器最多获得约 1.5 个逻辑 CPU 的计算时间。
在 cgroup v1 中,它通常映射到:
cpu.cfs_quota_uscpu.cfs_period_us
在 cgroup v2 中,通常映射到:
cpu.max
配额的基本关系是:
其中:
- 是一个周期内允许使用的 CPU 时间;
- 是调度周期;
- 是允许使用的 CPU 数量上限。
例如:
则:
这并不表示进程必须绑定在某一个 CPU 上,也不表示任何时刻都只能运行 1.5 个线程。它表示在调度器累计的周期配额意义上,容器总体最多获得相当于 1.5 个 CPU 的时间。
当容器在某个周期内消耗完 时,仍然可运行的任务会被限流,等待下一个周期补充配额。其表现通常是:
- CPU 使用率接近配置上限;
- 请求延迟在高负载下增加;
- 进程不一定退出;
docker stats看到的 CPU 使用率可能持续接近限制,而不是宿主机总 CPU 的 100%。
一个常见误解是:--cpus=1 会让程序只拥有一个线程。它不会禁止程序创建多个线程,只会限制这些线程总体获得的 CPU 时间。
3.2 --cpu-quota 与 --cpu-period
也可以显式指定:
docker run -d \
--name cpu-demo-explicit \
--cpu-period=100000 \
--cpu-quota=50000 \
alpine \
sh -c 'while :; do :; done'
这里:
即最多获得约半个 CPU。
周期过短或配额过小可能增加调度抖动;周期过长则可能导致一次限流持续时间较长。除非有明确的延迟和调度测试依据,否则通常优先使用语义更直接的 --cpus。
3.3 --cpu-shares / --cpu-weight 是相对权重
相对权重不是硬上限。以 Docker 的传统参数为例:
docker run -d --name low \
--cpu-shares=512 alpine sh -c 'while :; do :; done'
docker run -d --name high \
--cpu-shares=1024 alpine sh -c 'while :; do :; done'
当两个容器都持续有 CPU 需求、并且没有绝对 quota 限制时,high 获得的调度权重约为 low 的两倍。
若两个容器的权重分别为 和 ,且都在竞争同一个 CPU 资源,则理想化的 CPU 分配比例为:
但这只在以下条件近似成立:
- 两个任务都持续 runnable;
- 处于相同的 CPU 资源域;
- 没有 CPU quota;
- 没有 cpuset 隔离;
- 没有其他任务和调度层级影响。
当宿主机空闲时,低权重容器仍可能使用大量 CPU。因此下面的配置不能表达“最多使用半个 CPU”:
--cpu-shares=512
它只表达“发生竞争时优先级较低”。
在 cgroup v2 中,这类相对权重通常表现为 cpu.weight;不同 Docker 版本和 cgroup 版本会进行参数映射,不能简单把 v1 的数值直接与 v2 文件中的数值比较。
3.4 --cpuset-cpus 是 CPU 亲和性
docker run -d \
--name pinned \
--cpuset-cpus="2,3" \
alpine \
sh -c 'while :; do :; done'
这表示容器任务只能被调度到逻辑 CPU 2 和 3 上。
它与 --cpus=2 不等价:
--cpus=2:可以在多个 CPU 间迁移,但总体 CPU 时间上限约为两个 CPU;--cpuset-cpus=2,3:只能在编号为 2、3 的 CPU 上运行,但如果没有 quota,理论上可以占满这两个 CPU。
CPU 编号还涉及宿主机的:
- 超线程;
- NUMA 节点;
- CPU 离线状态;
- 容器编排平台的 CPU 分配策略。
因此,设置 cpuset 前应先检查:
lscpu
nproc
3.5 CPU 诊断:区分“配额不足”和“程序没有工作”
查看容器统计:
docker stats cpu-demo
查看配置:
docker inspect cpu-demo \
--format '{{json .HostConfig}}' | jq
在 cgroup v2 中,找到容器对应的 cgroup 后,可以查看:
cat cpu.max
cat cpu.weight
cat cpu.stat
cpu.stat 中常见的字段包括运行时间、被限流的时间和限流次数。不同内核版本字段可能略有差异,但 nr_throttled、throttled_usec 一类信息可用于判断 CPU quota 是否造成了限流。
如果 CPU 使用率很低但延迟很高,不能直接得出“CPU 限制太小”的结论,还要检查:
- I/O 等待;
- 锁竞争;
- 下游服务;
- GC;
- CPU steal;
- 线程池是否耗尽;
- cgroup CPU throttling 统计。
四、内存治理:限制、回收、交换区与 OOM
内存是最容易产生误解的资源,因为“使用量”“可回收缓存”“交换区”“进程 RSS”和“容器限制”不是同一个量。
4.1 --memory 是 cgroup 内存上限
docker run -d \
--name mem-demo \
--memory=256m \
alpine \
sh -c 'sleep 3600'
--memory=256m 为容器 cgroup 设置内存上限。进程申请内存时,内核会把该 cgroup 的匿名页、文件页以及其他受控制的内存计入相应统计;具体计费细节受 cgroup 版本和内核实现影响。
在 cgroup v2 中,常见文件包括:
memory.max 硬上限
memory.current 当前使用量
memory.high 高水位,触发回收或限速
memory.events 事件计数
memory.stat 分类统计
memory.swap.max swap 使用上限
memory.max 的值为 max 时表示不设置该层级的硬上限。
容器内的 /proc/meminfo 不能始终准确表达容器可用内存。应用应优先使用容器运行环境提供的 cgroup 信息,或者使用已经正确感知 cgroup 的运行时和监控代理。
4.2 memory.high 与 memory.max 的区别
在 cgroup v2 中:
memory.high是压力控制线;memory.max是硬限制。
达到 memory.high 后,内核通常会增加回收和限制该 cgroup 的行为,使其变慢,但不必立即杀死进程。
达到 memory.max 后,内核尝试回收内存;如果仍无法满足分配请求,就可能在该 cgroup 内触发 OOM 处理。
Docker 的常用 --memory 主要对应硬上限,不应把它理解为“超过后先优雅降速”。
4.3 swap 的含义
--memory-swap 的含义依赖宿主机是否启用 swap,也受到 cgroup v1/v2 和 Docker 版本行为影响。常见关系可以这样理解:
--memory=256m --memory-swap=512m:内存和 swap 合计上限通常为 512 MiB,理论上允许约 256 MiB swap;--memory=256m --memory-swap=256m:通常表示内存与 swap 合计上限相同,因此不额外允许 swap;--memory-swap=-1:通常表示不限制总的 memory+swap,但仍受宿主机实际 swap 和其他限制影响。
不能把 swap 当作免费内存。它可以避免部分瞬时分配直接失败,但会引入明显的访问延迟;数据库、低延迟服务和高吞吐服务是否允许 swap,需要依据应用特性验证。
Docker 还提供:
--memory-swappiness=0
该参数控制匿名页被换出的倾向,但它不是“绝对禁止 swap”的统一保证,且在不同 cgroup 版本、内核和配置下可用性存在差异。生产环境必须结合宿主机 swap 设置和实际内核行为验证。
4.4 OOM 的完整故障路径
OOM(Out Of Memory)表示内存分配无法满足,不等于“容器退出码一定是 137”。
一个典型流程如下:
- 容器进程申请更多内存;
- cgroup 当前使用量接近
memory.max; - 内核尝试回收文件页、匿名页或使用允许的 swap;
- 仍无法满足分配;
- 触发 cgroup 内存 OOM;
- 内核选择一个或多个进程发送
SIGKILL; - 如果被杀的是容器 PID 1,容器退出;
- Docker 记录容器状态,并根据
--restart策略决定是否重启。
如果没有设置特殊的退出处理,进程收到 SIGKILL 后通常不能执行清理代码。Docker 中常见的退出码 137 是:
它通常表示进程因 SIGKILL 退出,但 137 本身不能证明一定是 OOM。还可能来自管理员执行 kill -9、运行时故障处理或其他外部操作。
判断是否为 OOM,应组合检查:
docker inspect container_name \
--format 'Exit={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}'
查看内核日志:
dmesg -T | grep -i -E 'out of memory|oom|killed process'
使用 systemd 的机器还可以:
journalctl -k -g 'oom|out of memory|killed process'
cgroup v2 中可以查看:
cat memory.events
常见字段包括:
high:触发memory.high的次数;max:触及memory.max的次数;oom:发生 OOM 条件的次数;oom_kill:因 OOM 被杀死的次数。
这些计数是诊断“容器内存达到边界”的证据,比单独看退出码更可靠。
4.5 oom_score_adj 与 --oom-kill-disable
Linux 在全局内存压力下需要选择被杀进程,进程的 oom_score_adj 会影响选择倾向。Docker 的:
--oom-score-adj=-500
可以调整容器进程的 OOM 选择分数,但这不是内存预留,也不是禁止 OOM。
--oom-kill-disable
会尝试禁止 OOM killer 杀死该容器内进程,但它不是“让内存无限增长”。如果容器设置了内存限制,达到限制后进程可能卡在分配、回收或内核等待路径;如果宿主机整体耗尽内存,其他进程可能承担更大风险。
除非已经理解宿主机 OOM 行为和恢复路径,否则不应为了避免看到 OOMKilled=true 而启用该选项。
4.6 完整内存测试示例
下面使用 Python 逐步分配匿名内存:
docker run --rm \
--name mem-test \
--memory=64m \
python:3.12-alpine \
python -c '
import time
blocks = []
for i in range(20):
blocks.append(bytearray(8 * 1024 * 1024))
print(f"allocated={(i + 1) * 8} MiB", flush=True)
time.sleep(0.2)
'
预期现象:
- 容器可能打印若干次
allocated=...; - 在接近 64 MiB 限制后,进程可能被杀死;
- 宿主机执行
docker inspect时可能看到OOMKilled=true; - 实际可分配量不一定正好等于 64 MiB,因为解释器、动态库、页表和其他内存也要计入 cgroup。
这个示例不能用于测量精确 OOM 阈值,只能展示“应用分配、cgroup 计费、内核回收和 OOM”之间的关系。
五、PID 资源治理:限制进程和线程数量
PID controller 限制的是一个 cgroup 中可以创建的任务数量。Linux 中线程也属于 task,因此线程数过多同样会消耗 PID 配额。
docker run -d \
--name pid-demo \
--pids-limit=128 \
alpine \
sh -c 'sleep 3600'
--pids-limit=128 表示该容器及其子进程、线程最多只能拥有约 128 个 cgroup task。它不是限制“容器内可见 PID 最大值”,也不是 PID namespace 的 PID 编号上限。
当应用继续创建进程或线程时,常见表现是:
fork()返回EAGAIN;- 创建线程失败;
- 线程池扩容失败;
- shell 执行新命令失败;
- 应用日志出现 “resource temporarily unavailable”。
可以查看:
docker stats pid-demo
docker top pid-demo
如果需要在容器内观察进程和线程:
docker exec pid-demo sh -c 'ps -eLf | wc -l'
但 Alpine 镜像中的 ps 能显示的信息取决于安装的 procps 工具。宿主机侧还可以查看容器进程的 cgroup 归属:
docker inspect -f '{{.State.Pid}}' pid-demo
再根据该 PID 检查:
cat /proc/<host-pid>/status | grep -E 'Threads|NSpid'
设置 PID 上限时必须保留余量。应用本身可能需要:
- 工作线程;
- DNS 解析线程;
- JIT 编译线程;
- 日志线程;
- 子进程;
- 监控和探针进程。
过小的 pids-limit 会把正常扩容变成间歇性故障;不设置上限则 fork bomb 或错误的线程泄漏可能拖垮宿主机。PID 限制不是应用线程池配置的替代品,应用仍应有自己的并发上限。
六、I/O 资源治理:设备、队列与可观察性边界
I/O 限制比 CPU 和内存更依赖底层设备。Docker 只能把策略传给内核,实际效果还取决于:
- 宿主机使用的块设备;
- device-mapper、overlayfs、LVM、NVMe 等存储栈;
- cgroup v1
blkio或 cgroup v2io控制器; - 是否使用绑定挂载或命名卷;
- 底层设备是否支持按 cgroup 限流;
- 实际 I/O 是直接发生在块设备,还是被页缓存吸收。
6.1 按吞吐量限制
示例:
docker run -d \
--name io-demo \
--device-read-bps /dev/sda:10mb \
--device-write-bps /dev/sda:5mb \
alpine \
sh -c 'sleep 3600'
语义是对指定块设备设置每秒读写字节数上限。单位和设备路径必须以当前 Docker Engine 支持的格式为准;设备名也必须是宿主机真实存在并且 Docker 可识别的块设备。
6.2 按 IOPS 限制
docker run -d \
--name io-iops-demo \
--device-read-iops /dev/sda:100 \
--device-write-iops /dev/sda:50 \
alpine \
sh -c 'sleep 3600'
IOPS 是每秒 I/O 操作次数,不等于吞吐量。一个 4 KiB 请求和一个 1 MiB 请求都可能各计为一次操作,因此:
这只是理想化关系。实际吞吐还受队列深度、设备并发、合并请求、缓存和文件系统影响。
6.3 相对 I/O 权重
docker run -d \
--name io-low \
--blkio-weight=300 \
alpine sh -c 'sleep 3600'
docker run -d \
--name io-high \
--blkio-weight=900 \
alpine sh -c 'sleep 3600'
相对权重只在多个任务竞争同一个受控 I/O 资源时产生明显效果。宿主机空闲时,低权重容器仍可能获得较高吞吐。
在 cgroup v2 中,相关接口通常是 io.max、io.weight 和 io.stat;在 cgroup v1 中则常见 blkio.* 文件。Docker 参数到内核控制器的映射不是所有版本、存储驱动和设备类型都完全一致。
6.4 I/O 限制的常见反例:页缓存
下面的测试不一定立即体现磁盘限速:
dd if=/dev/zero of=/tmp/testfile bs=1M count=1024
原因是写入可能先进入页缓存,dd 很快返回,而真正的块设备写回在之后发生。更可靠的测试需要明确同步语义,例如:
dd if=/dev/zero of=/tmp/testfile bs=1M count=1024 conv=fdatasync
但这仍然不能保证结果只反映 Docker 限制,因为 /tmp 可能位于容器可写层,overlayfs 还会引入额外行为。
生产测试应明确:
- 测试文件所在卷;
- 读写是否绕过页缓存;
- 是否使用宿主机相同的设备;
- 是否存在其他工作负载;
- 观察的是应用延迟、设备吞吐还是 cgroup 统计。
查看容器的实际 I/O 统计:
docker stats container_name
也可以在对应 cgroup 中查看 io.stat。如果 docker stats 与应用侧指标不一致,应确认统计口径:一个可能统计块设备 I/O,另一个可能统计文件系统调用完成时间或页缓存行为。
七、Docker 参数、Compose 配置与 BuildKit 的边界
7.1 docker run 是运行时限制
常用运行时参数包括:
docker run -d \
--name app \
--cpus=2 \
--memory=1g \
--pids-limit=512 \
--cpuset-cpus=0-3 \
--restart=unless-stopped \
image:tag
这些参数作用于该容器运行期创建的 cgroup。它们不会修改镜像,也不会自动限制同一镜像启动的其他容器。
--restart=unless-stopped 只决定退出后的启动策略,不能修复 OOM。若应用每次启动都在相同阶段达到 memory.max,结果可能是:
启动 → OOM → 重启 → OOM → 重启
这种循环需要结合 docker events 和监控告警识别,而不是简单增加重启次数:
docker events \
--filter container=app \
--filter event=die \
--filter event=restart
7.2 Compose 配置
一个运行时资源配置示例:
services:
api:
image: example/api:1.0
cpus: 1.5
mem_limit: 512m
pids_limit: 256
restart: unless-stopped
mem_swappiness: 0
Compose 还支持 deploy.resources 形式,例如:
services:
worker:
image: example/worker:1.0
deploy:
resources:
limits:
cpus: "1.0"
memory: 256M
reservations:
cpus: "0.25"
memory: 128M
这里必须区分规范和实现:
deploy.resources最初与 Swarm 服务部署语义关系密切;- 现代 Compose 实现对其中部分资源字段可以应用到本地容器;
- 不同 Compose 版本、部署目标和字段的支持情况可能不同;
reservations通常表达调度或资源预留意图,不等于容器已经获得一个独立的内核硬配额。
在目标环境执行:
docker compose config
docker compose up -d
docker inspect <container>
通过最终容器的 HostConfig 和实际 cgroup 文件确认,而不要只依据 YAML 文件推断限制已生效。
7.3 BuildKit 构建资源与运行时资源不是一回事
docker build 或 docker buildx build 使用 BuildKit 时,构建步骤可能运行在:
- Docker Engine 管理的 BuildKit worker;
docker-container驱动创建的 BuildKit 容器;- Kubernetes 或远程 BuildKit worker。
因此:
docker run --memory=512m ...
不会限制后续 docker build 的构建过程。构建资源边界必须配置在 BuildKit worker 或构建命令支持的资源参数上,并根据所用 builder 驱动验证。
查看 builder:
docker buildx ls
docker buildx inspect --bootstrap
构建阶段的 CPU 和内存消耗还可能来自:
- 编译器;
- 链接器;
- 包管理器;
- 并行构建任务;
- BuildKit worker 自身。
镜像构建资源和最终容器资源应分别设计:前者防止 CI 节点被编译任务拖垮,后者控制服务运行期行为。
八、资源限制与容器生命周期、日志、监控的连接
资源治理不能只看“当前配置”,还必须覆盖退出、重启、日志和告警。
8.1 退出与信号
OOM killer 发送的是 SIGKILL,应用无法捕获,也不会执行优雅退出逻辑。普通停止通常是 Docker 先发送 SIGTERM,等待超时后再发送 SIGKILL。因此:
- 优雅停止失败不等于 OOM;
- OOM 后没有应用清理日志是正常的;
- 关键状态不能只依赖进程退出前写入;
- 应用应定期持久化必要状态。
8.2 日志可能放大磁盘压力
容器资源限制不自动限制 Docker 日志文件的磁盘容量。应用持续输出日志时,可能出现:
- 容器本身 CPU 被日志格式化和写入消耗;
- I/O 竞争加剧;
/var/lib/docker所在文件系统被写满;- Docker daemon 或其他容器受影响。
查看日志驱动:
docker inspect app \
--format '{{json .HostConfig.LogConfig}}'
创建容器时可以设置日志轮转,例如:
docker run -d \
--name app \
--log-opt max-size=10m \
--log-opt max-file=5 \
image:tag
日志轮转只控制 Docker 日志驱动的文件行为,不能替代应用日志采样、级别控制和集中式日志容量治理。
8.3 docker stats 不是完整监控系统
docker stats --no-stream
适合快速观察,但有明确边界:
- CPU 百分比需要结合宿主机逻辑 CPU 数和 Docker 统计口径理解;
- 内存统计可能受缓存和 cgroup 版本影响;
- 网络和块 I/O 是累计值或采样值;
- 短时 OOM、瞬时 PID 峰值可能被采样遗漏;
- 容器删除后,历史数据可能无法仅靠 Docker CLI 恢复。
生产监控至少应保留:
- cgroup CPU usage、throttling;
memory.current、memory.events、working set;- PID current 与 limit;
- I/O bytes、IOPS、等待时间;
- 容器退出码、
OOMKilled状态; - Docker events;
- 宿主机内存、swap、磁盘和 inode。
九、一个可复现的综合诊断流程
启动一个同时设置 CPU、内存和 PID 限制的测试容器:
docker run -d \
--name governed \
--cpus=0.5 \
--memory=128m \
--pids-limit=64 \
--restart=no \
alpine \
sh -c 'while :; do :; done'
检查配置:
docker inspect governed \
--format 'Pid={{.State.Pid}} Exit={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'
实时观察:
docker stats governed
检查容器是否仍在运行:
docker ps -a --filter name=governed
如果它被手工停止,可能看到退出码 137,但 OOMKilled 不一定为 true。如果它因内存压力被内核杀死,通常可以在 docker inspect 和内核日志中找到 OOM 证据。
清理测试资源:
docker rm -f governed
docker rm -f 会强制停止并删除容器,但不会自动删除命名卷,也不会解决宿主机上已经产生的日志、临时文件或监控数据。资源清理应区分:
- 容器元数据;
- 可写层;
- 命名卷;
- bind mount 目录;
- Docker 日志;
- 外部监控数据。
十、常见错误推理
错误一:退出码 137 就是 OOM
正确判断需要同时检查:
docker inspect <container> \
--format '{{.State.ExitCode}} {{.State.OOMKilled}}'
并结合内核日志、cgroup memory.events。137 只说明常见情况下进程因 SIGKILL 终止。
错误二:cpu-shares 是 CPU 百分比上限
不是。它是竞争时的相对权重。要表达硬上限,应使用 --cpus 或 quota;要表达 CPU 绑定,应使用 --cpuset-cpus。
错误三:内存限制只计算应用 RSS
cgroup 的内存计费范围比单个进程 RSS 更复杂,通常还包括文件页、页表、内核相关对象以及其他受控制的内存类别。不能简单用宿主机 ps 的 RSS 加总替代 cgroup 统计。
错误四:限制了容器 I/O,就限制了所有磁盘活动
I/O 控制通常针对特定块设备和 cgroup 路径。页缓存、overlayfs、挂载卷、宿主机其他进程和底层存储队列都可能改变观测结果。
错误五:PID 限制只限制进程,不限制线程
Linux 线程也消耗 task/PID 资源。高线程数应用必须将线程池、运行时线程和辅助进程一并纳入容量计算。
错误六:Docker 接受了参数,所以限制一定生效
必须验证:
- Docker 使用 cgroup v1 还是 v2;
- 目标控制器是否启用;
- 当前运行模式是否有权限;
- 最终容器配置是否包含该参数;
- cgroup 文件是否呈现预期值;
- 压测时是否出现对应的 throttling、回收、OOM 或 I/O 统计。
十一、资源治理的核心取舍
资源上限的目标不是让每个容器都尽可能小,而是把故障边界从“拖垮宿主机”变成“单个工作负载受控失败”。
一个完整的配置通常需要同时回答:
- CPU 是需要硬上限、相对权重,还是 CPU 绑定?
- 内存达到边界时允许 swap 吗?
- 应用是否能在被
SIGKILL前保存状态? - PID 上限是否覆盖线程和子进程峰值?
- I/O 限制作用于哪个真实块设备?
- 资源不足后容器是否自动重启?
- 重启循环如何告警?
- 日志和监控本身是否会消耗相同的 CPU、内存和磁盘资源?
- 构建阶段和运行阶段是否使用了不同的资源边界?
cgroups 提供的是内核级资源边界,不会自动解决容量规划、线程泄漏、内存泄漏、日志失控或错误的重启策略。只有把 cgroup 配置、应用并发模型、容器生命周期、日志容量和监控证据放在同一条故障链上,CPU、内存、PID、I/O 与 OOM 才能真正形成可验证的资源治理体系。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Compose 完整指南:服务、网络、卷、依赖、Profile 和生产边界
- 下一篇:Docker 配置与 Secret:环境变量、文件注入、轮换和泄漏防护
- 延伸:Docker 容器生命周期:创建、启动、信号、退出、重启与清理
- 延伸:Docker 日志与监控:Logging Driver、Metrics、事件和容量告警
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论