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
需要区分三个概念:
- 应用自身内存异常:堆、缓存或连接数持续增长;
- cgroup OOM:服务达到 memory.max;
- 系统级 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
建议按以下顺序:
- 盘点依赖固定 cgroup 路径的脚本;
- 验证监控、EDR、安全代理和备份代理;
- 验证 Docker、containerd、CRI-O 与 kubelet;
- 确认 Java、数据库和中间件支持情况;
- 在同内核、同 systemd 的测试节点演练;
- 对资源策略建立 v1/v2 映射表;
- 金丝雀迁移少量节点;
- 比较 CPU 限流、内存回收和 I/O 延迟;
- 分批扩大并保留回退启动项。
迁移时不要照搬 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、地址空间、线程栈和应用线程池配置。
十六、安全上线与回滚
资源治理应按照“观测、软约束、硬上限”的顺序推进:
- 先启用 Accounting,记录至少一个业务周期;
- 建立工作集、峰值和 PSI 基线;
- 设置 CPU、I/O 权重和 MemoryHigh;
- 观察性能和压力事件;
- 再设置 MemoryMax、CPUQuota 和 TasksMax;
- 先覆盖单个副本或非核心节点;
- 准备运行时放宽参数的应急命令;
- 将最终参数持久化并纳入配置管理。
紧急回滚示例:
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 有容量依据;
- [ ] 宿主机规则不会与容器编排冲突;
- [ ] 节点升级前执行压力和回归测试。
参考资料
- Linux 内核 cgroup v2 官方文档
- systemd.resource-control 手册
- systemd-oomd 手册
- Linux PSI 官方文档
- Kubernetes cgroup v2 文档
cgroup v2 不是单纯的限速工具,而是一套把资源优先级、保护、上限和可观测性统一起来的治理机制。最有效的实践不是给所有服务套同一个硬限制,而是先理解业务工作集,再用 systemd 把策略稳定、可审计地落实到每个服务和 Slice。