原子网络科技 原子网络科技

Docker容器运行时参数调优与cgroup限制详解:避免单个容器耗尽宿主机资源的实战经验

a169kai 2026年11月12日 · 阅读约 5 分钟

在多容器共存的宿主机上,没有受到任何限制的容器就如同没有熔断机制的线程一样——内存泄漏、死循环和fork风暴会在几分钟之内使整个系统崩溃掉。OOM Killer会首先杀死的是肇事者本身,而不是SSH、监控Agent或者数据库进程等正常运行的服务。正确的做法并不是等到出现问题才去重启,而是在cgroup层面上给每个容器划定好资源边界。

一、cgroup 才是真正起作用的

Docker 的 --memory 和 --cpus 参数是声明式的,它们都会被转换成 Linux 内核中的控制组:

  • cgroup v1中,CPU、内存和IO分别有不同的层次,《memory.limit_in_bytes》和《cpu.cfs_quota_us》用不同的文件来管理
  • cgroup v2:采用统一的层次结构,memory.max、memory.high、cpu.max、pids.max等参数一起进行管理,包括页面缓存和套接字缓冲区在内的所有资源都被算作内存

v2有一个隐藏的问题:读取大文件时生成的page cache会被计入到容器的内存中,从而造成“堆内存很小但是被OOM”的情况。在排查的时候不能只看应用层的RSS,还要查看memory.current以及memory.events。

确认宿主机版本:

stat -fc %T /sys/fs/cgroup
# cgroup2fs = v2,tmpfs = v1
docker info | grep -i "cgroup version"

二、内存:先固定好顶部,然后再讨论Swap策略

1. 基本的硬性约束

docker run -d \
  --name api \
  --memory=1.5g \
  --memory-swap=1.5g \
  --memory-reservation=1g \
  myapp:latest
  • --memory:硬顶,超过直接 OOM Kill(退出码 137)
  • --memory-swap=同值:不允许使用Swap来代替内存,一旦出现错误就会马上被发现
  • --memory-reservation:软限制,在内存不足的时候会被回收掉,但是不会杀死进程

数据库和缓存服务应该关闭swap,而批处理和CI构建可以保留一些swap作为缓冲区。

2. JVM/Node要明码标价

老的JVM没有读取cgroupv2的功能,在计算堆大小时使用的是宿主机的物理内存:

# Java 11+
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -jar app.jar
# Node
NODE_OPTIONS="--max-old-space-size=1024" node server.js

如果设置了1.5G的内存限制的话,那么JVM仍然会请求8G的内存,这样就会导致内存溢出。

三、CPU:硬配额和权重分别使用

docker run -d \
  --name worker \
  --cpus=1.5 \
  --cpu-shares=512 \
  --cpuset-cpus=0,1 \
  myworker:latest
  • --cpus=1.5:CFS硬配额,相当于v2中的cpu.max(即150000 100000=每100ms最多使用1.5个CPU)
  • --cpu-shares=512:只有存在竞争的情况下才有效,默认值为1024,并不是一个限制
  • --cpuset-cpus:绑定核心,适用于对延迟要求较高的服务,但是会降低调度的灵活性

常见的错误就是把 --cpu-shares 当作上限来使用,这样容器仍然可以占用全部4个CPU核心。

查看节流情况:

cat /sys/fs/cgroup/cpu.stat
# nr_throttled 持续增长 = CPU 配额给少了

四、进程数、文件句柄和I/O:防止“隐形耗尽”

1. PID 上限(防止fork炸弹)

--pids-limit=200

如果不设置的话,一个容器fork出去之后就会把宿主机的PID表全部填满,所有的服务都会失去连接。

2. 文件描述符

--ulimit nofile=65535:65535
--ulimit nproc=4096

Nginx、网关和WebSocket的服务应该提高一些,不然的话就会遇到EMFILE的问题了。

3. 磁盘I/O和临时空间

--blkio-weight=500
--tmpfs /tmp:size=100m
--shm-size=256m
  • blkio-weight:相对IO优先级(100-1000),数据库为高,日志收集为低
  • /dev/shm 默认只有64MB,在Chrome、Postgres和Redis哨兵等情况下需要扩大

五、Compose 中使用的声明式基线

services:
  web:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
          pids: 100
        reservations:
          memory: 128M
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
    tmpfs:
      - /tmp:size=100m

必须在创建的时候说明一下,不能依靠运行时的修改来改变它——一旦重新启动就会丢失。

六、检验限制是否真的起作用

# 容器视角
docker stats --no-stream

# cgroup v2 直接读文件
CG=$(docker inspect -f '{{.Id}}' api)
cat /sys/fs/cgroup/system.slice/docker-${CG}.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-${CG}.scope/cpu.max
cat /sys/fs/cgroup/system.slice/docker-${CG}.scope/pids.max

# 确认是否 OOM 致死
docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}}' api

容器中的 free 显示的是宿主机的总内存量,应该用 cgroup 文件里的数据来判断。

七、生产环境默认基线(2核4G宿主机示例)

维度推荐值目的
内存单容器 ≤ 1.2G,预留 800M 给系统防系统级 OOM
CPU单容器 ≤ 1.5 核留 0.5 核给内核与 Dockerd
PID100–256防 fork 风暴
Swap等同内存或全禁故障快速暴露
日志配 json-file max-size 10m max-file 3防磁盘被日志吃满
关键服务oom-score-adj 调低降低被误杀概率

daemon 层全局兜底(/etc/docker/daemon.json):

{
  "default-ulimits": {
    "nofile": { "Name": "nofile", "Hard": 65535, "Soft": 65535 }
  },
  "oom-score-adjust": -500
}

八、典型事故复盘

现象:Java 服务半夜退出,Exit Code 137,同机 MySQL 也连不上。

根因:没设 --memory,JVM 按宿主机 16G 初始化堆,实际占用 12G,触发宿主机全局 OOM,先杀监控再杀数据库。

修复:

--memory=2g --memory-swap=2g \
-e JAVA_OPTS="-XX:MaxRAMPercentage=70"

上线之后没有发生类似的事故。

总结

容器资源调优并不是简单地分配多少资源的问题,而是要画出更严格的界限来。内存采用硬性上限+应用层堆对齐的方式,CPU使用硬性配额而不是权重,PID、fd等隐藏泄露被堵住,在cgroup v2中用memory.max和cpu.max的实际值作为标准。把限制写成声明、验证写入磁盘、监控节流以及OOM事件,单个容器失控不会导致整个宿主机崩溃。

0 0 投票数
文章评分
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票