Linux 系统运维:用 cgroups v2 定位 CPU 限流与内存压力

举报
yd_249802650 发表于 2026/10/30 20:16:53 2026/10/30
【摘要】 从 cgroups v2 服务层级定位 CPU 限流、内存压力、任务数与 I/O 瓶颈,提供只读快照脚本和 systemd 演示配置,并说明安全变更与回滚要点。

服务器整体 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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。