Docker 容器内 CPU 被限流排查:cfs_quota、cpuset 与 top 显示异常

    发布时间:2026-09-19 12:31 更新时间:2026-09-19 12:31 阅读量:0

    不少站长在 Docker 里跑编译任务或 PHP 常驻进程时会遇到一种怪现象:容器里 top 显示有 8 个 CPU,负载却怎么都上不去,压测时响应时间忽高忽低,宿主机整体 CPU 又没跑满。时区问题早就排查过了,日志时间也对得上,问题其实出在容器被 cgroup 限流了。这篇就从 cpu.cfs_quota、cpuset 和容器内的 CPU 视图讲起,把限流这件事说清楚。

    容器内的 top 为什么和宿主机不一样

    容器本质上还是宿主机上的普通进程,只是被 cgroup 和 namespace 圈了起来。CPU 相关的两个 namespace 是 CLONE_NEWCGROUP 和 CLONE_NEWNS,前者让容器看到自己的 cgroup 挂载点,后者配合 /proc 的挂载方式影响 top、nproc 读取的 CPU 数量。默认情况下,Docker 不会给容器做 CPU namespace 隔离,所以容器里的 /proc/cpuinfo 和 nproc 直接反映宿主机核心数——这就是「8 核」的来源,它并不代表你能用满 8 核。

    真正决定能用多少 CPU 的是 cgroup v1 里的两个文件:cpu.cfs_period_us 和 cpu.cfs_quota_us。前者是调度周期,默认 100000 微秒(100ms);后者是一个周期内允许占用的总时间。比如 --cpus=2 在 Docker 里会被翻译成 quota=200000、period=100000,也就是每 100ms 最多跑 200ms 的 CPU 时间,等价于 2 个核。如果 quota 用完了,这个周期内该 cgroup 的进程就会被强制挂起,直到下个周期开始,这段时间在容器里看就是「进程莫名卡住」。

    在宿主机和容器里读 cgroup 指标

    排查的第一步是找到容器对应的 cgroup 路径。用 docker inspect 拿到容器 ID,再拼接路径,cgroup v1 下通常是 /sys/fs/cgroup/cpu/docker/<ID>/。下面这组命令可以直接看配额和累计限流次数:

    CID=$(docker inspect -f '{{.Id}}' myapp)
    CPUDIR=/sys/fs/cgroup/cpu/docker/$CID
    cat $CPUDIR/cpu.cfs_period_us
    cat $CPUDIR/cpu.cfs_quota_us
    cat $CPUDIR/cpu.stat

    其中 cpu.cfs_quota_us 为 -1 表示不限流;cpu.stat 里的 nr_periods 是经历过的周期数,nr_throttled 是被限流的周期数,throttled_time 是累计被挂起的纳秒数。如果 nr_throttled 持续增长、throttled_time 明显变大,基本可以确认瓶颈在 CPU 配额而不是宿主机算力。容器内如果装了 cgroup 工具,也可以用 cat /sys/fs/cgroup/cpu/cpu.stat 读取,但要注意容器内看到的路径是相对挂载点,具体以实际挂载方式为准。

    另一种限流是 cpuset。用 --cpuset-cpus="0,1" 启动的容器,会被限制只能跑在 0、1 号核上,此时 quota 可能是不限的,但可用核心被钉死。检查 cpuset.cpus 和 cpuset.mems 就能看出来:

    cat /sys/fs/cgroup/cpuset/docker/$CID/cpuset.cpus
    cat /sys/fs/cgroup/cpuset/docker/$CID/cpuset.mems
    ps -o pid,psr,comm -p $(pgrep -d, -f myapp) | head

    ps -o psr 显示进程实际落在哪个核上,如果始终只在少数几个核之间跳,说明 cpuset 生效了。cpuset 和 quota 是两套独立机制,可以同时存在,排查时要分开看。

    常见误判与调整建议

    第一类误判是把容器内 top 的 CPU 百分比当成宿主机占用。容器里 top 读的是宿主机全局的 /proc/stat,看到的是整机数据,不是容器自己的;要看容器真实用量,应该用 docker stats 或直接读 cpuacct.usage。第二类误判是负载高就加 --cpus,但如果瓶颈其实是 IO 或锁竞争,加配额只会让限流统计变化,吞吐未必提升,建议先用 docker stats 和 pidstat -u 1 区分是 CPU 饱和还是等待。

    调整时,--cpus 适合希望容器按比例分享 CPU 的场景,比如多个 Web 容器共存;--cpuset-cpus 适合对缓存亲和性有要求、希望固定核的场景,比如数据库或高频交易类服务。二者混用时要注意:如果 cpuset 只给了 1 个核,即使 --cpus=4,实际也跑不出 4 核的并行度。修改配额需要重建容器或使用 docker update(部分参数支持热更新,具体以所用 Docker 版本和官方文档为准),生产环境调整前建议先在测试环境验证。

    小结一下:容器内 nproc 看到的核数是宿主机的视图,不代表可用额度;限流的直接证据是 cpu.stat 里的 nr_throttled 和 throttled_time;cfs_quota 管的是每周期总时间,cpuset 管的是能用哪几个核,两者要分别核对。下次再遇到「容器里进程跑不动」的情况,先按上面的路径读一遍 cgroup 文件,再决定是加配额、改绑核,还是去查 IO 和锁的问题。

    继续阅读

    📑 📅
    Nginx gzip 与 Brotli 压缩配置实战:静态资源体积优化 2026-09-19
    PHP-FPM 进程数怎么调:pm.max_children 与内存估算 2026-09-19
    Docker容器DNS解析异常排查:resolv.conf与自定义网络 2026-09-19
    MySQL连接被拒绝Connection refused逐层排查思路 2026-09-19
    systemd-journald 日志转发远程 syslog:rsyslog 对接与丢日志排查 2026-09-19
    Nginx 与后端长连接调优:keepalive 与 upstream 复用 2026-09-20
    Docker 容器日志写满磁盘:json-file 限制与 max-size 配置 2026-09-20
    Linux时间与时区排查:date、timedatectl与容器时区一致性 2026-09-20
    服务器网卡多队列与中断绑定入门:RPS、RSS 与 irqbalance 怎么取舍 2026-09-20
    Nginx 上传大文件报 413 与超时:参数与缓冲区排查 2026-09-20