Linux 系统运维:用 cgroups v2 定位 CPU 限流与内存压力
服务器整体 CPU 使用率不高,某个服务却出现延迟尖峰;主机还有可用内存,应用却被 OOM 终止。这两种现象都值得从服务所属的 cgroup 入手检查。本文给出一条“确认环境 → 找到服务 → 读取计数器 → 对照限制 → 小范围调整 → 回滚验证”的排障路线。
本文面向使用 systemd 和统一 cgroups v2 层级的 Linux 服务器。命令中的 demo-worker.service 是演示单元,不代表任何真实业务。所有资源数值仅用于说明配置方式,不是生产推荐值;本文不提供未经实测的性能提升数据。生产变更应经过授权、容量评估与维护窗口审批。
一、资源隔离不等于资源充足
cgroup 将进程组织成层级树,并在层级内分配和约束资源。子层级不能突破祖先施加的资源约束,因此“主机有空闲”并不意味着“当前服务还能使用这些资源”。cgroups v2 使用统一层级,进程在 /proc/PID/cgroup 中的 v2 记录采用 0::路径 的形式。[1]
在 systemd 管理的机器上,优先通过 unit 的资源控制属性操作,而不是直接修改 systemd 管理的 /sys/fs/cgroup 文件。systemd 会根据单元的资源配置启用相关控制器,并处理父层级关系。[2]
先执行以下只读检查,确认实际环境;不要根据发行版名称推断启用了哪个版本:
stat -fc %T /sys/fs/cgroup
findmnt -t cgroup2
systemctl --version
如果该挂载点显示 cgroup2fs,可继续下面的统一层级排查;如果挂载布局不同,先定位实际的 cgroup2 挂载点。容器内的挂载可能是只读的,也可能经过 cgroup namespace 映射;本文后续路径示例应在宿主机执行,不应直接套用到容器内部。
二、先找到服务所在的 cgroup
unit=demo-worker.service
systemctl show "$unit" \
-p ActiveState -p MainPID -p ControlGroup \
-p CPUQuotaPerSecUSec -p CPUWeight \
-p MemoryCurrent -p MemoryHigh -p MemoryMax -p TasksMax
ControlGroup 给出的路径是后续读指标的入口。单看主进程的 RSS 或 top 排名容易遗漏子进程、线程和组内其他资源消耗;同时,MainPID 为 0 或服务已经退出时,应先处理生命周期问题,而不是继续套用运行中服务的诊断步骤。
下面的脚本只读取指定服务的 cgroup 指标,不修改资源限制。它假设宿主机的统一层级挂载在 /sys/fs/cgroup;文件缺失时跳过,避免把“不支持或未启用”误报成“数值为零”。
#!/usr/bin/env bash
set -eu
unit=${1:?用法: bash cgroup-snapshot.sh demo-worker.service}
cg=$(systemctl show "$unit" -p ControlGroup --value)
if [[ -z "$cg" || "$cg" != /* || "$cg" == / ]]; then
printf '未找到有效的服务 cgroup 路径\n' >&2
exit 1
fi
base="/sys/fs/cgroup${cg}"
if [[ ! -d "$base" ]]; then
printf 'cgroup 路径不存在,请检查服务状态和挂载布局\n' >&2
exit 1
fi
printf '时间: %s\n服务: %s\ncgroup: %s\n' \
"$(date -Is)" "$unit" "$cg"
for f in cpu.max cpu.stat cpu.pressure \
memory.current memory.high memory.max \
memory.events memory.events.local memory.pressure \
pids.current pids.max pids.events io.stat io.pressure; do
if [[ -r "$base/$f" ]]; then
printf '\n--- %s ---\n' "$f"
cat -- "$base/$f"
fi
done
间隔一段固定时间采集两份快照,并与同一时间窗口的请求延迟、错误率、吞吐量关联。累计计数器应看增量;历史发生过一次限流,并不能证明当前延迟就是限流造成的。如果服务重启或 cgroup 被重建,需要重新建立基线。
三、CPU:区分权重竞争与配额限流
CPUWeight 控制同一 slice 内单元之间的相对 CPU 时间权重,不是硬上限。CPUQuota 则设置相对于单个 CPU 的 CPU 时间上限;例如 CPUQuota=50% 表示最多获得一个 CPU 一半的 CPU 时间,而不是整台多核服务器算力的一半。[2]
对应的内核接口 cpu.max 有配额和周期两个字段,max 表示没有该层级的配额限制。cpu.stat 中的 nr_throttled 和 throttled_usec 可用于观察公平调度任务的限流情况;是否出现这些字段也取决于控制器是否启用。[1]
建议按以下顺序判断:
- 对照 cpu.max 与 systemd 的 CPUQuota,确认限制是否符合部署预期。
- 对比两份 cpu.stat 中 nr_throttled、throttled_usec 的增量,并检查延迟恶化是否发生在同一时间段。
- 检查父 slice 的限制:叶子单元没有配额,不代表祖先没有配额。[1]
- 如果没有明显配额限流,再结合 CPU pressure、宿主机负载、线程阻塞和应用剖析继续排查,不要只凭 top 的单个数字下结论。
不要把 nr_throttled 除以请求数当作“受限请求比例”。调度周期和业务请求不是同一种统计对象;throttled_usec 也不能直接等同于单次请求增加的延迟。
四、内存:先辨认 high,再判断 max
memory.high 是压力控制边界,超过后会触发节流和直接回收;越过 high 本身不会调用 OOM killer。memory.max 是硬限制,在回收仍无法满足分配需求时可能触发 cgroup 内 OOM。systemd 将二者分别映射为 MemoryHigh 和 MemoryMax,并建议以前者作为主要控制机制、后者作为最后防线。[1][2]
检查 memory.events 时,high 增量可帮助识别高边界压力,oom_kill 增量表示发生了 OOM 杀进程事件;memory.events 默认包含层级事件,memory.events.local 更适合区分当前层级与子层级来源,具体行为还应留意挂载选项。[1]
可形成以下排障假设,但必须继续交叉验证:
- high 持续增长、memory.pressure 同期升高、延迟恶化:重点检查工作集、回收压力和 MemoryHigh 是否过紧。
- oom_kill 增长、服务出现异常重启:对照内核日志、服务日志和 MemoryMax,确认被杀进程与发生时间。
- 主机空闲内存较多,但服务触碰硬限制:先核对该服务和祖先 cgroup 的限制,而不是直接扩容整台机器。[1]
memory.current 不是主进程 RSS 的同义词;memory.stat 可进一步帮助区分匿名内存、文件缓存等组成。[1] 对于有内存保护、swap 或多级层级的环境,也应同时核对相关策略,不能仅靠提高 MemoryMax 掩盖内存泄漏。
五、别忽略任务数和 I/O 压力
pids 控制器限制的是任务数量,线程也会参与计数;达到 pids.max 后,新建进程或线程可能失败。systemd 的 TasksMax 对应该限制,所以线程池扩张失败不一定是系统总内存不足。[1][2]
I/O 排障可以从 io.stat 与 io.pressure 入手,再结合设备级延迟和应用读写模式确认瓶颈。I/O 权重和限速的效果还取决于设备、调度器以及 I/O 路径,不应在未确认支持条件时承诺固定收益。[1][2]
六、用短生命周期单元验证配置链路
以下演示仅创建一个运行 sleep 的临时单元,用于验证“systemd 属性 → cgroup 接口”的映射,不会制造 CPU 或内存压力,因此也不能证明限流性能或 OOM 行为:
sudo systemd-run --unit=demo-worker --collect \
-p CPUQuota=50% \
-p MemoryHigh=128M \
-p MemoryMax=256M \
-p TasksMax=32 \
/usr/bin/sleep 300
systemctl show demo-worker.service \
-p ControlGroup -p MemoryHigh -p MemoryMax -p TasksMax
bash cgroup-snapshot.sh demo-worker.service
sudo systemctl stop demo-worker.service
运行前确认该单元名未被使用、sleep 路径存在,且当前用户具有相应授权。已有同名单元时应换用一个未占用的演示名称;不要停止真实业务单元。--collect 使该临时单元在结束且满足卸载条件后被回收。不同发行版的 systemd 版本可能对部分选项支持不同,应以本机手册为准。
七、生产调整需要有可执行的回滚
真实服务的修改应记录原始有效值、配置来源和父层级限制。临时试验可在批准范围内使用 systemctl set-property --runtime,但运行时变更也会立即影响服务,不能把“临时”理解为“没有风险”。
持久化配置示例可放入专用 drop-in 文件,例如 demo-worker.service.d/50-resource-budget.conf:
[Service]
CPUWeight=100
CPUQuota=100%
MemoryHigh=512M
MemoryMax=768M
TasksMax=256
这些数值只是格式示例。[2] 是否需要重启、何时 daemon-reload,以及如何生效,应根据本机 systemd 版本和服务变更规范确认。先在测试环境验证,再灰度到小范围实例,观察延迟、错误率、限流增量、OOM 事件和主机余量。
回滚时只撤销本次新增或修改的专用配置,并恢复已记录的原始值;不要使用会清除其他业务定制项的“一键恢复”。回滚后再次读取 unit 属性与 cgroup 文件,并重复相同监控窗口的验证。
结语
cgroups v2 排障的关键不是把限制一律调大,而是把主机、父层级、服务和应用指标放到同一时间窗口中。先读接口、再验证假设;先小范围调整、再核对效果;始终保留原始配置与回滚方案。这样才能区分资源竞争、配额节流、内存回收、任务数限制和真正的容量不足。
Sources
[1] https://docs.kernel.org/admin-guide/cgroup-v2.html — Linux Kernel Control Group v2 [2] https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html — systemd.resource-control
- 点赞
- 收藏
- 关注作者
评论(0)