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树的每个参数节点之中。


点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.394154s