Linux内核cgroup v2深度实战:统一层级架构、资源隔离机制与容器运行时工程实践
容器技术从诞生之初就依赖cgroup实现资源隔离,但cgroup v1的设计缺陷让运维人员饱受层级混乱、控制器协调困难的困扰。Linux 4.5正式引入的cgroup v2从根本上解决了这些痛点,如今已成为Fedora、Ubuntu 24.04+、Kubernetes 1.25+的默认配置。本文从内核源码级解析cgroup v2的统一层级设计、内存/CPU/IO控制器的实现机制、压力积压通知机制(PSI),以及在containerd、systemd中的工程实践。
一、为什么要从v1迁移到v2
1.1 cgroup v1的四大顽疾
cgroup v1采用多子系统独立层级的设计——每个控制器(cpu、memory、blkio等)各自维护一棵独立的cgroup树。这导致三个核心问题:
层级分裂:当containerd需要为容器同时设置内存限制和CPU权重时,容器进程可能被放置在不同层级节点的cgroup中,造成语义上的不一致。例如,memory控制器的/system.slice/containerd.service/xxx.scope与cpu控制器的/runtime/docker/xxx可能指向同一个容器但层级路径完全不同。
控制器争抢优先级:v1中memory、cpu、blkio各自遍历cgroup树时产生不同的"首选节点",内核必须在多个层级间协调进程的归属,增加了OOM killer触发的不确定性。
接口不一致:memory.limit_in_bytes、cpu.shares、blkio.weight各自使用完全不同的单位、粒度和语义,用户态框架难以统一抽象。
v2如何解决:cgroup v2引入了统一层级树(unified hierarchy)——所有单一控制器的cgroup附着在同一棵树上,且进程只能出现在叶节点。写入cgroup.procs一次迁移,所有控制器同时生效。
1.2 v2的核心设计原则
┌──────────────────────────────────────────────────────┐
│ cgroup v2 设计哲学 │
├──────────────────────────────────────────────────────┤
│ 1. 统一层级:所有控制器共享一棵cgroup树 │
│ 2. 进程在叶节点:非空中间节点不能再附加进程 │
│ 3. 内部进程无意义:内核建议不在非叶节点上放进程 │
│ 4. 先控制再记录:控制优先于统计 │
│ 5. 所有权模型:文件所有者自动获得子cgroup的配置权限 │
└──────────────────────────────────────────────────────┘
二、统一层级架构源码解析
2.1 核心数据结构
cgroup v2在内核中的核心结构是cgroup(include/linux/cgroup-defs.h):
struct cgroup {
struct cgroup_subsys_state self; // 通用子系统的css
unsigned long flags; // 状态标志
struct cgroup_id id; // 全局唯一ID
int level; // 在树中的深度
int max_descendants; // 子节点数上限
int max_depth; // 子树深度上限
struct cgroup *parent; // 父节点
struct list_head sibling; // 兄弟链表
struct list_head children; // 子节点链表
struct kernfs_node *kn; // sysfs/dentry inode关联
struct cgroup_root *root; // 所属的cgroup_root
struct cgroup_subsys_state **subsys; // 各控制器的CSS数组
atomic_t nr_populated_cnt; // 已填充控制器计数
struct cgroup_file file; // cgroup.events等文件
struct work_struct release_work; // 释放工作项
};
2.2 控制器的统一注册
v2中,控制器的启用通过cgrp_dfl_root.cgrp.subsys[]数组统一管理。每个控制器在初始化时通过cgroup_subsys注册自己的回调:
struct cgroup_subsys {
struct cgroup_subsys_state *(*css_alloc)(struct cgroup_subsys_state *parent);
int (*css_online)(struct cgroup_subsys_state *css);
void (*css_offline)(struct cgroup_subsys_state *css);
void (*css_free)(struct cgroup_subsys_state *css);
void (*css_rstat_flush)(struct cgroup_subsys_state *css);
int (*css_show)(struct seq_file *seq, struct cgroup_subsys_state *css);
int (*can_attach)(struct cgrp_css *css, struct cgroup_taskset *tset);
void (*attach)(struct cgrp_css *css, struct cgroup_taskset *tset);
void (*cancel_attach)(struct cgrp_css *css, struct cgroup_taskset *tset);
bool early_init:1;
bool broken_hierarchy:1;
bool threaded:1;
int id;
const char *name;
const char *legacy_name; // 用于v1兼容
};
2.3 线程模式(threaded mode)
v2允许cgroup进入"线程模式",意味着一个cgroup可以将线程分布在其不同子cgroup中。这是对v2"进程只能在叶节点"约束的有用补充:
cgroup v2 线程模式示意:
/sys/fs/cgroup/tenant-a/
├── cgroup.type = domain ← 根为 domain
├── service-a/
│ ├── cgroup.type = threaded ← 标记为线程组
│ ├── thread-1/cgroup.type = thread (isolated)
│ ├── thread-2/cgroup.type = thread (domain)
│ └── thread-3/cgroup.type = thread (domain)
通过cgroup.type文件在domain、threaded、thread之间切换:
# 查看当前类型
cat /sys/fs/cgroup/system.slice/cgroup.type
# domain
# 将service-a标记为线程组
echo "threaded" > /sys/fs/cgroup/system.slice/service-a/cgroup.type
三、内存控制器:从限制到安全
3.1 内存限制的核心机制
cgroup v2的内存控制器(mm/memcontrol-v2.c,历史上与v1共用大部分逻辑)通过mem_cgroup追踪每个内存页的归属:
struct mem_cgroup {
struct cgroup_subsys_state css;
struct page_counter memory; // current memory usage (bytes)
struct page_counter swap; // current swap usage
struct page_counter memsw; // memory + swap
struct page_counter kmem; // kernel memory
struct page_counter tcpmem; // tcp socket buffer memory
struct page_counter zswap; // zswap compressed size
unsigned long soft_limit; // 软限制(回收优先级)
unsigned long min; // 硬性下限保护
unsigned long high; // 高压阈值(触发限流)
unsigned long max; // 硬性上限
unsigned long oom_group; // OOM group
int oom_kill_disable; // 禁用OOM kill
struct mem_cgroup_thresholds thresholds;
struct memcg_vmstats_percpu __percpu *vmstats_percpu;
struct memcg_vmstats *vmstats;
struct lruvec // LRU 链表(活跃/不活跃)
// 其他字段...
struct deferred_work refcnt_dw; // 延迟引用计数归零工作
};
3.2 内存限制的工作流程
用户态写入 memory.max = 8G
↓
cgroup_memory_write() [kernel/cgroup/cgroup.c]
↓
page_counter_set_max(&memcg->memory, nr_pages)
↓
memory计数器持续追踪脏页分配
↓
触发回收? → 是 → mem_cgroup_oom() → OOM kill 当前cgroup
↓
allocation失败但不在硬限制 → 触发kswapd/直接回收
关键概念memory.high:v2引入的"内存高压阈值"。当使用量超过memory.high时,内核会主动对该cgroup执行回收,而非等待全局内存不足。这是延迟敏感型服务的重要配置:
# 对延迟敏感的服务
mkdir /sys/fs/cgroup/latency-critical
echo "8G" > /sys/fs/cgroup/latency-critical/memory.max # 硬限制
echo "6G" > /sys/fs/cgroup/latency-critical/memory.high # 软限制,主动回收
echo "4G" > /sys/fs/cgroup/latency-critical/memory.min # 硬保证,OOM不会触发在此cgroup
echo "3G" > /sys/fs/cgroup/latency-critical/memory.low # 尽力而为的底部保护
3.3 OOM事件通知
v2中通过inotify监控cgroup.events文件来实现OOM通知:
# cgroup.events 内容示例:
populated 1 ← cgroup中有进程
frozen 0 ← cgroup未被冻结
oom_kill 2 ← 触发了2次OOM kill
四、CPU控制器:权重与带宽控制
4.1 CPU权重与份额(cpu.weight)
cgroup v2使用cpu.weight(默认100,范围1-10000)作为细分资源分配权重,取代了v1的cpu.shares。其语义完全兼容CFS带宽控制:
// kernel/sched/core.c中的CFS带宽控制
struct cfs_bandwidth {
ktime_t period; // 控制周期(默认100ms)
u64 quota; // 周期内配额(微秒)
u64 runtime; // 剩余quota
s64 hierarchical_quota; // 层级传递的quota
struct hrtimer period_timer; // 周期定时器
struct list_head throttled_cfs_rq; // 被限流的运行队列
int nr_periods; // 统计已过去周期
int nr_throttled; // 被限流次数
u64 throttled_time; // 被限流总时间
};
4.2 CPU带宽控制实战
# 限制某服务每100ms最多使用20ms CPU时间(相当于0.2个核)
echo "100000" > /sys/fs/cgroup/my-service/cpu.period
echo "20000" > /sys/fs/cgroup/my-service/cpu.quota
# 使用权重分配(默认period=100ms,weight=100表示相对份额)
mkdir /sys/fs/cgroup/services/frontend
mkdir /sys/fs/cgroup/services/backend
echo "500" > /sys/fs/cgroup/services/frontend/cpu.weight # 500/600≈83%
echo "100" > /sys/fs/cgroup/services/backend/cpu.weight # 100/600≈17%
4.3 CPU压力积压(PSI)
PSI(Pressure Stall Information)是Linux 4.20引入的子系统,通过追踪任务因CPU、内存或IO资源不足而等待运行的时间比例来量化"压力积压"。
# 查看CPU压力
cat /sys/fs/cgroup/my-service/cpu.pressure
some avg10=15.23 avg60=8.45 avg300=3.12 total=984325623
# 查看内存阻塞压力
cat /sys/fs/cgroup/my-service/memory.pressure
some avg10=5.3 avg60=2.1 total=623456
full avg10=1.8 avg60=0.9 total=323456
# 查看IO压力
cat /sys/fs/cgroup/my-service/io.pressure
PSI关键字段含义:
| 字段 | 含义 |
|---|---|
| avg10 | 最近10秒阻塞时间占比(%) |
| avg60 | 最近60秒阻塞时间占比(%) |
| avg300 | 最近300秒阻塞时间占比(%) |
| total | 累计阻塞时间(微秒) |
| some | 至少有一个任务在等待的总时间 |
| full | 所有同时在运行任务都在等待的时间 |
源码级PSI追踪机制(kernel/sched/psi.c):
struct psi_group {
struct psi_group_cpu __percpu *pcpu; // 每CPU结构
u64 avg[2][3]; // [some/full][10/60/300]
u64 total[2][NR_PSI_STATES]; // 累计值
u64 avg_total[2]; // 平均值分母
struct delayed_work clock_work; // 周期性采样任务
struct mutex seq_lock;
struct psi_states state;
u32 nr_tasks[2]; // 追踪的任务数
u64 some_tasks_delayed_since; // 开始有时间阻塞的时间点
u64 full_tasks_delayed_since;
};
void psi_task_change(struct task_struct *task, int clear, int set)
{
// 当任务阻塞/唤醒时更新psi_group的状态追踪
int cpu = task_cpu(task);
struct psi_group *group;
// 遍历进程所属的cgroup层次结构
for (group = task_psi_group(task, cpu); group; group = group->parent) {
// 更新各层级psi_group的统计
u32 state = group_flags_to_psi_state(clear, set);
if (state != PSI_NOTHING) {
psi_group_update(group, cpu, state);
}
}
}
五、IO与PID控制器
5.1 IO权重控制(io.weight)
cgroup v2的IO控制通过io.max、io.weight、io.latency实现,由blk-iocost和blk-throttle两个机制支撑:
# 限制读写带宽:riops/wiops/bwbps/wbps
echo "8:0 rbps=104857600 wbps=104857600" > /sys/fs/cgroup/db-tier/io.max
# 限制为100MB/s读写,设备号8:0(sda)
# 设置IO权重
echo "500" > /sys/fs/cgroup/web-tier/io.weight
echo "100" > /sys/fs/cgroup/batch-tier/io.weight
# 设置IO延迟目标(针对延迟敏感场景)
echo "8:0 target=100" > /sys/fs/cgroup/latency-tier/io.latency
# 目标延迟100μs
5.2 PID控制器(pids.max)
防止fork炸弹和服务失控:
echo "1024" > /sys/fs/cgroup/web-service/pids.max
echo "unlimited" > /sys/fs/cgroup/web-service/pids.current # 只读,查询当前数量
六、容器运行时中的实战
6.1 Containerd的cgroup v2驱动
Containerd通过runc执行容器,cgroup配置在config.json的linux.resources段:
// 解析Kubernetes QoS到cgroup v2配置
type LinuxResources2 struct {
CPU *LinuxCPU // shares → weight, quota/period → cpu.max
Memory *LinuxMemory // limit → memory.max, request → memory.min
IO *LinuxBlockIO // throttle → io.max
Pids *LinuxPids // limit → pids.max
}
// config.json片段示例
"linux": {
"resources": {
"memory": {
"limit": 8589934592, // 8G → memory.max
"reservation": 4294967296 // 4G → memory.min
},
"cpu": {
"shares": 512, // 转换为 weight = (shares-2)*9999/262142+1 ≈ 200
"quota": 200000, // 20ms/100ms
"period": 100000
},
"blockIO": {
"throttleReadBpsDevice": [
{"major":8, "minor":0, "rate":104857600} // sda 100MB/s
]
},
}
}
Containerd启动脚本中启用cgroup v2驱动:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
SystemdCgroup = true # 使用systemd驱动管理cgroup
# 验证挂载点
# mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
6.2 systemd与cgroup v2的深度融合
systemd在v2下管理服务的cgroup树:
# 查看cgroup树
systemd-cgls
├─user.slice
│ ├─user-1000.slice
│ │ └─session-3.scope
├─system.slice
│ ├─containerd.service
│ │ ├─docker-<container_id>.scope ← 容器在这里
│ │ └─...
│ ├─nginx.service
│ └─postgresql.service
# 查看服务使用的资源
systemctl status nginx
● nginx.service - A high performance web server
Tasks: 18 (limit: 9439)
Memory: 12.4M (limit: 800.0M, anon: 8.2M, ...)
CPU: 1.234s
CGroup: /system.slice/nginx.service
├─ 123 /usr/sbin/nginx -g ...
└─ 456 nginx: worker process
通过systemctl set-property动态调整服务cgroup:
# 运行时修改容器服务的CPU限制
docker run -d --name nginx-frontend --cpus="0.5" \
--memory="8g" \
--memory-reservation="4g" \
--pids-limit=4096 \
-p 80:80 nginx:latest
# 查看映射关系(containerd驱动下)
systemctl cat docker-<container_id>.scope
# /run/systemd/transient/docker-<container_id>.scope.d/50-CPUQuotaPerSecUSec.conf
# [Scope]
# CPUAccounting=yes
# CPUQuotaPerSecUSec=500ms # --cpus=0.5 自动映射
# MemoryHigh=8.0G
# MemoryMax=8.0G
# MemoryMin=4.0G
# TasksMax=4096
七、生产级故障排查
7.1 OOM排查全链路
# 1. 找到触发OOM的cgroup
dmesg | grep -i oom | tail -5
# [123456.789] oom-kill: KILLED process 12345 (java) group "/system.slice/postgres.service"
# 2. 查看该cgroup的OOM统计
cat /run/systemd/system/postgres.service.d/cgroup.events
# oom_kill 2
# 3. 查看memory.stat定位root cause
cat /sys/fs/cgroup/system.slice/postgres.service/memory.stat
# anon 7248814080 ← 匿名内存占7.2G
# file 524288000 ← 文件缓存500MB
# kernel_stack 123456
# slab 98765432
# pagetables 12345678
# shmem 456789
# ...
# 4. 检查压力的来源(memory.pressure)
cat /sys/fs/cgroup/system.slice/postgres.service/memory.pressure
# some avg10=45.6 avg60=22.3 total=234567890
# 表明最近10秒内45.6%时间有任务在等待内存
7.2 高CPU占用定位
# 使用perf+cgroup联合分析
perf c2c record -c cgroup:/sys/fs/cgroup/system.slice/heavy-service \
-a sleep 10
# 或查看cpu.stat判断是被主动限速还是真实算力不足
cat /sys/fs/cgroup/system.slice/heavy-service/cpu.stat
# usage_usec 9823456789
# user_usec 6723456789
# system_usec 3100000000
# core_sched_forceidle_usec 0
# nr_periods 98 # 过去被分成了98个周期
# nr_throttled 15 # 15次被限流
# throttled_usec 1234567 # 累计被限流1234毫秒
7.3 IO延迟异常排查
# 查看各设备的IO压力
for dev in /sys/fs/cgroup/*/io.pressure; do
echo "$dev: $(cat $dev)"
done
# 查看blk-iocost统计
cat /sys/fs/cgroup/system.slice/mysql.service/io.stat
# 8:0 rbytes=1234567 rios=1234 wbytes=9876543 wios=4567 dbytes=0 dios=0
八、与Kubernetes的集成演进
8.1 Kubernetes cgroup v2支持
Kubernetes 1.25+正式支持cgroup v2,通过kubelet的--cgroup-driver和--cgroup-root配置:
# kubelet cgroup v2配置
cgroupDriver: systemd
cgroupRoot: /system.slice
# Pod QoS映射到cgroup v2
# Guaranteed: limits == requests → cpu.weight分配最大, memory.min=request
# Burstable: requests < limits → cpu.weight按request分配
# BestEGuaranteed: → cpu.weight最小, memory无保护
8.2 cgroup v2对Kubernetes稳定性价值的案例
某大型互联网公司将所有生产节点升级到cgroup v2后(Linux 6.1内核,containerd 2.0):
| 指标 | cgroup v1 | cgroup v2 | 改善 |
|---|---|---|---|
| OOM误杀事件 | 月均143次 | 月均7次 | 降低95% |
| CPU限流抖动(帧时间P99) | ±35ms | ±8ms | 改善77% |
| 节点调度延迟(P50) | 23ms | 12ms | 改善48% |
| 内存回收性能(latency) | 周期性毛刺 | 平滑 | 消除P99延迟尖峰 |
改善原因:v2的统一层级消除了控制器间的协调延迟,memory.high让回收发生在内存尚有余量时已主动开始而非被动OOM,PSI驱动的分级驱逐策略替代了粗暴的cgroup迁移。
九、最佳实践总结
┌─────────────────────────────────────────────────────────────────┐
│ cgroup v2 生产环境最佳实践 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 启用层级统一:确保使用unified cgroup挂载 │
│ ● mount -t cgroup2 none /sys/fs/cgroup │
│ │
│ 2. 使用systemd驱动管理容器: │
│ ● 容器时延一致性更好 │
│ ● 避免孤儿cgroup堆积 │
│ │
│ 3. 配置memory.high做压力提前释放: │
│ ● memory.high = 0.8 * memory.max │
│ │
│ 4. 部署PSI监控告警: │
│ ● memory.pressure "some" > 30% (持续30s) → 扩容预警 │
│ ● cpu.pressure "some" > 20% (持续1分钟) → 容量不足 │
│ │
│ 5. 为关键服务设置memory.min保护: │
│ ● 防止低优先级邻居过度挤占 │
│ │
│ 6. 启用io.latency而非仅依赖io.max: │
│ ● 同时防止IO延迟失控 │
│ │
│ 7. 限制递归子cgroup深度: │
│ ● default 8-12层足够 │
│ │
│ 8. 监控cgroup.events populated计数: │
│ ● 异常归零意味着服务崩溃 │
│ │
└─────────────────────────────────────────────────────────────────┘
十、总结
cgroup v2不再是"cgroup v1的小幅升级"——它重新定义了Linux资源隔离的语义基础。从统一层级消除多控制器的协调成本,到memory.high的提前回收避免PSI毛刺,再到threaded模式对容器混合编排的支持,v2解决了真实生产环境中历时十年的痛点。
对于正在进行基础设施现代化的团队,迁移到cgroup v2的最后障碍已不复存在:containerd、Kubernetes、Docker、systemd全部原生支持,Linux 6.x的controllers已完全稳定。唯一需要的,就是理解本文中的这些工程细节,并在合适的层级设置正确的参数。
容器编排的下一个战场不在Kubernetes API层——它藏在cgroup树的每个参数节点之中。

发表评论 取消回复