Linux cgroup v2 与 systemd 资源治理实战

CPU、内存、磁盘 I/O 和进程数耗尽,是 Linux 生产事故中最常见的资源类问题。仅靠 nice、ulimit 或事后杀进程,无法提供稳定的资源隔离。cgroup v2 使用统一层级控制资源,而 systemd 则把服务、会话和容器组织到可持续管理的 cgroup 中。

本文从资源模型、systemd 配置、压力观测、容器映射和故障处置几个方面,给出一套可以用于服务器基线和服务治理的实践方法。

一、cgroup v2 解决什么问题

cgroup 可以把一组进程放入同一个资源控制组,并对这组进程应用统一策略。cgroup v2 相比 v1 的重要变化包括:

  • 所有控制器位于统一层级;
  • 进程只属于一棵 cgroup 树;
  • CPU、内存和 I/O 的控制语义更加一致;
  • 支持 memory.low、memory.high、PSI 等生产治理能力;
  • 更容易与 systemd、容器运行时和 Kubernetes 协同。

资源控制通常分为四类:

类型 作用 典型参数
权重 资源竞争时决定相对份额 CPUWeight、IOWeight
上限 防止单个服务无限占用 CPUQuota、MemoryMax
保护 压力下优先保障关键服务 MemoryLow、MemoryMin
记账 统计实际消耗 CPU、Memory、IO Accounting

权重只有在发生竞争时才生效;上限即使机器空闲也会限制服务。生产配置应优先使用权重和软阈值,谨慎设置硬上限。

二、确认系统是否使用 cgroup v2

查看文件系统类型:

stat -fc %T /sys/fs/cgroup

输出 cgroup2fs 表示统一的 cgroup v2。继续检查:

cat /sys/fs/cgroup/cgroup.controllers
cat /proc/cgroups
systemd --version

常见控制器包括 cpu、cpuset、io、memory、pids。控制器出现在 cgroup.controllers 中不代表已经在所有子树启用,systemd 会根据配置完成大部分委派和启用操作。

如果系统仍为混合模式或 cgroup v1,迁移前必须确认内核、systemd、容器运行时、监控代理和业务程序的兼容性。

三、理解 systemd 的层级

systemd 把工作负载组织为三类单元:

  • service:长期运行的系统服务;
  • scope:由外部创建的一组进程,例如登录会话或容器;
  • slice:用于组织 service 和 scope 的层级。

常见根 Slice:

  • system.slice:系统服务;
  • user.slice:用户会话;
  • machine.slice:虚拟机和容器。

查看层级:

systemd-cgls
systemd-cgtop
systemctl status system.slice
systemctl show nginx.service -p ControlGroup

可以为业务创建专用 Slice:

# /etc/systemd/system/business-critical.slice
[Unit]
Description=Critical business services

[Slice]
CPUWeight=800
MemoryLow=4G
IOWeight=800

服务再通过 Slice=business-critical.slice 加入。这样可以先限制一组业务,再在组内区分各服务优先级。

四、CPU 治理

1. CPUWeight:竞争时分配份额

CPUWeight 范围为 1 到 10000,默认 100。两个始终繁忙的服务若权重为 100 和 400,在同一 CPU 资源域竞争时,后者大约获得四倍份额。

[Service]
CPUAccounting=yes
CPUWeight=500

权重不是固定百分比,机器空闲时服务仍可使用更多 CPU。

2. CPUQuota:设置硬上限

[Service]
CPUQuota=150%
CPUQuotaPeriodSec=100ms

150% 表示最多使用约 1.5 个 CPU 核心的计算时间。对延迟敏感业务,过低配额会产生周期性限流,表现为整机 CPU 使用率不高但请求延迟明显升高。

3. CPUAffinity 与 AllowedCPUs

只有在 NUMA、实时任务或噪声隔离有明确需求时才绑定 CPU。普通 Web 服务过度绑核会降低调度弹性。

查看 CPU 限流:

systemctl show app.service -p CPUUsageNSec -p CPUQuotaPerSecUSec
CG=/sys/fs/cgroup/system.slice/app.service
cat "$CG/cpu.stat"

nr_throttled 和 throttled_usec 持续增长通常意味着配额过紧。

五、内存治理

cgroup v2 的内存控制不应只设置一个硬上限。

1. MemoryLow 与 MemoryMin

  • MemoryLow:内存回收时尽量保护的容量;
  • MemoryMin:更强的保护,应非常谨慎,配置过多可能导致系统无可回收内存。

2. MemoryHigh

超过 MemoryHigh 后,进程会受到回收和节流压力,但不会立即被杀死。它适合作为主要控制阈值,让服务在接近危险区时先出现可观测的性能下降。

3. MemoryMax

这是硬上限。服务超过上限且无法回收时,cgroup 内进程可能被 OOM 杀死。

4. MemorySwapMax

限制服务可使用的交换空间。完全禁止 Swap 并不总是最优,应根据延迟要求、匿名页比例和宿主机策略测试。

[Service]
MemoryAccounting=yes
MemoryLow=1G
MemoryHigh=3G
MemoryMax=4G
MemorySwapMax=1G

建议先通过监控获取峰值和工作集,再设置 MemoryHigh,最后决定是否需要 MemoryMax。

六、OOM、systemd-oomd 与内核 OOM

需要区分三个概念:

  1. 应用自身内存异常:堆、缓存或连接数持续增长;
  2. cgroup OOM:服务达到 memory.max;
  3. 系统级 OOM:整机内存耗尽,内核选择进程终止。

systemd-oomd 可根据 cgroup v2 的内存压力和 Swap 使用情况提前处理持续压力。它与内核 OOM 的触发条件不同。

[Service]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=80%

使用前确认发行版是否启用 systemd-oomd:

systemctl status systemd-oomd
oomctl
journalctl -u systemd-oomd

对数据库等重要服务不要直接套用激进阈值,应结合内存保护、优雅降级和副本切换策略。

七、磁盘 I/O 治理

1. IOWeight

多个服务竞争同一块设备时,用相对权重分配带宽:

[Service]
IOAccounting=yes
IOWeight=600

I/O 调度器和设备类型会影响权重效果。NVMe、虚拟磁盘和网络块存储必须通过压测确认。

2. 带宽与 IOPS 上限

[Service]
IOReadBandwidthMax=/dev/nvme0n1 200M
IOWriteBandwidthMax=/dev/nvme0n1 100M
IOReadIOPSMax=/dev/nvme0n1 20000
IOWriteIOPSMax=/dev/nvme0n1 10000

如果文件系统跨多块盘、LVM、RAID 或 Device Mapper,必须确认最终对应的块设备。限制错层可能不生效,或意外限制其他业务。

cat /sys/fs/cgroup/system.slice/app.service/io.stat
lsblk -o NAME,MAJ:MIN,TYPE,MOUNTPOINTS

八、进程数与文件描述符

TasksMax 限制线程和进程总数:

[Service]
TasksAccounting=yes
TasksMax=4096
LimitNOFILE=262144

Java、数据库、代理和高并发网络服务可能创建大量线程,阈值过低会出现 Resource temporarily unavailable。TasksMax 与 LimitNPROC 的语义不同,排障时应分别检查。

systemctl show app.service -p TasksCurrent -p TasksMax -p LimitNOFILE
cat /sys/fs/cgroup/system.slice/app.service/pids.current
cat /sys/fs/cgroup/system.slice/app.service/pids.events

九、完整服务配置示例

不要直接修改软件包自带的 Unit。使用 drop-in:

systemctl edit app.service

写入:

[Service]
Slice=business-critical.slice

CPUAccounting=yes
CPUWeight=600
CPUQuota=300%

MemoryAccounting=yes
MemoryLow=1G
MemoryHigh=6G
MemoryMax=8G
MemorySwapMax=1G

IOAccounting=yes
IOWeight=700

TasksAccounting=yes
TasksMax=8192
LimitNOFILE=262144

应用配置:

systemctl daemon-reload
systemctl restart app.service
systemctl show app.service \
  -p Slice -p CPUWeight -p CPUQuotaPerSecUSec \
  -p MemoryLow -p MemoryHigh -p MemoryMax \
  -p IOWeight -p TasksMax

部分属性可在线调整:

systemctl set-property --runtime app.service MemoryHigh=7G
systemctl set-property --runtime app.service CPUWeight=700

runtime 参数只在本次启动周期生效,适合应急验证;确认后再写入持久配置。

十、使用 PSI 观察资源压力

Pressure Stall Information(PSI)衡量任务因为 CPU、内存或 I/O 不可用而停顿的时间:

cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

示例:

some avg10=2.10 avg60=1.40 avg300=0.80 total=123456789
full avg10=0.30 avg60=0.20 avg300=0.10 total=1234567
  • some:至少一个任务因资源等待而停顿;
  • full:所有非空闲任务同时停顿,通常更严重;
  • avg10、avg60、avg300:最近 10、60、300 秒压力比例。

单看 CPU 使用率无法发现限流,单看剩余内存也无法判断回收抖动。PSI 应与延迟、吞吐、cgroup 事件和主机指标一起分析。部分 cgroup 目录也提供 cpu.pressure、memory.pressure 和 io.pressure,可定位到具体服务。

十一、关键监控项

CPU

  • cpu.stat 中 usage、user、system;
  • nr_periods、nr_throttled、throttled_usec;
  • PSI CPU;
  • 业务延迟和队列长度。

内存

  • memory.current、memory.peak;
  • memory.stat 中匿名页、文件缓存和工作集;
  • memory.events 中 high、max、oom、oom_kill;
  • memory.pressure;
  • Swap、页面回收和 major fault。

I/O

  • io.stat 每设备读写字节和次数;
  • I/O PSI;
  • 块设备延迟、队列深度和利用率;
  • 应用 fsync、数据库提交和日志刷盘延迟。

进程

  • pids.current 与 pids.max;
  • pids.events;
  • 文件描述符使用率;
  • 线程池和连接池队列。

十二、容器与 Kubernetes 的映射

容器并没有绕过 cgroup。容器运行时会为 Pod 和容器创建子 cgroup,Kubernetes 的 requests、limits 和 QoS 最终会映射为相应资源控制。

生产环境注意:

  • kubelet 与容器运行时应使用一致的 cgroup driver;
  • 使用 systemd 的系统优先选择 systemd driver;
  • 节点升级前验证 cgroup v2 和运行时版本;
  • Pod CPU limit 可能造成 CFS 限流;
  • 内存 limit 对应硬边界,超过可能 OOMKilled;
  • requests 影响调度和相对保障,不等于绝对预留;
  • 不要在宿主机上用冲突规则再次限制容器 Slice。

排查 Pod 时既要看 Kubernetes:

kubectl describe pod <pod> -n <namespace>
kubectl top pod <pod> -n <namespace> --containers

也要在节点查看对应 cgroup 的 memory.events、cpu.stat 和 PSI。仅看到 OOMKilled 不能判断是应用泄漏、Pod limit 过低还是节点整体压力。

十三、用户服务与委派

rootless 容器或用户级 systemd 需要 cgroup 委派。错误地给普通用户开放整棵层级会破坏隔离。

systemctl status user@$(id -u).service
systemctl --user status
loginctl show-user "$USER"

若服务需要自行创建子 cgroup,应使用 systemd 的 Delegate 配置,并只委派必要控制器。不要让应用直接修改不属于自己的父层级。

十四、从 cgroup v1 迁移到 v2

建议按以下顺序:

  1. 盘点依赖固定 cgroup 路径的脚本;
  2. 验证监控、EDR、安全代理和备份代理;
  3. 验证 Docker、containerd、CRI-O 与 kubelet;
  4. 确认 Java、数据库和中间件支持情况;
  5. 在同内核、同 systemd 的测试节点演练;
  6. 对资源策略建立 v1/v2 映射表;
  7. 金丝雀迁移少量节点;
  8. 比较 CPU 限流、内存回收和 I/O 延迟;
  9. 分批扩大并保留回退启动项。

迁移时不要照搬 v1 参数,特别是内存层级、软限制和 I/O 控制语义需要重新验证。

十五、常见故障排查

配置已写入但未生效

systemd-analyze verify /etc/systemd/system/app.service
systemctl cat app.service
systemctl show app.service -p ControlGroup
systemctl daemon-reload

确认参数放在正确的 Service 或 Slice 段,且没有被其他 drop-in 覆盖。

服务频繁被杀

journalctl -u app.service --since -1h
journalctl -k --since -1h | grep -i -E 'oom|killed process'
cat /sys/fs/cgroup/system.slice/app.service/memory.events

若 high 快速增长但没有 oom_kill,说明服务正在受回收压力;若 oom_kill 增长,则检查 MemoryMax 和应用工作集。

CPU 使用率不高但延迟升高

检查 cpu.stat 的限流时间、线程队列、CPU PSI 和 CPUQuota。总机 CPU 空闲不代表该服务没有达到自身配额。

I/O 限制不生效

确认实际块设备、I/O 调度器、Device Mapper 层级和 systemd 版本。网络文件系统不一定受本地块 I/O 控制器约束。

无法创建线程

同时检查 TasksMax、pids.max、LimitNPROC、地址空间、线程栈和应用线程池配置。

十六、安全上线与回滚

资源治理应按照“观测、软约束、硬上限”的顺序推进:

  1. 先启用 Accounting,记录至少一个业务周期;
  2. 建立工作集、峰值和 PSI 基线;
  3. 设置 CPU、I/O 权重和 MemoryHigh;
  4. 观察性能和压力事件;
  5. 再设置 MemoryMax、CPUQuota 和 TasksMax;
  6. 先覆盖单个副本或非核心节点;
  7. 准备运行时放宽参数的应急命令;
  8. 将最终参数持久化并纳入配置管理。

紧急回滚示例:

systemctl set-property --runtime app.service MemoryMax=infinity
systemctl set-property --runtime app.service MemoryHigh=infinity
systemctl set-property --runtime app.service CPUQuota=infinity

回滚后仍应分析根因,避免无限制服务再次拖垮整机。

十七、生产检查清单

基线

  • [ ] 已确认系统运行 cgroup v2;
  • [ ] systemd、内核和容器运行时兼容;
  • [ ] 服务按业务重要性归入合理 Slice;
  • [ ] 资源参数由配置管理维护。

CPU 与内存

  • [ ] 优先使用权重,硬配额经过压测;
  • [ ] MemoryHigh 低于 MemoryMax,并留有系统余量;
  • [ ] 已监控 memory.events 和 CPU 限流;
  • [ ] OOM 处置区分 cgroup OOM 与系统 OOM。

I/O 与进程

  • [ ] I/O 规则对应正确块设备;
  • [ ] 数据库、日志和备份任务已验证相互影响;
  • [ ] TasksMax、NOFILE 与应用并发模型匹配;
  • [ ] PSI 纳入主机和关键服务告警。

容器

  • [ ] kubelet 与运行时使用一致的 cgroup driver;
  • [ ] requests、limits 有容量依据;
  • [ ] 宿主机规则不会与容器编排冲突;
  • [ ] 节点升级前执行压力和回归测试。

参考资料

cgroup v2 不是单纯的限速工具,而是一套把资源优先级、保护、上限和可观测性统一起来的治理机制。最有效的实践不是给所有服务套同一个硬限制,而是先理解业务工作集,再用 systemd 把策略稳定、可审计地落实到每个服务和 Slice。

Copyright © yan-jian.com 2023 - 2026 all right reserved,powered by Gitbook更新时间: 2026-10-22 21:20:02

results matching ""

    No results matching ""