Docker 基础体系 · 第 68/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker OOM 与性能诊断:cgroups、PSI、CPU Throttle、I/O 和内存
Docker 容器的“性能问题”通常不是一个单独的 Docker 问题,而是 Linux 内核对进程组进行资源控制后的结果。Docker 负责创建容器、设置 cgroups 限制并汇总状态;真正执行内存回收、CPU 调度、I/O 调度和 OOM 处置的是 Linux 内核。
本文讨论 Linux 容器边界内的现代 Docker Engine。Docker Desktop 中的容器实际运行在 Linux 虚拟机内,宿主机上看到的 CPU、内存和 I/O 指标,不能直接等同于容器内部 Linux 虚拟机的指标。
先建立一条完整的故障路径
一个请求从应用进入容器后,可能经历以下资源路径:
flowchart LR
A[应用线程] --> B{CPU 调度}
B -->|配额耗尽| C[CPU Throttle]
B -->|获得时间片| D[执行系统调用]
D --> E[内存分配]
E -->|超过 memory.max| F[内存回收]
F -->|无法回收且仍超限| G[cgroup OOM Kill]
D --> H[文件系统/块设备 I/O]
H -->|设备队列拥塞| I[I/O Stall]
H -->|cgroup I/O 限速| J[I/O Throttle]
C --> K[请求延迟上升]
G --> L[进程退出,常见状态 137]
I --> K
J --> K
K --> M[PSI 记录资源等待]
这里有几个容易混淆的事实:
- CPU 使用率高不等于 CPU 被 throttle。进程可能确实获得了大量 CPU。
- 内存使用量接近限制不等于已经 OOM。内核可能仍能回收页缓存。
- I/O 使用量高不等于 I/O 饱和。真正影响延迟的是设备队列、服务时间和等待时间。
- PSI 高表示任务因为资源不可用而等待,但 PSI 不直接告诉你是哪个进程导致的。
- 容器退出码 137通常表示进程收到
SIGKILL,但仅凭 137 不能证明一定是 cgroup OOM;宿主机 OOM、人工kill -9等也可能产生相同退出码。
cgroups:Docker 资源限制的内核基础
cgroups 解决什么问题
Linux control groups,简称 cgroups,将一组进程放入一个层级结构,并对这组进程应用资源控制。Docker 容器中的主进程及其子进程通常位于同一个容器 cgroup 或其子 cgroup 中。
cgroups 主要提供三类能力:
- 限制:例如内存最多使用 512 MiB、CPU 每 100 ms 最多运行 50 ms。
- 计量:统计 CPU 时间、内存事件、I/O 字节数等。
- 分组:使内核能够按容器、服务或其他进程组执行控制。
现代 Linux 通常使用 cgroup v2,其统一层级中的典型文件包括:
memory.max
memory.current
memory.events
cpu.max
cpu.stat
io.max
io.stat
pids.max
较早的系统可能使用 cgroup v1。v1 将控制器拆分为多个独立层级,例如:
memory.limit_in_bytes
memory.usage_in_bytes
cpu.cfs_quota_us
cpu.cfs_period_us
blkio.throttle.read_bps_device
Docker 命令行参数保持相对稳定,但底层文件名和部分语义会随 cgroup 版本变化。因此诊断时不能只背某个路径,必须先确认版本:
docker info --format 'CgroupVersion={{.CgroupVersion}}'
典型输出:
CgroupVersion=2
这表示 Docker 当前使用 cgroup v2;它不表示宿主机一定启用了所有控制器,具体可用控制器还应检查:
cat /sys/fs/cgroup/cgroup.controllers
Docker 参数如何映射到 cgroups
下面的容器限制分别作用于不同控制器:
docker run -d --name demo \
--memory=512m \
--memory-swap=1g \
--cpus=0.5 \
--pids-limit=256 \
nginx:alpine
其含义是:
--memory=512m:容器内存使用上限为 512 MiB 左右的字节限制。--memory-swap=1g:在支持该语义的 Linux 配置中,容器内存与 swap 的总额度约为 1 GiB;因此可使用的 swap 额度约为1 GiB - 512 MiB。--cpus=0.5:设置 CPU 带宽配额,等效于每个调度周期允许使用半个 CPU。--pids-limit=256:限制 cgroup 中的进程/线程数量,防止 fork bomb 或线程失控。
实际限制应通过 docker inspect 查看,而不是只看启动命令:
docker inspect demo --format '
Memory={{.HostConfig.Memory}}
MemorySwap={{.HostConfig.MemorySwap}}
NanoCpus={{.HostConfig.NanoCpus}}
CpuQuota={{.HostConfig.CpuQuota}}
CpuPeriod={{.HostConfig.CpuPeriod}}
PidsLimit={{.HostConfig.PidsLimit}}
'
其中内存通常以字节表示,NanoCpus 以十亿分之一 CPU 表示。不同 Docker 或 Compose 版本对未设置字段的输出可能不同。
内存:使用量、回收和 OOM 不是同一件事
内存使用量包含什么
容器的内存消耗不只有应用堆内存,还可能包括:
- 匿名页:语言运行时堆、线程栈、malloc 分配等;
- 文件页缓存:读取文件后缓存的内容;
- tmpfs:包括
/dev/shm和显式挂载的 tmpfs; - 内核记账项目:部分 socket、内核数据结构等;
- cgroup v2 下由内核计量的其他内存类型。
因此,一个进程在容器内报告的 RSS,与 cgroup 的 memory.current 并不总是相等。
在 cgroup v2 中,可检查:
CG=/sys/fs/cgroup/docker/<container-cgroup>
cat "$CG/memory.current"
cat "$CG/memory.max"
cat "$CG/memory.events"
cat "$CG/memory.stat"
实际路径依发行版和 Docker cgroup 驱动而变化。更可靠的定位方法是先取得容器主进程 PID:
PID=$(docker inspect -f '{{.State.Pid}}' demo)
cat /proc/"$PID"/cgroup
在 cgroup v2 中可能看到:
0::/system.slice/docker-<id>.scope
然后将该相对路径拼接到 /sys/fs/cgroup。如果 Docker 使用 systemd cgroup 驱动,路径通常位于 system.slice 下;不能假定所有机器都有 /sys/fs/cgroup/docker/<id>。
memory.max 是硬限制
在 cgroup v2 中:
memory.max是硬限制;memory.current是当前计量值;memory.high是高水位线,不是立即杀进程的硬上限;memory.events记录触发过的内存事件。
例如:
cat "$CG/memory.events"
可能得到:
low 0
high 12
max 3
oom 1
oom_kill 1
这些字段不能按同一个含义解释:
high增加:任务越过memory.high,内核对其施加回收和节流压力;max增加:尝试超过memory.max;oom增加:发生了 cgroup 内存不足处理;oom_kill增加:内核实际杀死了 cgroup 中的进程。
当内存接近 memory.max 时,内核首先尝试回收可回收页面,例如干净文件页缓存。只有在无法通过回收、内存申请仍然无法满足时,才进入 OOM 处理。
可以把过程简化为:
其中:
- 是当前 cgroup 需要满足的可计量内存;
- 是
memory.max; - 当 超过 时,内核先尝试回收;
- 若回收后仍有 ,则触发 OOM 路径。
这解释了一个常见反例:容器曾经达到 500 MiB,但限制是 512 MiB,并不意味着它必然 OOM。若其中 100 MiB 是可回收页缓存,内核可以先回收它。反过来,容器只显示 450 MiB,也可能因瞬时分配、不可回收匿名页或记账时序而触发失败。
一个可重复的内存实验
在测试机上运行:
docker run --rm --name memtest \
--memory=64m \
python:3.12-alpine \
python -c '
a = []
while True:
a.append(bytearray(1024 * 1024))
'
这个程序不断分配匿名内存。可能观察到:
docker wait memtest
输出:
137
随后检查事件:
docker events --since 1m --filter container=memtest
并查看内核日志:
dmesg -T | grep -Ei 'oom|out of memory|killed process'
该实验的风险是触发宿主机压力,因此应只在有足够余量的测试机运行。64 MiB 是容器硬限制,不是“保证程序只能分配 64 MiB”;运行时本身、线程栈和其他内存也需要计入。
--oom-kill-disable 不是增加内存
Docker 支持 --oom-kill-disable,但它并不会增加内存,也不会让超限分配成功。它只是影响 OOM killer 的行为。在没有同时设置内存限制、或宿主机整体内存不足时使用它,可能使问题扩散到宿主机。
一个容器内的子进程被杀死后,主进程可能仍然存在,容器也可能暂时保持运行。因此不能只通过“容器是否退出”判断是否发生过 OOM,应检查:
docker inspect demo --format '
OOMKilled={{.State.OOMKilled}}
ExitCode={{.State.ExitCode}}
Error={{.State.Error}}
'
OOMKilled=true 是 Docker 对容器状态的记录;内核层面还应结合 memory.events 和内核日志确认。
swap 的边界
--memory 与 --memory-swap 的行为依赖 Linux swap 是否存在、Docker 是否能配置对应 cgroup 文件以及 cgroup 版本。常见规则是:
memory=512m、memory-swap=1g:内存与 swap 合计上限约 1 GiB;memory=512m、memory-swap=512m:通常表示不允许额外 swap;- 不设置
memory-swap时,不能简单断言所有环境都会有相同的 swap 行为。
swap 能缓解短时内存压力,但会把匿名页换出到块设备,导致 I/O 等待和尾延迟上升。对延迟敏感服务,必须把“避免 OOM”和“允许 swap”作为两个独立的取舍来评估。
OOM:谁杀了谁,必须区分层级
OOM 是“无法满足内存分配请求”的故障处理机制,但至少有三个层级:
- 进程级或 cgroup 级 OOM:容器内存限制被触发。
- 宿主机级 OOM:整台 Linux 主机可用内存不足。
- 虚拟机或云平台级处置:例如宿主机回收、实例重启。
cgroup OOM 只应选择受限 cgroup 内的进程;宿主机 OOM 则可能杀死任意进程,包括 Docker daemon、数据库或另一个容器。
因此,诊断顺序应当是:
docker inspect <container> --format '{{json .State}}'
docker events --since 30m
dmesg -T | grep -Ei 'oom|out of memory|killed process'
cat "$CG/memory.events"
如果 oom_kill 增加,且内核日志显示被杀进程属于该容器,证据链较完整。若只有退出码 137,没有 OOM 事件,就不能直接下结论。
CPU Throttle:CPU 不够用的两种完全不同的情况
CPU quota 的形式化含义
Linux CFS 带宽控制通常使用周期 和配额 :
- :调度周期,常见单位为微秒;
- :每个周期允许该 cgroup 使用的 CPU 时间;
- 可用 CPU 比例近似为:
例如:
docker run -d --name cputest \
--cpu-period=100000 \
--cpu-quota=50000 \
alpine sh -c 'while :; do :; done'
每 100 ms 周期最多使用 50 ms CPU,因此 。这相当于平均半个逻辑 CPU 的带宽,而不是固定绑定到某一颗 CPU。
--cpus=0.5 是更直观的写法,通常会转换为相近的 period/quota 配置。
CPU throttle 的状态变化
一个周期内的状态可以这样推导:
- 周期开始,cgroup 获得 的运行预算;
- 一个或多个线程运行,消耗预算;
- 当累计消耗达到 时,cgroup 被标记为 throttled;
- 该 cgroup 中仍然 runnable 的线程不能继续运行;
- 下一个周期开始,预算恢复。
因此,进程可能看到:
- 宿主机 CPU 仍然很空闲;
- 容器 CPU 使用率只有 50%;
- 但请求延迟显著上升。
这是因为容器的额度耗尽,并不意味着整台机器没有空闲 CPU。
在 cgroup v2 中检查:
cat "$CG/cpu.max"
cat "$CG/cpu.stat"
可能得到:
50000 100000
usage_usec 8123456
user_usec 7000000
system_usec 1123456
nr_periods 1200
nr_throttled 800
throttled_usec 45000000
关键变量是:
nr_periods:经过的调度周期数;nr_throttled:发生过节流的周期数;throttled_usec:累计被节流的时间。
一个有意义的比例是:
但不能只看 nr_throttled / nr_periods。如果每次只节流很短时间,比例高不一定严重;如果节流总时长很大,即使周期比例不高,也可能影响尾延迟。
CPU 高使用率与 throttle 的反例
设容器限制为 0.5 CPU:
- 容器实际使用 0.49 CPU;
- 没有 throttle;
- 宿主机还有很多空闲 CPU。
此时容器已经接近自身配额,但不能说它被 throttle。
另一个场景:
- 容器实际使用 0.45 CPU;
throttled_usec持续增加;- 容器内有 8 个 runnable 线程;
- 宿主机还有 4 个空闲 CPU。
这可能是突发并发在每个周期前半段耗尽额度,后半段所有线程等待下一个周期。平均使用率不高,但延迟和 PSI CPU 会变差。
cpu.weight、--cpu-shares 不是硬配额
CPU weight 是竞争时的相对权重。只有当多个 cgroup 同时争用同一 CPU 资源时,权重才决定相对分配;它不会保证某个容器一定获得某个 CPU 百分比,也不会单独产生 quota 式硬节流。
因此:
--cpus、--cpu-quota更接近硬上限;--cpu-shares或 cgroup v2 的cpu.weight是竞争优先级;--cpuset-cpus是 CPU 亲和性约束,不等于 CPU 配额。
诊断 CPU 时至少同时观察:
docker stats <container>
cat "$CG/cpu.stat"
cat /proc/pressure/cpu
docker stats 适合快速查看,cgroup 文件适合判断是否真正发生 quota throttle,PSI 适合判断系统或该 cgroup 是否有任务因 CPU 不可运行而等待。
PSI:从“资源使用了多少”转向“任务等了多久”
PSI 的定义
Pressure Stall Information,简称 PSI,记录任务因为某类资源不足而无法继续推进的时间比例。它关注的是 stall,不是资源占用率。
Linux 常见全局文件:
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io
cgroup v2 中,若内核和挂载配置支持,还可以在 cgroup 目录看到:
cat "$CG/cpu.pressure"
cat "$CG/memory.pressure"
cat "$CG/io.pressure"
典型输出:
some avg10=12.50 avg60=4.10 avg300=1.20 total=987654
full avg10=1.50 avg60=0.20 avg300=0.05 total=12345
含义是:
some:至少有一个任务因该资源等待;full:该 cgroup 中所有可运行任务同时因该资源等待;avg10、avg60、avg300:最近 10、60、300 秒内的平均 stall 百分比;total:累计 stall 时间,通常以微秒表示。
例如 cpu some avg10=20 不是“CPU 使用率 20%”,而是观察窗口内累计有约 20% 的时间,至少一个任务因 CPU 不可得而停顿。
PSI 与 CPU throttle 的关系
CPU quota throttle 会让任务不能运行,因此可能增加 CPU PSI;但二者不是同一个指标:
- CPU throttle:该 cgroup 的配额是否耗尽;
- CPU PSI:任务是否因 CPU 不可得而等待;
- 系统 CPU PSI:整个系统是否存在 CPU 运行队列压力。
以下情况可能出现:
| 现象 | 可能原因 |
|---|---|
| throttle 高,系统 CPU PSI 低 | 容器自身配额太小,宿主机仍空闲 |
| throttle 低,系统 CPU PSI 高 | 多个任务在宿主机上竞争 CPU,但该容器未被 quota 限制 |
| CPU 使用率低,CPU PSI 高 | 线程频繁等待运行,或配额周期造成间歇性停顿 |
| CPU 使用率高,CPU PSI 低 | 容器持续获得 CPU,计算密集但不排队 |
PSI 的 full 对服务尤其有价值:如果所有任务都在等待,通常意味着该 cgroup 在某些时间点完全无法推进;但它仍然不能替代应用级指标和线程分析。
I/O:限速、设备拥塞和文件系统缓存要分开看
I/O 的数据流
容器中的路径通常经过多层:
应用
-> 进程文件描述符
-> overlay2 上层目录或 bind mount
-> page cache / writeback
-> 块设备队列
-> 虚拟磁盘或物理磁盘
容器隔离并不意味着容器拥有独立磁盘。多个容器可能共享同一宿主机块设备,Docker 的 I/O 限制也必须作用于实际承载数据的设备。
写入时,write() 成功可能只表示数据进入 page cache,并不表示已经落盘。真正的设备拥塞可能在后续 writeback 阶段出现。因此“应用写系统调用很快”与“磁盘没有压力”并不矛盾。
Docker I/O 限制
常见运行参数包括:
docker run -d --name iotest \
--read-bps=/dev/sdb:10mb \
--write-iops=/dev/sdb:100 \
nginx:alpine
参数作用于指定设备:
--read-bps:限制读取吞吐;--write-bps:限制写入吞吐;--read-iops:限制读取操作次数;--write-iops:限制写入操作次数。
设备名必须是容器所在 Linux 环境能够识别、且承载目标数据的块设备。对 bind mount,不能想当然地把设备写成容器内路径;应在宿主机确认:
findmnt -T /var/lib/docker
findmnt -T /path/to/bind-mounted-data
lsblk -o NAME,TYPE,FSTYPE,MOUNTPOINTS
cgroup v2 中可以查看:
cat "$CG/io.max"
cat "$CG/io.stat"
io.stat 常见字段包括读写字节数和 I/O 次数;具体字段由内核版本和设备支持情况决定。它说明该 cgroup 产生了多少 I/O,不直接说明这些 I/O 花费了多少时间。
I/O PSI 与设备指标
I/O 压力应结合多层指标:
cat /proc/pressure/io
iostat -xz 1
pidstat -d -p <pid> 1
vmstat 1
重点观察:
iostat中设备的await:I/O 平均等待时间;%util:设备忙碌程度,但在现代多队列设备上不能孤立解释;r_await、w_await:读写等待;vmstat中的wa:CPU 等待 I/O 的时间比例;- PSI I/O:任务因 I/O 无法推进的时间;
- cgroup
io.stat:容器是否真的发起了大量 I/O。
一个典型故障推导是:
- 容器
io.stat的写入字节持续增加; iostat显示宿主机设备await上升;/proc/pressure/io的some上升;- 应用线程在
write()、fsync()或后台 flush 上等待; - 请求延迟上升。
如果只有 io.stat 数字高、设备 await 很低、PSI 也接近零,那么这是“使用量高”而不是“当前 I/O 瓶颈”。
overlay2 的写放大和日志
容器可写层通常使用 overlay2。频繁修改大文件可能触发 copy-up:文件第一次在上层修改时,需要从只读层复制到可写层。这会造成额外 I/O 和磁盘空间消耗。
应用日志写到 stdout/stderr 后,Docker logging driver 还可能将其写入宿主机。默认 json-file 日志若没有轮转,日志增长会:
- 消耗宿主机磁盘;
- 增加写 I/O;
- 在磁盘满时导致应用写入失败或 Docker 行为异常。
因此 I/O 诊断要区分:
- 数据目录;
- overlay2 可写层;
- 容器 stdout/stderr 日志;
- bind mount 或 volume;
- swap 和内核 writeback。
一次完整的诊断流程
第一步:确认容器实际状态
docker ps -a --no-trunc
docker inspect app --format '
Status={{.State.Status}}
Running={{.State.Running}}
ExitCode={{.State.ExitCode}}
OOMKilled={{.State.OOMKilled}}
Error={{.State.Error}}
Pid={{.State.Pid}}
'
先确认是:
- 进程退出;
- 容器被重启;
- 健康检查失败;
- OOMKilled;
- Docker daemon 或宿主机异常。
不要先看到 137 就直接修改内存上限。
第二步:确认限制和实际 cgroup
docker inspect app --format '
Memory={{.HostConfig.Memory}}
MemorySwap={{.HostConfig.MemorySwap}}
NanoCpus={{.HostConfig.NanoCpus}}
CpuQuota={{.HostConfig.CpuQuota}}
CpuPeriod={{.HostConfig.CpuPeriod}}
BlkioWeight={{.HostConfig.BlkioWeight}}
PidsLimit={{.HostConfig.PidsLimit}}
'
取得 PID 和 cgroup:
PID=$(docker inspect -f '{{.State.Pid}}' app)
test "$PID" -gt 0
cat /proc/"$PID"/cgroup
PID 为 0 通常表示容器当前未运行,此时不能再从进程路径读取实时 cgroup 文件。
第三步:采集一段时间的增量,而非只看单点
CPU throttle、I/O 计数和 PSI 都是时间相关指标。单次 cat 很难说明问题:
while sleep 1; do
date
cat "$CG/cpu.stat"
cat "$CG/memory.events"
cat "$CG/memory.current"
cat "$CG/cpu.pressure" 2>/dev/null || true
cat "$CG/io.pressure" 2>/dev/null || true
done
生产环境不应无限制地打印高频采样结果;应交给 node exporter、cAdvisor、OpenTelemetry 或已有监控系统采集,并保存时间序列。
第四步:对齐应用指标
系统指标必须与应用时间线对齐:
- 请求 P95/P99 延迟;
- 活跃请求数;
- GC 暂停;
- 线程数;
- 队列长度;
- 数据库连接等待;
- 日志写入量;
- 重启时间。
例如 P99 在固定的 100 ms 周期附近出现尖峰,同时 throttled_usec 增长,往往比“CPU 使用率 50%”更能证明 quota throttle 影响了延迟。
使用 docker stats 时不要误读
docker stats --no-stream app
它适合快速查看:
- 内存使用和限制;
- CPU 使用率;
- 网络流量;
- 块设备读写;
- PIDs 数量。
但它存在重要边界:
- CPU 百分比是采样窗口内的使用率,不直接显示
throttled_usec。 - 内存数字受 Docker 版本、cgroup 版本和统计口径影响,不应替代
memory.current、memory.stat。 - BLOCK I/O 是累计读写量,不是设备延迟。
- 网络 I/O 只表示容器网络命名空间可见的流量,不等于应用端到端吞吐。
- 单次
--no-stream只提供一个瞬时采样,不能判断趋势和增量。
docker stats 的正确定位是“快速筛查”,不是 OOM、节流和 I/O 瓶颈的最终证据。
Compose 中的限制与验证
Compose 可以表达常见资源限制,例如:
services:
api:
image: nginx:alpine
mem_limit: 512m
memswap_limit: 1g
cpus: 0.5
pids_limit: 256
blkio_config:
device_read_bps:
- path: /dev/sdb
rate: 10mb
device_write_iops:
- path: /dev/sdb
rate: 100
启动后应验证实际生效配置:
docker compose config
docker compose up -d
docker compose ps
docker inspect "$(docker compose ps -q api)" \
--format 'Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}}'
需要区分 Compose 规范和部署环境:
mem_limit、cpus等由本地 Docker Engine 创建容器时执行;- 某些
deploy.resources字段主要面向编排器,不能假设在所有非 Swarm 场景中都以相同方式生效; - cgroup v1/v2、Docker rootless、宿主机内核能力和设备路径都会影响最终效果;
- 配置文件声明成功,不等于宿主机实际提供了对应控制器。
常见错误判断
“容器内存还有余量,所以不是内存问题”
错误原因:单点内存使用可能遗漏瞬时峰值、页缓存回收、多个进程总和或宿主机级压力。
验证方式:
watch -n 0.5 '
cat /sys/fs/cgroup/.../memory.current
cat /sys/fs/cgroup/.../memory.events
'
同时查看应用进程 RSS、运行时堆和宿主机内存。
“CPU 使用率只有 40%,所以 CPU 不可能是瓶颈”
错误原因:40% 可能是受限于一个很小的 quota;也可能是线程在周期性等待。
验证:
cat "$CG/cpu.stat"
cat "$CG/cpu.max"
cat "$CG/cpu.pressure"
“磁盘吞吐没有跑满,所以不是 I/O 问题”
错误原因:随机 I/O、同步写、低队列深度或单线程串行写入可能在低吞吐下产生高延迟。
验证:
iostat -xz 1
pidstat -d -p "$PID" 1
cat /proc/pressure/io
“PSI 高就说明某个容器被限流”
错误原因:全局 PSI 反映整台机器,可能由其他容器、内核回写或宿主机任务造成。
应优先查看容器 cgroup 的 *.pressure;若没有 cgroup PSI,再将全局 PSI、容器计数器和应用时间线结合分析。
“增加 --memory 就能修复所有 OOM”
错误原因:可能真正的问题是:
- 内存泄漏;
- 缓存没有上限;
- 子进程或线程数量失控;
- swap 引发 I/O 压力;
- 宿主机总体内存不足;
- 日志或数据目录导致系统级资源耗尽。
扩大限制只是改变触发点,不能替代确认分配来源和生命周期。
生产取舍:限制应与故障隔离一起设计
内存限制过小,会增加 cgroup OOM;限制过大,则可能让单个容器挤压其他服务并诱发宿主机 OOM。CPU quota 过小会产生周期性 throttle;只设置权重又无法阻止单个服务在低竞争时使用大量 CPU。I/O 限速可以保护共享设备,但过低的限速会把业务等待隐藏成应用延迟。
因此资源治理至少需要同时记录:
- 配置的限制:
memory.max、cpu.max、io.max; - 实际使用:
memory.current、CPU usage、io.stat; - 失败计数:
memory.events、进程退出和重启; - 等待程度:CPU、内存、I/O PSI;
- 业务结果:延迟、错误率、队列和吞吐。
容量告警不应只设置“内存使用率超过 80%”。更有诊断价值的组合是:
memory.current / memory.max持续升高;memory.events中high或oom_kill增量出现;- CPU
throttled_usec在业务高延迟窗口持续增加; - I/O PSI 与设备
await同时升高; - 容器重启、退出码和应用错误在同一时间线上出现。
最终要回答的不是“哪个数字变红了”,而是:
- 哪个 cgroup 受到了哪种限制;
- 内核采取了回收、节流还是杀进程;
- 任务实际等待了多久;
- 等待是否与业务延迟或错误同时发生;
- 调高限制、降低并发、修复泄漏或迁移 I/O 后,计数器和业务指标是否恢复。
只有把 Docker 配置、cgroup 状态、PSI、内核日志、设备指标和应用时间线连成一条证据链,OOM 与性能诊断才不会停留在猜测层面。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 网络故障诊断:Namespace、Bridge、DNS、NAT、MTU 和抓包
- 下一篇:Docker 在 Linux、macOS 与 Windows 的差异:VM、路径、网络和性能
- 延伸:Docker 资源治理:CPU、内存、PID、I/O、cgroups 与 OOM
- 延伸:Docker 日志与监控:Logging Driver、Metrics、事件和容量告警
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论