☰
容器资源限制与监控:从 --memory 到 Prometheus
2026/9/28 18:06:53 网站建设 项目流程

1. 引言

容器化部署早已成为后端开发的标配,但很多同学对容器的资源限制仍停留在「启动时加几个参数」的层面。--memory、--cpus这些参数到底限制了什么?容器被 OOM 杀掉时,系统里发生了什么?为什么 JVM 在容器里经常「不听话」?监控面板上的指标又是从哪里来的?

这篇文章会从 Docker 的资源限制参数讲起,完整复现一次「容器 OOM」事故,带你走一遍从现象到根因的排查路径,最后用 cAdvisor + Prometheus + Grafana 搭一套容器监控面板。全文约 4500 字,建议配合命令行实际操作。

2. 前置依赖

阅读本文前,建议先掌握以下内容(对应本系列前两篇):

  • Docker 基础操作:docker run、docker ps、docker exec、docker logs
  • Linux 基础命令:top、free、dmesg、cat /proc/...

实验环境建议:

  • Linux 内核 5.10+(完整支持 cgroup v2)
  • Docker Engine 20.10+
  • 至少 2 核 CPU、4 GB 内存的机器或虚拟机

提示:如果你用的是 macOS 或 Windows 的 Docker Desktop,底层是虚拟机,cgroup 版本取决于虚拟机的内核配置,实验现象可能与 Linux 直装略有差异。

3. 资源限制参数详解

3.1 --memory:内存上限

--memory(简写-m)限制容器最多能使用的物理内存,单位支持b、k、m、g。

dockerrun-d--namedemo-mem--memory512m nginx

这个限制是硬限制:当容器内进程的内存使用超过 512 MB 时,内核会触发 OOM 机制,优先杀掉容器内占用内存最多的进程。

3.2 --memory-swap:交换分区上限

--memory-swap限制容器内存 + swap 的总和。它必须与--memory一起使用,且值必须大于等于--memory。

# 内存 512m,swap 上限 256m(总上限 768m)dockerrun-d--namedemo-swap--memory512m --memory-swap 768m nginx# 内存 512m,swap 无上限(不推荐)dockerrun-d--namedemo-swap2--memory512m --memory-swap-1nginx

坑位提醒:很多人以为--memory-swap是「swap 的上限」,其实它是「内存 + swap 的总上限」。如果只设--memory不设--memory-swap,Docker 默认--memory-swap等于--memory的两倍,即容器默认可以用 swap,只是总量被限制。

3.3 --cpus:CPU 配额

--cpus限制容器可以使用的 CPU 核数,支持小数:

# 限制最多使用 1.5 个 CPU 核dockerrun-d--namedemo-cpu--cpus1.5nginx

它底层是 cgroup 的cpu.max(cgroup v2)或cpu.cfs_quota_us / cpu.cfs_period_us(cgroup v1)。注意:--cpus是软限制,容器内进程可以短暂超过配额,但会被内核调度器持续压制。

3.4 --pids-limit:进程数上限

--pids-limit限制容器内同时存在的进程/线程总数:

# 限制容器内最多 100 个进程dockerrun-d--namedemo-pids --pids-limit100nginx

坑位提醒:这个参数很容易误伤。比如 Java 应用每创建一个线程就是一个「进程」,线程池开大了、或者用了 ForkJoinPool,很容易撞上--pids-limit导致线程创建失败,报Unable to create native thread。

4. 完整复现一次容器 OOM

4.1 准备一个「内存黑洞」镜像

先写一个会持续吃内存的小程序。用 Python 最方便:

# oom_demo.pyimporttime data=[]whileTrue:# 每次分配 10 MBdata.append(b"x"*10*1024*1024)print(f"allocated{len(data)*10}MB",flush=True)time.sleep(0.1)

写一个 Dockerfile:

FROM python:3.11-slim WORKDIR /app COPY oom_demo.py . CMD ["python", "oom_demo.py"]

构建镜像:

dockerbuild-toom-demo.

4.2 启动容器并观察现象

# 限制内存 128m,不给 swapdockerrun-d--nameoom-test--memory128m --memory-swap 128m oom-demo

观察容器状态:

dockerps-a

你会看到容器状态变成Exited (137)。137 = 128 + 9,即被 SIGKILL(信号 9)杀死。这是 OOM 杀进程的典型退出码。

4.3 查看 OOM 日志

dockerlogs oom-test

日志会停在某个分配量附近,比如:

allocated 120 MB allocated 130 MB

然后戛然而止——进程被内核直接杀掉,来不及打印任何错误。

4.4 用 dmesg 定位根因

OOM 的真正原因在内核日志里:

dmesg|grep-ioom

你会看到类似这样的输出:

oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker-xxx,mems_allowed=0-1,oom_memcg=/docker/xxx,task_memcg=/docker/xxx,task=python,pid=1234,uid=0 Memory cgroup out of memory: Killed process 1234 (python) total-vm:...kB, anon-rss:...kB, file-rss:...kB, shmem-rss:...kB, UID:0 pgtables:...kB oom_score_adj:1000

关键信息解读:

  • oom_memcg=/docker/xxx:被 OOM 的 cgroup 是哪个容器
  • task=python:被杀的进程
  • oom_score_adj:1000:该进程的 OOM 优先级(Docker 默认给容器内进程加 1000,优先被杀)

4.5 从现象到根因的完整路径

步骤现象排查手段结论
1容器退出,状态Exited (137)docker ps -a进程被 SIGKILL
2日志戛然而止docker logs进程被强杀,无业务报错
3内核记录 OOM 事件dmesg | grep -i oom内存 cgroup 超限
4确认限制参数docker inspect oom-test--memory 128m触发

5. docker stats 与 docker events

5.1 docker stats:实时资源占用

dockerstats

输出示例:

CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS a1b2c3d4e5f6 web 0.50% 256MiB / 512MiB 50.00% 1.2kB / 0B 0B / 0B 12

docker stats的数据来源是 cgroup 的统计文件,适合临时排查,不适合长期监控。

5.2 docker events:事件流

dockerevents--filtercontainer=oom-test

当容器被 OOM 杀掉时,你会收到:

2026-09-27T08:30:00.000000000Z container die oom-test (image=oom-demo, ...)

docker events能帮你捕捉容器的生命周期事件(创建、启动、停止、死亡),是排查「容器什么时候挂的」的好帮手。

6. cgroup v2 下的差异

6.1 如何确认当前 cgroup 版本

stat-fc%T /sys/fs/cgroup/
  • 输出cgroup2fs→ cgroup v2
  • 输出tmpfs→ cgroup v1

6.2 主要差异

维度cgroup v1cgroup v2
内存限制文件memory.limit_in_bytesmemory.max
CPU 限制文件cpu.cfs_quota_us+cpu.cfs_period_uscpu.max
进程数限制pids.max(独立控制器)pids.max(统一控制器)
内存压力通知需手动配置内置memory.events
OOM 事件分散在各控制器统一在memory.events的oom字段

cgroup v2 下查看容器内存限制:

cat/sys/fs/cgroup/memory.max

查看 OOM 事件计数:

cat/sys/fs/cgroup/memory.events

输出:

oom 3 oom_kill 2

这比 v1 时代方便得多——oom表示触发过几次 OOM 判定,oom_kill表示实际杀过几次进程。

7. JVM 不感知 cgroup 的坑

7.1 问题现象

在容器里跑 Java 应用,明明限制了--memory 512m,JVM 却按宿主机内存(比如 16 GB)来算默认堆大小,结果:

  • JVM 启动时申请了远超容器限制的堆内存
  • 容器被 OOM 杀掉,JVM 连 GC 的机会都没有

7.2 原因

老版本 JDK(8u131 之前)不感知 cgroup,-XX:+UseContainerSupport默认关闭。JVM 通过/proc/meminfo读取的是宿主机的内存,而不是容器 cgroup 的限制。

7.3 解决方案

方案一:升级 JDK 到 8u191+ 或 JDK 10+,默认开启容器感知。

方案二:显式指定堆大小:

dockerrun-d--memory512m\-eJAVA_OPTS="-Xmx256m -XX:MaxMetaspaceSize=128m"\my-java-app

方案三:使用-XX:MaxRAMPercentage按比例分配:

dockerrun-d--memory512m\-eJAVA_OPTS="-XX:MaxRAMPercentage=50.0"\my-java-app

坑位总结:老版本 JDK 在容器里默认堆大小 = 宿主机内存的 1/4,极易触发 OOM。升级 JDK 或显式设置堆大小是必做项。

8. cAdvisor + Prometheus + Grafana 搭建监控面板

8.1 架构总览

Docker 容器

cAdvisor

Prometheus

Grafana

监控面板

  • cAdvisor:采集容器资源指标(CPU、内存、网络、磁盘)
  • Prometheus:时序数据库,负责存储和查询指标
  • Grafana:可视化面板

8.2 启动 cAdvisor

dockerrun-d\--namecadvisor\--restartalways\-p8080:8080\-v/:/rootfs:ro\-v/var/run:/var/run:ro\-v/sys:/sys:ro\-v/var/lib/docker/:/var/lib/docker:ro\-v/dev/disk/:/dev/disk:ro\--privileged\--device=/dev/kmsg\gcr.io/cadvisor/cadvisor:latest

验证:

curlhttp://localhost:8080/metrics|head-20

8.3 配置 Prometheus

写一个prometheus.yml:

global:scrape_interval:15sscrape_configs:-job_name:"cadvisor"static_configs:-targets:["cadvisor:8080"]

启动 Prometheus:

dockerrun-d\--nameprometheus\--restartalways\-p9090:9090\-v$(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml\prom/prometheus:latest

验证:

curlhttp://localhost:9090/api/v1/status/targets

8.4 启动 Grafana

dockerrun-d\--namegrafana\--restartalways\-p3000:3000\grafana/grafana:latest

访问http://localhost:3000,默认账号密码admin/admin。

8.5 配置数据源与面板

  1. 在 Grafana 中Configuration → Data Sources → Add data source,选择 Prometheus
  2. URL 填http://prometheus:9090(如果 Grafana 和 Prometheus 在同一 Docker 网络)
  3. 导入官方面板 ID:893(cAdvisor 容器监控面板)
  4. 选择数据源后即可看到容器 CPU、内存、网络、磁盘的实时图表

8.6 常用 PromQL 查询

容器内存使用率:

100 * (1 - (container_memory_working_set_bytes{name=""} / container_spec_memory_limit_bytes{name=""}))

容器 CPU 使用率:

rate(container_cpu_usage_seconds_total{name=""}[5m]) * 100

容器 OOM 事件计数:

container_oom_events_total{name=""}

9. 坑位总结

9.1 swap 上限 vs 内存上限

--memory-swap是「内存 + swap 的总上限」,不是 swap 单独的上限。只设--memory时,Docker 默认给容器分配等量的 swap 额度,可能导致容器在内存超限后先吃 swap 再被杀,延迟了 OOM 的触发,掩盖了真实的内存问题。

9.2 JVM 不感知 cgroup

老版本 JDK 按宿主机内存计算默认堆大小,容器限制形同虚设。升级 JDK 或显式设置-Xmx/-XX:MaxRAMPercentage。

9.3 --pids-limit 误伤

--pids-limit限制的是容器内所有线程/进程的总数。Java 应用线程池、Python 多进程模型都容易撞上限。设置前先评估应用的线程模型,别拍脑袋定一个很小的值。

10. 总结

本文从 Docker 资源限制参数讲起,完整复现了一次容器 OOM 事故,并给出了从docker ps到dmesg的排查路径。随后对比了 cgroup v1/v2 的差异,剖析了 JVM 不感知 cgroup 的经典坑,最后用 cAdvisor + Prometheus + Grafana 搭建了一套容器监控面板。

核心要点回顾:

  • --memory是硬限制,超限即 OOM
  • --memory-swap是内存 + swap 的总上限
  • --cpus是软限制,会被调度器持续压制
  • --pids-limit限制进程/线程总数,容易误伤
  • 容器退出码 137 = SIGKILL,配合dmesg定位 OOM 根因
  • cgroup v2 用memory.events统一记录 OOM 事件
  • 老版本 JDK 不感知 cgroup,必须显式设置堆大小
  • cAdvisor + Prometheus + Grafana 是容器监控的标准组合

下一篇文章可以聊聊 Kubernetes 的 requests/limits 与 QoS 等级,看看容器资源管理在编排层面的进阶玩法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询