想象你住进了一栋老式合租公寓,房东把一层楼隔成了六个房间。厨房只有一个,电表水表各一块,沙发和阳台是公用的。一开始大家相安无事,可日子久了矛盾就来了:有人半夜开空调、有人连烧三壶水、有人把游戏机插在客厅。房东只好定规矩——每家每月几度电、几吨水,超了自己想办法。可公寓里的设备五花八门,电和水分开记,煤气又是另一套账本,房东自己都理不清。 这栋公寓就是一台 Linux 机器,住在里面的租客是进程,房东就是内核里专门管资源的管家——cgroups。它给一群进程(一大家子租客)划定“公共资源额度”:CPU 用多少、内存住多大、磁盘读写多快。后来管家发现账本太多太乱,于是把 v1 的“多套账本”改成了 v2 的“一本总账”。再加上 Namespace 负责给每家发“独立钥匙和窗帘”,进程彼此看不见对方房间,容器隔离就像每户有了自己的一套小公寓。这套机制是 Kubernetes、Docker 能安全运行的基石,也是云上资源不打架的秘密。

岗位关联

理解 cgroups 与 Namespace 的配合逻辑,是设计稳定容器方案、排查资源争抢问题的底层基本功。想看懂监控指标、处理 OOM,甚至优化混部集群,先读懂这套“合租公寓管理手册”。

目录
  1. 可视化演示
  2. 核心概念:进程组的资源账本
  3. 资源控制器:分配资源的多块仪表
  4. v1 多层级:各自为政的多棵挂载树
  5. v2 统一层级:一本总账的家族治理
  6. PSI压力感知:楼道里的压力传感器
  7. eBPF辅助监控:装上透明摄像头
  8. 与Namespace配合:门锁加账本
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示cgroups v1多控制器分散挂载与v2统一层级结构对比、资源限制CPU/内存/IO分配及与Namespace配合的容器隔离机制,分步演进理解核心原理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

cgroups v1多控制器子系统 v1每个子系统CPU/memory/IO独立挂载到不同cgroup层级,进程需分别加入各控制器 多层级导致进程归属不一致且管理复杂,控制器间协调困难缺乏统一视图 v1: mount cpu -> /sys/fs/cgroup/cpu, mount memory -> /sys/fs/cgroup/memory v1多控制器分散挂载是架构设计的主要缺陷

核心概念:进程组的资源账本

合租公寓最头疼的是分配公共资源。管家 cgroups 的做法很简单:所有的租客不再各自单独找房东要水电,而是按“房间群”来记账。一个房间住了两个人,这两个人合成一组,共用一份电费额度。在 Linux 里,这个“房间群”就叫进程组,管家为每个组建立一个账本目录,组里的进程共同消耗这份配额。

把进程按组记账,还会形成天然的层次关系。厨房的用电和客厅的用电可以挂在大楼总账下,分出厨房小组、客厅小组。Linux 内核把这个层次做成了一棵倒挂的树:树的根是系统总资源,节点是子组,叶子挂着具体的进程。管理员想限制某个服务,只需往对应目录的配置文件中写入限额,就像房客在账单上签名承诺“我们屋本月最多用 100 度电”。

这种“分组 + 树 + 限额文件”的机制就是 cgroups 的核心形态。它由若干控制器(subsystem)组成,每个控制器负责一种资源——CPU、内存、IO、进程数。控制器像公寓里不同的表:电表、水表、煤气表各看各的。cgroups 负责让这些表协同工作:目录本身定义结构,写进文件的值定义限额,进程被放进目录就自动纳入限额。

合租公寓的类比收个尾:cgroups 成了公寓的管家,它手里只有一本账,账本上写了哪个门牌号(组)、哪些人(进程)、用了多少量(资源)。进程想搬家,只要改改目录里的归属即可;系统想暂停一家的权限,冻结这个目录即可。管理容器时,这套“账本”就是一切资源控制的基石。

cgroup 树: 根root→子组1→子组2→⋯→进程列表tasks\displaystyle \text{cgroup 树}: \ 根_{\text{root}} \rightarrow 子组_1 \rightarrow 子组_2 \rightarrow \cdots \rightarrow 进程列表_{\text{tasks}}
这套连招的意思是:cgroups 不是给单个进程设限,而是给一组进程设限;资源额度从根一路继承下来,最底层的叶子目录里才装着真正的进程。
  1. 根节点(root)树的起点,代表整台机器的全部可用资源,系统启动时自动创建。
    根root=所有进程的祖先组\displaystyle 根_{\text{root}} = \text{所有进程的祖先组}
  2. 子组(child group)从根上分出来的中间节点,可以无限向下嵌套,形成多级管理。
    子组i→子组i+1 (i≥1)\displaystyle 子组_{i} \rightarrow 子组_{i+1} \ (i \ge 1)
  3. 进程列表(tasks)叶子节点保存的成员,写进这个目录的进程才算真正“入住”。
    进程列表={pid1,pid2,… }\displaystyle 进程列表 = \{ \text{pid}_1, \text{pid}_2, \dots \}

资源控制器:分配资源的多块仪表

公寓管家要管四块仪表:电表管 CPU 时间,水表管内存容量,煤气表管磁盘 IO,还有一块“入住人数表”管进程数上限。这四个表在 cgroups 里就是四大核心控制器,各自负责不同资源,独立配置又相互配合。

先用 CPU 控制器讲清“分账”的思路。设在同一个空闲组里有两个房间 A 和 B,A 配置权重 200,B 配置权重 100。总时间片 300 份,A 拿走 2/3,B 拿走 1/3。权重不是绝对上限,而是按比例分饼——如果 B 没活干,A 可以吃到更多的空闲余量,这恰好对应合租时“谁先回来谁开空调”的弹性配额。

内存控制器就像公寓水表:memory.max 是一户人家的总用水上限,超过后系统会先回收缓存、压缩内存,实在不够就驱逐进程(OOM Kill)。IO 控制器则像限速阀——它可以规定某个目录每秒最多读写多少兆字节,防止下载任务把整栋楼的带宽吃光。PIDs 控制器管“最多能租几个人进去”:没有这个表,一个死循环进程可以无限 fork 出新进程,把系统挤爆,这相当于一间房住了千人。

四块表各司其职,但它们的配置在 v1 版本中散落在不同目录,管理起来相当混乱,也就引出了下一节要讲的多棵树问题。

CPU时间i=weighti∑j∈可运行组weightj×调度周期\displaystyle \text{CPU时间}_{i} = \frac{\text{weight}_{i}}{\sum_{j \in \text{可运行组}} \text{weight}_{j}} \times \text{调度周期}
权重决定相对份额:你在组里占的权重越大,调度器分配给你的 CPU 时间片比例就越高,但总量受 cfs_period 周期约束。
  1. weight(权重)每个 cgroup 目录里配置的相对优先级,默认值通常是 100。
    weighti=100+nice 增量\displaystyle \text{weight}_i = 100 + \text{nice 增量}
  2. 分母求合把当前所有可运行(runnable)组群的权重加起来,作为分饼的总基数。
    ∑jweightj=总权重\displaystyle \sum_{j} \text{weight}_{j} = \text{总权重}
  3. 乘以周期调度周期通常为 100ms,最后得到该组在一轮中的 CPU 时间预算。
    时间预算=份额×cfs_period\displaystyle \text{时间预算} = \text{份额} \times \text{cfs\_period}

v1 多层级:各自为政的多棵挂载树

早期版本(v1)里,每块表都自己独立记账:电表一本账、水表一本账、煤气表又一本账,而且每本账都从整栋楼开始重新画一棵树。公寓管家手里有 CPU、内存、IO 三棵互不相通的挂载树,同一个租客(进程)会同时出现在这三棵树里,各占一个节点。

目录结构上,v1 在 /sys/fs/cgroup/ 下为每个控制器单独建目录:cpu/、memory/、blkio/。一个进程在 CPU 树里属于“厨房组”,在内存树里却属于“客厅组”,两棵树之间没有任何关联。管理员想给某个服务统一限 CPU 和内存,必须分别在两个目录下创建同名子目录,配置量翻倍且容易漏。

v1 最痛的问题是协同困难。内存回收需要知道每个进程在 IO 树上的层级,而两棵树互不通信,导致写回(writeback)的 IO 无法正确归属到内存 cgroup,清理进程优先级也容易误判。从数学上看,协调代价随控制器数量组合爆炸:

除了协同困难,v1 的委托模型也受限:普通用户想把某棵子树“交给”容器运行时托管,需要逐棵树的目录都授权,且各控制器目录的挂载点分散在多个路径,容器引擎(如 Docker)创建容器时要写十几个文件、跨多个目录,一旦漏掉一个,资源限制就不完整。这些痛点直接催生了 v2 的“一本总账”设计。

协调复杂度=∏k=1nmk\displaystyle \text{协调复杂度} = \prod_{k=1}^{n} m_k
当有 n 个控制器、每个控制器有 m 个层级时,跨控制器的组合关系是乘积关系。控制器越多,维护成本指数级上升。
  1. n 个控制器v1 支持 CPU、内存、IO、PIDs、cpuset 等多个子系统。
    n=∣Controller_Set∣\displaystyle n = \left| \text{Controller\_Set} \right|
  2. m 个层级每个控制器可以独立创建多级子目录,形成自己的挂载树。
    mk=第 k 棵树的深度\displaystyle m_k = \text{第 } k \text{ 棵树的深度}
  3. 乘积组合跨控制器找同一个进程的所有配置,需要遍历所有树的笛卡尔积组合。
    ∏k=1nmk=m1×m2×⋯×mn\displaystyle \prod_{k=1}^{n} m_k = m_1 \times m_2 \times \cdots \times m_n

v2 统一层级:一本总账的家族治理

v2 相当于房东换了一位新管家,把所有账本合并成一本总账,所有资源统一挂在同一个挂载点 /sys/fs/cgroup 下。不再出现“进程在 CPU 树的厨房组、内存树的客厅组”的分裂局面——每个进程在 v2 中只属于一个 cgroup 目录。

v2 的最关键设计是 no internal processes(不允许内部进程) 规则:树上的中间节点只做分组管理,不能直接装进程;进程必须被放到叶子节点。这就像大楼的分表必须挂在各层,不能所有房间的水费都记在楼顶总表上。这条规则保证了资源归属清晰,父组的统计值可以由子组精确求和。

委托模型也随之焕然一新。v2 提供 subtree_control 接口,容器运行时只要在某个子目录上声明“我要管理这个子树下的 CPU 和内存”,内核就把这棵子树的写入权委派给它。用户命名空间配合后,非 root 的容器引擎也能安心创建自己的 cgroup 子树,相当于管家把一整层楼的钥匙交给一个可信的二房东。

v2 的原子性也让回扣更有说服力:所有控制器写同一个目录下的文件,事务统一,告别“配置写了一半”的状态。有了统一的层级,内核还可以在此基础上做更精细的统计——这正为后面的 PSI 压力感知铺平了道路。

cpu.max=quotaperiod (周期内可用核数)\displaystyle \text{cpu.max} = \frac{\text{quota}}{\text{period}} \ (\text{周期内可用核数})
cpu.max 文件中写入的 value 就是 CPU 配额:period 是结算周期(默认 100000 微秒),quota 是这个周期内允许跑的微秒数,两者相除得到等效核数。
  1. quota(配额)一个周期内该 cgroup 允许消耗的 CPU 时间,单位微秒。
    quota=50000⇒每周期 50ms\displaystyle \text{quota} = 50000 \Rightarrow \text{每周期 50ms}
  2. period(周期)结算窗口长度,通常固定为 100000 微秒(100ms)。
    period=100000μs\displaystyle \text{period} = 100000 \mu s
  3. 相除得核数50000/100000 = 0.5,表示最多使用半个 CPU 核心。
    50000100000=0.5 核\displaystyle \frac{50000}{100000} = 0.5 \ \text{核}

PSI压力感知:楼道里的压力传感器

当家是租出去了,可怎么知道楼道里的水电是不是已经快到极限了?PSI(Pressure Stall Information,压力停顿信息)就像装在各楼层的压力传感器,专门记录“资源不够用导致进程排队等待”的时间比例,而不是简单报告用过多少资源。

PSI 统计的是停顿:一个进程想要 CPU 却轮不到,想要内存却要等回收,想要 IO 却挤不进队列,这些等待时间都被记下来。内核以 10 秒、60 秒、300 秒三个窗口滚动平均,形成类似 load average 的曲线。它在 /proc/pressure/ 目录下分文件记录 CPU、内存、IO 三类压力值,每个文件都给出 some 和 full 两行指标:

对容器编排来说,PSI 的价值在于提前预警。在 v2 中,内核还会在 cgroup 目录里提供 cpu.pressure、memory.pressure、io.pressure 文件,指标细化到每个容器组。平台团队可以据此判断“这个 Pod 是 CPU hungry 还是 memory thrashing”,赶在 OOM 之前扩容或限制,而不用等到进程被杀。

PSI10s=∑t(墙钟时间−运行时间)墙钟时间×100%\displaystyle \text{PSI}_{10s} = \frac{\sum_{t} (\text{墙钟时间} - \text{运行时间})}{\text{墙钟时间}} \times 100\%
some 表示“至少有一个进程在等待资源”的时间占比;full 表示“所有进程都被阻塞”的时间占比。full 指标越高,系统濒临卡死的风险越大。
  1. 墙钟时间统计窗口内的实际流逝时间,对应公寓里真实的等待时长。
    墙钟时间=Δt窗口\displaystyle \text{墙钟时间} = \Delta t_{\text{窗口}}
  2. 运行时间进程真正跑在 CPU 上或得到资源的时间。
    运行时间=∑activei\displaystyle \text{运行时间} = \sum \text{active}_i
  3. 相减取比例两者差值是等待浪费的时间,除以窗口总长得到压力百分比。
    墙钟−运行墙钟=等待占比\displaystyle \frac{\text{墙钟} - \text{运行}}{\text{墙钟}} = \text{等待占比}

eBPF辅助监控:装上透明摄像头

有了压力指标,还想看清房间里到底谁在搞鬼,这就轮到 eBPF 登场。eBPF 像帮管家在关键通道装上透明摄像头:不用改造公寓结构,也无打扰住户(不阻塞),就能看到每次进程进出 cgroup、每次资源限额变更的过程。

内核在 cgroup 的关键路径上预埋了 tracepoint(跟踪点),比如 cgroup_attach_task(进程加入某个组)、cgroup_release(进程离开组)、cgroup_rename(配置变更)。传统的监控方式需要不断去读文件、轮询比对;而 eBPF 程序直接挂在这些跟踪点旁边,事件一发生就被内核送出,开销低到几乎可以忽略。

eBPF 程序通过 map(数据地图)把事件聚合起来:以 cgroup id 为键,统计每个组创建了多少进程、切换了多少次 CPU、内存峰值多高。工具如 bpftrace 可以用几行脚本挂到上述 tracepoint 上;搭配 v2 稳定的 cgroup id,监控数据可以精确关联到容器 PID 与 Namespace:

eBPF 与 cgroups 的配合让监控从“看仪表”升级为“看监控录像”。平台可以在不改宿主机内核的情况下,铺开细粒度的资源审计和异常检测,这也是现代云原生可观测性产品(如 Cilium、Kindling)的核心技术手段。

事件速率c=ΔcountcΔt\displaystyle 事件速率_{c} = \frac{\Delta \text{count}_c}{\Delta t}
单位时间内某个 cgroup 发生的事件数量。相比轮询快照,事件驱动方式能捕捉瞬时抖动,定位“谁在半夜偷开空调”。
  1. Δcount在同一 cgroup 上两次采样之间累计的事件次数。
    Δcountc=countc(t2)−countc(t1)\displaystyle \Delta \text{count}_c = \text{count}_c(t_2) - \text{count}_c(t_1)
  2. Δt(时间差)两次采样经过的墙钟时间。
    Δt=t2−t1\displaystyle \Delta t = t_2 - t_1
  3. 事件速率用差值除以时间窗口,得到该组的平均事件频率,用来判断活跃程度。
    rate=ΔcountΔt\displaystyle rate = \frac{\Delta \text{count}}{\Delta t}

与Namespace配合:门锁加账本

最后把整套容器隔离拼起来。cgroups 和 Namespace 分工极其明确:Namespace 决定进程“看得见什么”,cgroups 决定进程“用得到多少”。前者是公寓楼里给每户装独立门锁和窗帘——进程以为自己是楼里唯一的住户,看不到别的房间;后者是管家账本上写清楚每户的用电额度。

容器创建时,运行时(如 runc)先为进程创建一组 Namespace(PID、Network、Mount 等),让进程获得独立视图;紧接着在 /sys/fs/cgroup 下创建一个新的 cgroup 子树,把 CPU、内存、IO 限额写进对应文件,再把这个进程放入该组。两步合起来,容器就像一套带独立锁的“公寓内小公寓”,视图独立、资源受控,缺一不可:

容器的隔离边界也要讲清楚:cgroups 隔离不了内核共享数据,不能阻止进程读取宿主机的公开信息;Namespace 隔离不了资源消耗。所以生产环境中,容器逃逸防护通常要靠额外的安全机制(seccomp、Capabilities)补足——门锁和账本都只是公寓管理的一部分,而不是全部安保。

容器=Namespace 视图隔离⏟门锁与窗帘+cgroups 资源配额⏟水电额度\displaystyle \text{容器} = \underbrace{\text{Namespace 视图隔离}}_{\text{门锁与窗帘}} + \underbrace{\text{cgroups 资源配额}}_{\text{水电额度}}
如果没有 Namespace,进程能看穿邻居的房间;如果没有 cgroups,一个容器可以把整台机器吃干抹净。两者相互补足,容器隔离才算完整。
  1. Namespace 视图隔离PID、网络、文件系统等维度各自独立,进程只看到自己空间。
    view=⋂ns∈NS独立命名空间\displaystyle \text{view} = \bigcap_{\text{ns} \in \text{NS}} \text{独立命名空间}
  2. cgroups 资源配额CPU 与内存等控制器为容器目录写入精确限额。
    limit={cpu.weight,memory.max,io.max}\displaystyle \text{limit} = \{ \text{cpu.weight}, \text{memory.max}, \text{io.max} \}
  3. 叠加生效视图隔离与资源配额相乘,构成完整的容器运行沙箱。
    沙箱强度=视界×配额\displaystyle \text{沙箱强度} = \text{视界} \times \text{配额}

AI 应用场景

云原生平台资源配额与 QoS 分级

阶段:模型服务部署与资源调度

解决的问题:传统方式为每个模型服务单独准备整台服务器,资源利用率低。用 cgroups 可以按模型优先级把 CPU/内存配额分成 Guaranteed、Burstable、BestEffort 三档,低优先级任务在高峰时主动让路。

具体案例:某大模型推理平台用 v2 的 cpu.weight 给在线推理服务设高权重,给离线批处理设低权重,并用 memory.high 为离线任务设置软上限。高峰期在线推理延迟稳住了,离线任务在低谷期自动抢占空闲资源,整体 GPU 利用率提升 30%。

混部集群的在线离线资源隔离

阶段:训练任务与在线推理混部

解决的问题:混部时离线训练任务容易挤占在线服务的 CPU 和内存,造成抖动。cgroups v2 统一层级配合 PSI 指标,可以实时监测 memory.pressure,一旦压力值超过阈值就把离线任务降权。

具体案例:某公司把训练任务和在线推荐服务混部在同一批机器上,通过 cgroup v2 的 subtree_control 委托给 Kubernetes 节点代理,代理根据 /proc/pressure/memory 动态调节离线 batch 的 cpu.weight,实现在线服务 P99 延迟不变的前提下,混部资源利用率提升 25%。

基于 eBPF 的容器资源画像

阶段:GPU 训练集群可观测性

解决的问题:普通监控只能看到 cgroup 维度汇总值,看不到单次事件。结合 eBPF 跟踪 cgroup_attach_task 与 cpu.max 变更事件,可以建立每个训练任务的资源画像,定位“哪一步操作触发了 cgroup 限额变更”。

具体案例:数据团队用 bpftrace 挂在 v2 的 tracepoint 上,发现某个数据加载脚本每次启动都会把 memory.max 短暂调大再调小,导致 OOM Killer 误杀训练进程。修复脚本后训练稳定性从 92% 提升到 99.8%。

Python 演示

python53 行
#!/usr/bin/env python3"""模拟 cgroups v1 与 v2 的 CPU 权重计算与层级委托差异。"""from dataclasses import dataclass, field  @dataclassclass Node:    name: str    weight: int = 100    children: list = field(default_factory=list)     def cpu_share_v2(self, siblings):        total = self.weight + sum(s.weight for s in siblings)        return self.weight / total     def insert(self, child):        self.children.append(child)  def build_v2_tree():    root = Node("root", weight=100)    online = Node("online", weight=200)    offline = Node("offline", weight=50)    root.insert(online)    root.insert(offline)    return root, online, offline  def build_v1_trees():    # v1 中 CPU 与 MEMORY 是两棵独立挂载树    cpu_tree = Node("cpu_root")    mem_tree = Node("mem_root")    cpu_proc = Node("proc_A", weight=150)    mem_proc = Node("proc_A", weight=100)    cpu_tree.insert(cpu_proc)    mem_tree.insert(mem_proc)    return cpu_tree, mem_tree  def main():    root, online, offline = build_v2_tree()    share_online = online.cpu_share_v2([offline])    print(f"v2: online 分得 {share_online:.2%} 的 CPU 饼")    print(f"v2: root 子节点数 = {len(root.children)}")     cpu_tree, mem_tree = build_v1_trees()    print(f"v1: 进程 proc_A 在 CPU 树权重 {cpu_tree.children[0].weight}")    print(f"v1: 进程 proc_A 在内存树权重 {mem_tree.children[0].weight}")    print("v1: 同一进程分别挂在两棵树上,配置需各写一次")  if __name__ == "__main__":    main()
目的:用两个小型数据类模拟 v1 与 v2 的核心差异——v1 中同一进程在多个控制器树里分散配置,v2 中统一层级直接按权重分饼。
  1. class Node:用树节点表示一个 cgroup 目录,weight 对应 cpu.weight 配置。
  2. def cpu_share_v2(self, siblings):按 v2 的权重比例计算 CPU 份额,实现公式 $\frac{\text{weight}}{\sum \text{weight}}$。
  3. def build_v1_trees():构造两棵独立树,模拟 v1 中 CPU 与内存控制器分散挂载的情况。
  4. print(f"v2: online 分得...")演示 v2 只需一次计算即可得结果,直观对比 v1 的多树一致性成本。

常见误区

误区“cgroups 不是万能的资源管家吗?网络带宽、文件系统配额应该都归它管吧?我给它配个参数,容器就什么资源都能限制了。”

正解cgroups 只负责进程组的 CPU、内存、IO、PIDs、cpuset 等内核资源。网络带宽并不在 cgroups 的标准控制器里,通常需要借助 tc(流量控制)或 net_cls 分类器间接实现。文件系统配额也由磁盘层的 quota 机制负责。把 cgroups 理解为“合租公寓的水电煤账本”而非“整楼基础设施维修”,就能避免职责错位。

为何常见:因为 Kubernetes 里 --cpu、--memory 等参数配置太直观,容易让人误以为所有资源限制都由同一套机制完成,忽略了网络与存储是独立子系统的现实。

误区“既然叫 cgroup(控制组),进程放进一个组就完事了。v1 里有多个 cgroup,进程应该只能属于其中一个吧?否则归属关系岂不是乱了?”

正解v1 的进程可以同时属于每个控制器各自的挂载树。一个进程在 CPU 树里属于 A 组,在内存树里属于 B 组,在两棵树中的“身份”完全独立。这正是 v1 协调复杂、更新麻烦的根源,也是 v2 用 no internal processes 规则统一归属的动机。把 v1 想象成多个互不通气的包租公,就知道进程“多头记账”毫不出奇。

为何常见:人们习惯用“一个对象属于一个集合”的直觉理解 cgroup,却没有注意到 v1 的设计本身是“每个子系统一棵树”,从而忽略了多树共存的结构。

误区“我把容器的 memory.max 设成 512M,只要进程内存一超过 512M,内核就该立刻把这个进程杀掉,像闸刀一样干脆。”

正解memory.max 是硬上限,但触发行为是分阶段的:先尝试回收页缓存,再执行内存压缩,仍不足才触发 OOM Killer。进程超过 memory.high(软上限)时会被持续限流,而超过 memory.max 时才会被系统性回收甚至杀掉。这就像公寓水表超支先停水警告,实在不行才开除住户,不会直接拉总闸。

为何常见:人们常把 memory.max 类比 CPU quota 那样精确计数,忽略了内存回收有 LRU 缓存层和异步写回机制,从超限到杀进程存在延迟链条。

误区“v2 引入了 memory.oom.group 和 PSI,资源管理更智能了。只要升级到 v2,容器就不会再 OOM,对吗?”

正解v2 并没有消除 OOM 的可能,只是让 OOM 的触发更可预期、更可观测。memory.oom.group 可以把整组进程一起杀掉,PSI 指标可以提前发现压力趋势,但如果你把 memory.max 配得太低,进程照样会被内核杀死。v2 的价值在“管得清楚”,不是“资源无限”。

为何常见:因为 v2 的宣传总是强调“统一层级、可委派、与 PSI 深度集成”,让用户容易神化 v2 的管理能力,忽略了其本质仍只是一套限额描述机制。

面试题

Q1. cgroups 的三大核心能力中,下列哪项描述最准确?

单选题 · easy

  • A.限制、审计、命名
  • B.限制、统计、控制← 正确答案
  • C.虚拟化、隔离、快照
  • D.调度、中断、同步

答案:B。cgroups 的核心是资源限制、资源统计、资源控制。命名与虚拟化是 Namespace 或其他机制的能力,调度中断同步属于内核其他模块。可以理解为管家管水电(限制)、查用量(统计)、按约定分配(控制),而门锁(Namespace)是另一套系统。

Q2. 关于 cgroups v2 的统一层级,下列说法正确的是?

单选题 · easy

  • A.每个控制器仍然独立挂载目录
  • B.所有控制器挂载在同一个 /sys/fs/cgroup 下← 正确答案
  • C.v2 完全移除了 CPU 限制功能
  • D.进程只能挂在根节点下

答案:B。v2 将所有控制器统一挂载到同一个目录树,进程只属于一个 cgroup。不再有多树并行的问题。如同从“多个包租公各记一本账”变成“一个管家一本总账”。CPU 限制功能依然保留。

Q3. CPU 控制器中的 cpu.max 文件写入 quota/period,其中 period 的默认单位与含义是?

单选题 · easy

  • A.秒,一个统计周期
  • B.微秒,结算周期← 正确答案
  • C.毫秒,调度队列长度
  • D.纳秒,硬件时钟周期

答案:B。cpu.max 的 period 通常为 100000 微秒(100ms)。quota 表示周期内允许运行的时间,两者相除才是等效 CPU 核数。微秒单位是内核文档的标准配置,理解成“每个结算周期内可用的时间配额”即可。

Q4. 在 cgroups v1 中,为什么同一个进程会出现在多个控制器树中?

单选题 · mid

  • A.因为 v1 中进程数量太多
  • B.因为每个控制器独立挂载层级,进程在每个树中各有一个身份← 正确答案
  • C.因为内核只会把进程记录一次
  • D.因为 v1 不支持多控制器

答案:B。v1 中每个控制器(如 CPU、内存、IO)都有自己独立的挂载树,一个进程可以同时进入不同树的不同节点,导致跨树协同非常繁琐。v2 通过统一层级消除了这种分裂。这就像一员租客在电表册和水表册里分别登记了两个门牌号。

Q5. cgroups v2 的委派模型主要依靠哪个机制实现?

单选题 · mid

  • A.cgroup.clone_children
  • B.subtree_control← 正确答案
  • C.memory.oom.group
  • D.cpu.weight.nice

答案:B。subtree_control 允许父组把子树的控制器管理权委派给子 cgroup,容器运行时因此可以在自己的子树内创建并管理子组。委派模型让非特权容器引擎也能安全使用 cgroup。这如同管家把一整层楼的钥匙交给二房东。

Q6. PSI 指标中 some 与 full 的区别是什么?

单选题 · mid

  • A.some 表示至少一个进程在等待资源,full 表示所有进程都被阻塞← 正确答案
  • B.some 表示部分 CPU 空闲,full 表示满载
  • C.两者含义完全相同
  • D.some 用于 CPU,full 用于内存

答案:A。some 统计的是“至少有一个进程因资源不足而停顿”的时间比例;full 统计“所有进程都被阻塞”的时间比例。full 越高说明系统越接近卡死。类比楼道压力传感器:some 是有人在排队,full 是队伍堵住了所有人。

Q7. 在 cgroups v2 中,为什么不允许中间节点直接放置进程?

单选题 · hard

  • A.为了节省内存开销
  • B.为了保证父组统计值能从子组精确汇总← 正确答案
  • C.为了禁止嵌套 cgroup
  • D.为了支持多个根层级

答案:B。no internal processes 规则要求进程只能挂在叶子节点。若中间节点有自己的进程,父组资源统计会出现“统计两难”,无法确定资源消耗属于父组还是子组。这个规则换来了统一且自洽的资源归属核算,是 v2 一致性的基石。

Q8. 下列哪些属于 cgroups v2 相比 v1 的关键改进?

多选题 · mid

  • A.统一层级,单一挂载点← 正确答案
  • B.支持内部进程树委派← 正确答案
  • C.原子性的配置写入← 正确答案
  • D.取代 Namespace 隔离

答案:ABC。v2 的核心改进是统一层级、subtree_control 委派模型、以及同一目录下多控制器文件的原子写入。v2 并不替代 Namespace,它只负责资源配额,Namespace 负责视图隔离,两者配合才是容器隔离完整方案。

Q9. 下列哪些工具与机制常被用来增强 cgroups 的可观测性?

多选题 · hard

  • A./proc/pressure/memory← 正确答案
  • B.eBPF 跟踪 cgroup_attach_task← 正确答案
  • C.memory.oom.group
  • D.bpftrace 挂载 tracepoint← 正确答案

答案:ABD。PSI 文件(/proc/pressure/*)提供压力等待指标,eBPF 与 bpftrace 能抓取 cgroup 生命周期事件并聚合统计。memory.oom.group 是 OOM 行为控制,不是可观测性工具。可观测性目标是“看得清”,行为控制目标是“管得住”。

Q10. 关于 Namespace 与 cgroups 的协作边界,下列说法正确的有?

多选题 · extreme

  • A.Namespace 隔离进程视野,cgroups 限制资源用量← 正确答案
  • B.容器必须同时使用两者才能实现完整隔离← 正确答案
  • C.cgroups 可以隔离内核共享数据
  • D.缺少 cgroups 时,容器可能耗尽宿主机全部资源← 正确答案

答案:ABD。Namespace 管“看得见什么”,cgroups 管“用得到多少”。容器需要两者结合才能实现安全运行:没有 Namespace 会看到邻居,没有 cgroups 会吃光整机资源。但 cgroups 并不隔离内核数据,隔离边界之外还需 seccomp、Capabilities 等安全机制补足。