Skip to content
Published on

[Linux] cgroup 完全指南:容器资源控制的核心

分享
Authors

目录

  1. 什么是 cgroup
  2. cgroup v1 vs v2
  3. 主要控制器深度解析
  4. Docker 与 cgroup
  5. Kubernetes 与 cgroup
  6. cgroup 实战排障
  7. cgroup 与安全
  8. 总结

1. 什么是 cgroup

Control Groups 的诞生

cgroup(Control Groups)是 Linux 内核提供的 以进程组为单位的资源限制与隔离机制。 2006 年,Google 工程师 Paul Menage 与 Rohit Seth 以 “process containers” 之名启动了开发; 2007 年合入 Linux 内核 2.6.24 时,为避免与内核中原有的 “container” 术语混淆,更名为 cgroup

cgroup 解决的问题

在没有 cgroup 的年代,很难阻止某一个进程独占整个系统的 CPU 或内存。 cgroup 提供了如下能力:

  • 资源限制(Resource Limiting):为 CPU、内存、IO、网络带宽等设置使用量上限
  • 优先级(Prioritization):调整进程组之间的资源分配比例
  • 统计(Accounting):按组测量并报告资源使用量
  • 控制(Control):对进程组执行暂停、恢复、检查点操作

Namespace 与 cgroup 的关系

容器技术的两大支柱是 Namespacecgroup

+-------------------------------------------+
|            Linux 容器隔离模型              |
+-------------------------------------------+
|                                           |
|  Namespace (隔离 - Visibility)            |
|  ┌─────────────────────────────────────┐  |
|  │ PID NS  : 进程 ID 隔离              │  |
|  │ NET NS  : 网络栈隔离                │  |
|  │ MNT NS  : 文件系统挂载隔离          │  |
|  │ UTS NS  : 主机名隔离                │  |
|  │ IPC NS  : IPC 资源隔离              │  |
|  │ USER NS : UID/GID 映射隔离          │  |
|  └─────────────────────────────────────┘  |
|                                           |
|  cgroup (限制 - Resource Limits)          |
|  ┌─────────────────────────────────────┐  |
|  │ CPU     : CPU 时间限制              │  |
|  │ Memory  : 内存使用量限制            │  |
|  │ IO      : 块设备 IO 限制            │  |
|  │ PIDs    : 进程数量限制              │  |
|  │ Devices : 设备访问控制              │  |
|  └─────────────────────────────────────┘  |
|                                           |
+-------------------------------------------+

核心差异可以概括为:

  • Namespace = “能看到什么”(隔离)
  • cgroup = “能使用多少”(限制)

cgroup 文件系统的基本结构

cgroup 通过虚拟文件系统(VFS)来管理。所有设置都以文件读写的方式完成。

# 确认 cgroup 挂载情况
mount | grep cgroup

# 挂载的是 cgroup v2 时
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

# 查看当前进程的 cgroup
cat /proc/self/cgroup
# 0::/user.slice/user-1000.slice/session-1.scope

2. cgroup v1 vs v2

cgroup v1 的结构

在 cgroup v1 中,每个控制器(CPU、memory、blkio 等)都拥有 独立的 hierarchy

cgroup v1 结构:

/sys/fs/cgroup/
├── cpu/                    # CPU 控制器
│   ├── docker/
│   │   ├── container-abc/
│   │   │   ├── cpu.cfs_quota_us
│   │   │   ├── cpu.cfs_period_us
│   │   │   └── cpu.shares
│   │   └── container-xyz/
│   └── tasks
├── memory/                 # Memory 控制器
│   ├── docker/
│   │   ├── container-abc/
│   │   │   ├── memory.limit_in_bytes
│   │   │   └── memory.usage_in_bytes
│   │   └── container-xyz/
│   └── tasks
├── blkio/                  # Block IO 控制器
├── pids/                   # PID 控制器
├── devices/                # Device 控制器
└── freezer/                # Freezer 控制器

v1 的特点:

  • 每个控制器拥有 独立的目录树
  • 一个进程在不同控制器下可以属于 不同的组
  • 灵活,但管理复杂

cgroup v2 的结构(Unified Hierarchy)

cgroup v2 使用 单一的统一 hierarchy

cgroup v2 结构(Unified):

/sys/fs/cgroup/                     # 根 cgroup
├── cgroup.controllers              # 可用控制器列表
├── cgroup.subtree_control          # 为子组启用的控制器
├── system.slice/                   # systemd 系统服务
│   ├── docker.service/
│   └── sshd.service/
├── user.slice/                     # 用户会话
│   └── user-1000.slice/
└── kubepods.slice/                 # Kubernetes pods
    ├── kubepods-burstable.slice/
    └── kubepods-besteffort.slice/

v1 vs v2 对比表

特性cgroup v1cgroup v2
Hierarchy按控制器各自独立单一统一(Unified)
挂载点/sys/fs/cgroup/cpu/、/sys/fs/cgroup/memory/ 等只有 /sys/fs/cgroup/ 一个
进程归属每个控制器可归属不同的组所有控制器归属同一个组
PSI (Pressure Stall Info)不支持支持
Threaded mode不支持支持
子树委派有限完整的 delegation 模型
Memory QoS只有 memory.limitmemory.min/low/high/max 四级
IO 控制blkio(不支持 buffered IO)io(支持 buffered IO)
CPU burst不支持(需要内核补丁)支持(kernel 5.14+)

v2 迁移兼容性

软件开始支持 cgroup v2 的版本
systemd236+
Docker20.10+
containerd1.4+
Kubernetes1.25+(GA)
Podman原生支持
RHEL9+(默认值)
Ubuntu21.10+(默认值)

确认 cgroup v2 是否已启用

# 确认是否正在使用 cgroup v2
stat -fc %T /sys/fs/cgroup/
# cgroup2fs  -> v2
# tmpfs      -> v1

# 也可以通过挂载信息确认
grep cgroup /proc/mounts

# 通过内核启动参数强制使用 v2
# GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"

3. 主要控制器深度解析

3.1 CPU 控制器

CFS Bandwidth Control

Linux 的 CFS(Completely Fair Scheduler)会公平地分配 CPU 时间。 cgroup CPU 控制器在 CFS 之上增加了 bandwidth throttling

CFS Bandwidth Control 原理:

Period = 100ms (默认值)
Quota  = 50ms  (配额)

 0ms        50ms       100ms       150ms       200ms
  |----------|----------|----------|----------|
  [##########]...........[##########]...........
  ^-- 执行 --^ ^- 限流 --^ ^-- 执行 --^ ^- 限流 --^

  以 1 个 CPU 为基准:quota/period = 50ms/100ms = 0.5 CPU (50%)

v1 vs v2 的 CPU 参数

# ===== cgroup v1 =====
# CFS quota: 单位为微秒,-1 表示无限制
cat /sys/fs/cgroup/cpu/docker/CONTAINER_ID/cpu.cfs_quota_us
# 50000 (50ms)

cat /sys/fs/cgroup/cpu/docker/CONTAINER_ID/cpu.cfs_period_us
# 100000 (100ms)

# CPU shares: 相对权重(默认 1024)
cat /sys/fs/cgroup/cpu/docker/CONTAINER_ID/cpu.shares
# 512

# ===== cgroup v2 =====
# cpu.max: "quota period" 格式
cat /sys/fs/cgroup/kubepods.slice/.../cpu.max
# 50000 100000 (50ms quota, 100ms period)
# "max 100000" -> 无限制

# cpu.weight: 1-10000(默认 100)
cat /sys/fs/cgroup/kubepods.slice/.../cpu.weight
# 100

CPU shares/weight 换算

v1 cpu.shares -> v2 cpu.weight 换算:

v2_weight = (1 + ((v1_shares - 2) * 9999) / 262142)

示例:
  v1 shares=1024(默认) -> v2 weight=39(约)
  v1 shares=512        -> v2 weight=20(约)
  v1 shares=2048       -> v2 weight=78(约)

cpuset:CPU 绑核

可以把进程固定(pinning)到特定的 CPU 核心上。

# v2: 指定要使用的 CPU 核心
echo "0-3" > /sys/fs/cgroup/mygroup/cpuset.cpus
echo "0" > /sys/fs/cgroup/mygroup/cpuset.mems

# 确认当前设置
cat /sys/fs/cgroup/mygroup/cpuset.cpus.effective

实操:手动创建限制 CPU 的 cgroup

# 在 cgroup v2 环境下实操

# 1. 创建新的 cgroup
sudo mkdir /sys/fs/cgroup/cpu-test

# 2. 启用 CPU 控制器(在根节点上)
echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# 3. 限制为 CPU 50%(50ms quota / 100ms period)
echo "50000 100000" | sudo tee /sys/fs/cgroup/cpu-test/cpu.max

# 4. 把当前 shell 加入该 cgroup
echo $$ | sudo tee /sys/fs/cgroup/cpu-test/cgroup.procs

# 5. 制造 CPU 负载并确认限制生效
stress --cpu 1 --timeout 10 &

# 6. 查看限流统计
cat /sys/fs/cgroup/cpu-test/cpu.stat
# usage_usec 4500000
# user_usec 4500000
# system_usec 0
# nr_periods 100
# nr_throttled 50
# throttled_usec 5000000

# 7. 清理
echo $$ | sudo tee /sys/fs/cgroup/cgroup.procs
sudo rmdir /sys/fs/cgroup/cpu-test

CPU Throttling 问题与 burst

CPU 限流在短促的 burst 型工作负载上,可能引发严重的延迟尖刺。

问题场景:平均 CPU 使用率 30% 的 Web 服务器(limit: 1 CPU)

正常时:  [###-------][###-------][###-------]  -> 响应 10ms
突发时:  [##########][..........][###-------]  -> 响应 110ms!
           ^-- 100ms 全部用完 --^ ^-- 限流 --^

解决:CPU burst(kernel 5.14+,cgroup v2)
cpu.max.burst = 允许的 burst 微秒数
把上一个 period 未使用的 quota 存起来,在 burst 时使用
# CPU burst 设置(cgroup v2,kernel 5.14+)
echo 20000 > /sys/fs/cgroup/mygroup/cpu.max.burst
# 允许最多 20ms 的 burst

3.2 Memory 控制器

v2 的四级内存限制

cgroup v2 提供了比 v1 更精细的四级内存控制。

Memory 控制四级(cgroup v2):

memory.min   : 绝对保护(低于此值不可回收)
                ↑ 最低保障区
memory.low   : 软保护(尽量避免回收,best-effort)
                ↑ 优先区
memory.high  : 软限制(超过则 throttle,不是 OOM)
                ↑ 警告区
memory.max   : 硬限制(超过则触发 OOM Killer)
                ↑ 禁止区

  0 MB                                              1024 MB
  |=====[min]==[low]==========[high]====[max]========|
  |<-受保护->|<-优先->|<-正常使用->|<-限流->|<-OOM->|

v1 vs v2 的内存参数

# ===== cgroup v1 =====
# 硬限制
echo 536870912 > memory.limit_in_bytes   # 512MB

# 软限制(回收压力)
echo 268435456 > memory.soft_limit_in_bytes  # 256MB

# 含 Swap 的限制
echo 1073741824 > memory.memsw.limit_in_bytes  # 1GB (mem+swap)

# OOM 控制
echo 1 > memory.oom_control  # 禁用 OOM Killer

# ===== cgroup v2 =====
echo 512M > memory.max       # 硬限制
echo 256M > memory.high      # 软限制(throttle)
echo 128M > memory.low       # best-effort 保护
echo 64M  > memory.min       # 绝对保护
echo 256M > memory.swap.max  # swap 限制

memory.stat 分析

cat /sys/fs/cgroup/kubepods.slice/.../memory.stat
# anon 104857600          # 匿名内存(heap、stack)
# file 52428800           # 文件缓存
# kernel 8388608          # 内核内存(slab 等)
# shmem 0                 # 共享内存
# pgfault 250000          # 缺页次数
# pgmajfault 10           # 主缺页次数
# workingset_refault 500  # working set 被 evict 后再次被引用
# oom_kill 0              # OOM kill 次数

重要指标解读:

  • anon 偏高:应用堆内存使用量大
  • file 偏高:文件缓存较多(通常是正常的)
  • pgmajfault 偏高:从磁盘读回页面的频率高(内存不足的信号)
  • oom_kill > 0:发生过 OOM

实操:内存限制与 OOM 测试

# 1. 创建限制内存的 cgroup
sudo mkdir /sys/fs/cgroup/mem-test
echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# 2. 设置 100MB 硬限制
echo 104857600 | sudo tee /sys/fs/cgroup/mem-test/memory.max

# 3. 设置 80MB 软限制(超过则 throttle)
echo 83886080 | sudo tee /sys/fs/cgroup/mem-test/memory.high

# 4. 把进程加入 cgroup 并分配内存
echo $$ | sudo tee /sys/fs/cgroup/mem-test/cgroup.procs

# 5. 尝试分配超过 100MB 的内存
python3 -c "
data = []
for i in range(200):
    data.append(bytearray(1024 * 1024))  # 每次分配 1MB
    print(f'Allocated {i+1} MB')
"
# -> 在约 100MB 处被 OOM Killed

# 6. 确认 OOM 事件
cat /sys/fs/cgroup/mem-test/memory.events
# oom 1
# oom_kill 1
# high 150

# 7. 在 dmesg 中确认 OOM 日志
dmesg | grep -i "killed process"

Swap 控制

# 在 cgroup v2 中限制 swap
echo 0 > /sys/fs/cgroup/mygroup/memory.swap.max  # 禁止使用 swap
echo max > /sys/fs/cgroup/mygroup/memory.swap.max  # swap 无限制

# v1 中通过 memory + swap 的合计来控制
echo 1073741824 > memory.memsw.limit_in_bytes  # mem + swap = 1GB

3.3 IO 控制器

v1(blkio)vs v2(io)

v1 的 blkio 控制器只能控制 Direct IO。Buffered IO 要经过 page cache,因此 blkio 追踪不到。 v2 的 io 控制器通过 writeback 机制,也能控制 Buffered IO。

# ===== cgroup v2: io.max =====
# 格式: MAJ:MIN rbps=NUM wbps=NUM riops=NUM wiops=NUM

# 确认设备号
lsblk -o NAME,MAJ:MIN
# sda   8:0

# 限制为读 10MB/s、写 5MB/s
echo "8:0 rbps=10485760 wbps=5242880" > /sys/fs/cgroup/mygroup/io.max

# IOPS 限制:读 1000 IOPS、写 500 IOPS
echo "8:0 riops=1000 wiops=500" > /sys/fs/cgroup/mygroup/io.max

io.weight(相对权重)

# io.weight: 1-10000(默认 100)
echo "default 200" > /sys/fs/cgroup/mygroup/io.weight

# 针对特定设备设置权重
echo "8:0 500" > /sys/fs/cgroup/mygroup/io.weight

io.latency(v2 专用)

通过设置 latency target 来保障 IO 延迟。

# 设置 5ms 的 latency target
echo "8:0 target=5000" > /sys/fs/cgroup/mygroup/io.latency

3.4 PID 控制器

防范 Fork Bomb

# 设置 PID 限制
echo 100 > /sys/fs/cgroup/mygroup/pids.max

# 查看当前 PID 数量
cat /sys/fs/cgroup/mygroup/pids.current
# 5

# Fork bomb 测试(只能在安全环境中做!)
# 没有限制时,整个系统都可能卡死
# 设置了 pids.max 后,到第 100 个时 fork() 就会失败
Fork Bomb 防范原理:

pids.max = 100

Process Tree:
init(1)
├── bash(2)
│   ├── worker(3)
│   ├── worker(4)
│   │   ├── child(5)
│   │   └── child(6)
│   ...
│   └── worker(100)    <- 达到 pids.max
│       └── fork() -> EAGAIN (失败!)

3.5 其他控制器

devices 控制器

以白名单/黑名单的方式控制设备访问。

# v1: devices.allow / devices.deny
echo 'c 1:3 rmw' > devices.allow   # 允许访问 /dev/null
echo 'b 8:0 r' > devices.allow     # 允许读取 /dev/sda
echo 'a' > devices.deny            # 拒绝所有设备

# GPU 访问控制(NVIDIA)
echo 'c 195:* rmw' > devices.allow  # 允许 NVIDIA 设备

freezer 控制器

可以暂停(freeze)与恢复(thaw)进程组。

# v2: cgroup.freeze
echo 1 > /sys/fs/cgroup/mygroup/cgroup.freeze  # 暂停
echo 0 > /sys/fs/cgroup/mygroup/cgroup.freeze  # 恢复

# 确认状态
cat /sys/fs/cgroup/mygroup/cgroup.events
# frozen 1

hugetlb 控制器

限制 Huge page 的使用量。

# 限制 2MB huge pages
echo 1073741824 > hugetlb.2MB.limit_in_bytes  # 1GB
cat hugetlb.2MB.usage_in_bytes

4. Docker 与 cgroup

Docker 的资源限制选项

# CPU 限制
docker run -d \
  --cpus="1.5" \              # 1.5 个 CPU(quota=150000, period=100000)
  --cpu-shares=512 \          # CPU shares(相对权重)
  --cpuset-cpus="0,1" \       # 只使用 CPU 0、1 号核心
  nginx

# 内存限制
docker run -d \
  --memory=512m \             # 内存硬限制 512MB
  --memory-swap=1g \          # 内存+交换合计 1GB(交换 512MB)
  --memory-reservation=256m \ # 软限制
  --oom-kill-disable \        # 禁用 OOM Killer(注意!)
  nginx

# IO 限制
docker run -d \
  --device-read-bps /dev/sda:10mb \   # 读 10MB/s
  --device-write-bps /dev/sda:5mb \   # 写 5MB/s
  --device-read-iops /dev/sda:1000 \  # 读 1000 IOPS
  nginx

# PID 限制
docker run -d \
  --pids-limit=100 \          # 最多 100 个进程
  nginx

Docker 创建的 cgroup 路径

# 使用 systemd cgroup driver 时
# /sys/fs/cgroup/system.slice/docker-CONTAINER_ID.scope/

# 确认容器 ID
CONTAINER_ID=$(docker ps -q --filter name=my-nginx)

# 确认 cgroup 路径
docker inspect --format='{{.HostConfig.CgroupParent}}' $CONTAINER_ID

# 直接查看 cgroup 文件(v2)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/cpu.max
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/pids.max

用 docker stats 做实时监控

# 所有容器的资源使用量
docker stats

# CONTAINER ID  NAME    CPU %  MEM USAGE/LIMIT   MEM %  NET I/O       BLOCK I/O
# abc123        nginx   0.50%  50MiB / 512MiB    9.77%  1.2kB/0B      0B/0B
# xyz789        redis   1.20%  30MiB / 256MiB    11.7%  500B/200B     4kB/0B

# 只看某一个容器
docker stats my-nginx --no-stream

Docker cgroup driver:cgroupfs vs systemd

+---------------------------+---------------------+---------------------+
|                           |    cgroupfs          |     systemd         |
+---------------------------+---------------------+---------------------+
| cgroup 管理方式           | Docker 直接管理      | 委派给 systemd       |
| 与 init 系统的冲突        | 有可能               | 无(已整合)         |
| Kubernetes 推荐度         | 不推荐               | 推荐(默认值)       |
| 配置位置                  | Docker 直接创建      | systemd scope/slice  |
| cgroup v2 兼容性          | 有限                 | 完全支持             |
+---------------------------+---------------------+---------------------+
// /etc/docker/daemon.json
// systemd cgroup driver 配置
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}

5. Kubernetes 与 cgroup

kubelet cgroup driver 配置

# kubelet 配置(/var/lib/kubelet/config.yaml)
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd # 推荐
cgroupsPerQOS: true # 为每个 QoS 创建 cgroup hierarchy
enforceNodeAllocatable:
  - pods
  - system-reserved
  - kube-reserved
kubeReserved:
  cpu: '500m'
  memory: '1Gi'
systemReserved:
  cpu: '500m'
  memory: '1Gi'

Pod QoS 类与 cgroup

Kubernetes 会根据 Pod 的 requests/limits 设置,赋予三种 QoS 类。

Pod QoS 类的判定逻辑:

1. Guaranteed: 所有容器都设置了 requests == limits
   - 优先级最高,OOM score = -997

2. Burstable: requests < limits(哪怕只有一部分)
   - 优先级中等,OOM score = 2~999

3. BestEffort: requests/limits 都未设置
   - 优先级最低,OOM score = 1000
# Guaranteed Pod 示例
apiVersion: v1
kind: Pod
metadata:
  name: guaranteed-pod
spec:
  containers:
    - name: app
      image: nginx
      resources:
        requests:
          cpu: '500m'
          memory: '256Mi'
        limits:
          cpu: '500m' # requests == limits
          memory: '256Mi' # requests == limits
# Burstable Pod 示例
apiVersion: v1
kind: Pod
metadata:
  name: burstable-pod
spec:
  containers:
    - name: app
      image: nginx
      resources:
        requests:
          cpu: '250m'
          memory: '128Mi'
        limits:
          cpu: '500m' # requests < limits
          memory: '256Mi'

Kubernetes 创建的 cgroup hierarchy

/sys/fs/cgroup/
└── kubepods.slice/                              # 所有 Pod
    ├── kubepods-burstable.slice/                # Burstable QoS
    │   └── kubepods-burstable-podABCD.slice/    # Pod 级别
    │       ├── cri-containerd-XXXX.scope        # 容器级别
    │       │   ├── cpu.max                      # limits.cpu
    │       │   ├── cpu.weight                   # 基于 requests.cpu
    │       │   ├── memory.max                   # limits.memory
    │       │   ├── memory.min                   # requests.memory (v2)
    │       │   └── pids.max                     # pod pid limit
    │       └── cri-containerd-YYYY.scope
    ├── kubepods-besteffort.slice/               # BestEffort QoS
    │   └── kubepods-besteffort-podEFGH.slice/
    └── kubepods-podIJKL.slice/                  # Guaranteed QoS
        └── cri-containerd-ZZZZ.scope

Kubernetes 资源请求映射到 cgroup 的方式

Kubernetes -> cgroup v2 映射:

resources.requests.cpu: 500m
  -> cpu.weight = 按(500m / 节点可分配 CPU 总量)比例换算
  -> Pod 竞争资源时可获得保障的 CPU 比例

resources.limits.cpu: 1000m
  -> cpu.max = "100000 100000" (100ms/100ms = 1 CPU)

resources.requests.memory: 256Mi
  -> memory.min = 268435456 (Guaranteed QoS in v2)

resources.limits.memory: 512Mi
  -> memory.max = 536870912

pid limit(kubelet 配置):
  -> pids.max = podPidsLimit(默认 -1,无限制)

节点资源预留

节点资源的分配:

节点总资源(例如 16 CPU、64Gi Memory)
├── kube-reserved    : kubelet、kube-proxy 等 (0.5 CPU, 1Gi)
├── system-reserved  : sshd、journald 等 (0.5 CPU, 1Gi)
├── eviction-threshold: 回收阈值 (memory.available=100Mi)
└── allocatable      : 可分配给 Pod 的资源 (15 CPU, 61.9Gi)

allocatable = total - kube-reserved - system-reserved - eviction-threshold

cgroup v2 + Kubernetes:MemoryQoS

Kubernetes 1.22+ 中的 MemoryQoS 功能(alpha/beta)利用了 cgroup v2 的 memory.high。

MemoryQoS 的行为:

Burstable Pod (requests: 256Mi, limits: 512Mi)

cgroup v2 设置:
  memory.min  = 0       (BestEffort 无保护)
  memory.low  = 0       (默认)
  memory.high = 268435456 (基于 requests,throttle point)
  memory.max  = 536870912 (基于 limits,OOM point)

内存使用超过 requests(256Mi) 时:
  -> 由 memory.high 触发 throttle(分配速度下降)
  -> 不会立刻被 OOM kill
  -> 超过 limits(512Mi) 时才会 OOM kill

6. cgroup 实战排障

6.1 确认 CPU Throttling

# 确认容器的 CPU throttling
# 找到 Pod 的 cgroup 路径
CGROUP_PATH=$(cat /proc/1/cgroup | grep -oP '(?<=::).*')

# 查看 cpu.stat
cat /sys/fs/cgroup${CGROUP_PATH}/cpu.stat
# usage_usec 45000000
# user_usec 40000000
# system_usec 5000000
# nr_periods 1000
# nr_throttled 350       <- 限流比例达到 35%!
# throttled_usec 15000000

# 计算限流比例
# throttle_ratio = nr_throttled / nr_periods
# 350 / 1000 = 35% -> 偏高!需要考虑提高 limits

CPU Throttling 的解决策略

CPU Throttling 诊断流程:

1. nr_throttled / nr_periods > 5% 吗?
   ├── 是 -> 进入 2
   └── 否 -> CPU throttling 不是问题

2. 平均 CPU 使用量是否接近 limits?
   ├── 是 -> 需要提高 limits
   └── 否 -> 属于 burst 模式问题 -> 进入 3

3. 是否在很短时间内发生 CPU burst?
   ├── 是 -> 设置 cpu.max.burst(kernel 5.14+)
   │         或者考虑移除 CPU limits
   └── 否 -> 考虑调整 period

6.2 OOM Killer 排障

# 1. 在 dmesg 中确认 OOM 事件
dmesg | grep -i "killed process"
# [12345.678] Killed process 1234 (java) total-vm:4096000kB,
#   anon-rss:524288kB, file-rss:8192kB, shmem-rss:0kB,
#   UID:1000 pgrp:1234

# 2. 确认 cgroup 内存事件
cat /sys/fs/cgroup/kubepods.slice/.../memory.events
# low 0
# high 500       # 超过 memory.high 的次数
# max 10         # 超过 memory.max 的次数
# oom 2          # 发生 OOM 的次数
# oom_kill 2     # OOM kill 的次数

# 3. 当前内存使用量 vs 限制
cat /sys/fs/cgroup/kubepods.slice/.../memory.current
# 524288000  (500MB)
cat /sys/fs/cgroup/kubepods.slice/.../memory.max
# 536870912  (512MB)  <- 几乎到顶了!

# 4. 内存使用的详细分析
cat /sys/fs/cgroup/kubepods.slice/.../memory.stat | head -10

OOM 的解决策略

OOM Kill 诊断流程:

1. 在 memory.events 中确认 oom_kill > 0
   ├── oom_kill 在增加 -> 进入 2
   └── oom_kill = 0    -> 不是 OOM 问题

2. 比较 memory.current 与 memory.max
   ├── current >= max * 0.9 -> 内存不足
   │   ├── anon 偏高 -> 可能是堆内存泄漏 -> 做性能剖析
   │   ├── file 偏高 -> 文件缓存过多 -> 有可能是正常的
   │   └── shmem 偏高 -> 确认共享内存的使用情况
   └── current << max -> 确认是否存在突发的分配模式

3. 解决办法:
   a) 提高 memory limits
   b) 修复应用的内存泄漏
   c) JVM: 设置 -XX:MaxRAMPercentage=75
   d) 用 memory.high 施加 soft throttle

6.3 确认进程所属的 cgroup

# 确认某个进程的 cgroup
cat /proc/PID/cgroup
# v2 输出: 0::/kubepods.slice/kubepods-burstable.slice/...
# v1 输出:
# 12:pids:/docker/abc123
# 11:memory:/docker/abc123
# 10:cpu,cpuacct:/docker/abc123

# systemd-cgls: 显示 cgroup 树
systemd-cgls
# Control group /:
# -.slice
# ├─user.slice
# │ └─user-1000.slice
# ├─system.slice
# │ ├─docker.service
# │ └─sshd.service
# └─kubepods.slice
#   ├─kubepods-burstable.slice
#   └─kubepods-besteffort.slice

6.4 systemd-cgtop:实时 cgroup 资源监控

# systemd-cgtop: 类似 top 的 cgroup 监控
systemd-cgtop

# Control Group              Tasks   %CPU   Memory  Input/s Output/s
# /                             235   15.2     3.5G        -        -
# /system.slice                  45    5.1     1.2G        -        -
# /kubepods.slice               120    8.3     2.0G        -        -
# /kubepods.slice/burstable      80    6.1     1.5G        -        -

6.5 cAdvisor 与 Prometheus 指标

主要的 cAdvisor Prometheus 指标:

CPU:
  container_cpu_usage_seconds_total      # 累计 CPU 使用时间
  container_cpu_cfs_throttled_seconds_total  # 累计限流时间
  container_cpu_cfs_periods_total        # CFS period 总数
  container_cpu_cfs_throttled_periods_total  # 被限流的 period 数

Memory:
  container_memory_usage_bytes           # 当前内存使用量
  container_memory_working_set_bytes     # working set(OOM 判定依据)
  container_memory_rss                   # RSS(实际物理内存)
  container_memory_cache                 # 页缓存

OOM:
  container_oom_events_total             # OOM 事件次数
  kube_pod_container_status_last_terminated_reason  # OOMKilled 等

实用的 PromQL 查询

# CPU 限流比例(5 分钟平均)
rate(container_cpu_cfs_throttled_periods_total[5m])
/ rate(container_cpu_cfs_periods_total[5m])

# 内存使用率(相对 limits)
container_memory_working_set_bytes
/ container_spec_memory_limit_bytes

# 发生 OOM Kill 的 Pod
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}

7. cgroup 与安全

容器隔离的三要素

容器安全隔离的层次:

+--------------------------------------------------+
|                 Host Kernel                       |
|                                                  |
|  ┌──────────┐ ┌──────────┐ ┌──────────┐         |
|  │ Namespace│ │  cgroup  │ │ Seccomp/ │         |
|  │  (隔离)  │ │  (限制)  │ │ AppArmor │         |
|  │          │ │          │ │  (安全)  │         |
|  │ - PID    │ │ - CPU    │ │ - syscall│         |
|  │ - NET    │ │ - Memory │ │   过滤   │         |
|  │ - MNT    │ │ - IO     │ │ - MAC    │         |
|  │ - USER   │ │ - PIDs   │ │   策略   │         |
|  └──────────┘ └──────────┘ └──────────┘         |
|                                                  |
+--------------------------------------------------+

三者齐备,隔离才安全!

CVE-2022-0492:cgroup escape 漏洞

CVE-2022-0492 是滥用 cgroup v1 的 release_agent 机制实现容器逃逸的漏洞。

攻击原理:

1. release_agent 是 cgroup 中最后一个进程退出时执行的程序
2. 漏洞:cgroup_release_agent_write() 中缺少权限校验
3. 攻击者用 unshare() 创建新的 user/cgroup namespace
4. 挂载可写的 cgroupfs
5. 在 release_agent 中设置要在宿主机上执行的二进制路径
6. 以宿主机权限执行任意代码

防御:
- 已启用 Seccomp + AppArmor/SELinux 时可以防住
- 阻断 unshare() syscall,或者
- 限制 cgroupfs 的挂载

rootless containers 与 cgroup v2 delegation

# 在 cgroup v2 上要让 rootless 容器正常工作,
# 需要把 cgroup 子树委派给用户

# 在 systemd 中配置委派
sudo systemctl edit user@1000.service
# [Service]
# Delegate=cpu memory pids io

# 或者按用户单元配置
mkdir -p /etc/systemd/system/user@.service.d/
cat > /etc/systemd/system/user@.service.d/delegate.conf << 'CONF'
[Service]
Delegate=cpu cpuset io memory pids
CONF
systemctl daemon-reload

Kubernetes Pod Security Standards 与 cgroup

# Pod Security Standard: restricted
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: nginx
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ['ALL']
        readOnlyRootFilesystem: true
      resources:
        limits: # cgroup 资源限制必不可少
          cpu: '500m'
          memory: '256Mi'
        requests:
          cpu: '250m'
          memory: '128Mi'

8. 总结

从 cgroup v1 迁移到 v2 的检查清单

  • 确认内核版本:推荐 5.8+(PSI、CPU burst 等)
  • 确认 systemd 版本:236+
  • 确认容器运行时:Docker 20.10+、containerd 1.4+
  • 确认 Kubernetes 版本:1.25+(GA)
  • 设置 GRUB 启动参数
  • 把 kubelet cgroup driver 改为 systemd
  • 确认监控工具的兼容性(cAdvisor、Prometheus)
  • 如果有自定义的 cgroup 脚本,更新为 v2 接口
  • 在测试环境充分验证后再应用到生产

生产环境推荐配置

# Kubernetes 节点推荐配置
# kubelet config
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
cgroupsPerQOS: true
podPidsLimit: 1024 # 防范 Fork bomb
enforceNodeAllocatable:
  - pods
  - system-reserved
  - kube-reserved
kubeReserved:
  cpu: '500m'
  memory: '1Gi'
  ephemeral-storage: '1Gi'
systemReserved:
  cpu: '500m'
  memory: '1Gi'
  ephemeral-storage: '1Gi'
evictionHard:
  memory.available: '100Mi'
  nodefs.available: '10%'
  imagefs.available: '15%'

核心命令速查表

# === 信息确认 ===
cat /proc/self/cgroup                    # 当前进程的 cgroup
stat -fc %T /sys/fs/cgroup/             # v1(tmpfs) vs v2(cgroup2fs)
cat /sys/fs/cgroup/cgroup.controllers   # 可用控制器列表
systemd-cgls                            # 显示 cgroup 树
systemd-cgtop                           # 实时 cgroup 监控

# === CPU ===
cat /sys/fs/cgroup/PATH/cpu.max         # CPU quota/period
cat /sys/fs/cgroup/PATH/cpu.weight      # CPU weight
cat /sys/fs/cgroup/PATH/cpu.stat        # 限流统计

# === Memory ===
cat /sys/fs/cgroup/PATH/memory.max      # 硬限制
cat /sys/fs/cgroup/PATH/memory.current  # 当前使用量
cat /sys/fs/cgroup/PATH/memory.stat     # 详细统计
cat /sys/fs/cgroup/PATH/memory.events   # OOM 事件

# === IO ===
cat /sys/fs/cgroup/PATH/io.max          # IO 限制
cat /sys/fs/cgroup/PATH/io.stat         # IO 统计

# === PID ===
cat /sys/fs/cgroup/PATH/pids.max        # PID 限制
cat /sys/fs/cgroup/PATH/pids.current    # 当前 PID 数量

# === Docker ===
docker stats                             # 实时资源监控
docker inspect CONTAINER | grep -i cgroup  # 确认 cgroup 配置

# === 排障 ===
dmesg | grep -i "killed process"         # OOM kill 日志
cat /proc/PID/cgroup                     # 确认进程的 cgroup
cat /proc/PID/oom_score                  # 确认 OOM 分数

测验:cgroup 理解度测试

Q1. cgroup v1 与 v2 在结构上最大的差别是什么?

A: v1 中每个控制器拥有各自独立的 hierarchy,而 v2 使用单一统一(unified)hierarchy。在 v2 中,一个进程在所有控制器下都属于同一个 cgroup。

Q2. Kubernetes 的 Pod QoS 类中,要成为 “Guaranteed” 需要什么条件?

A: 所有容器的 CPU 与内存的 requests 和 limits 都必须设置成相同的值。

Q3. cgroup v2 的 memory.high 与 memory.max 有什么区别?

A: memory.high 是软限制,超过时会对进程进行 throttle(限速)。memory.max 是硬限制,超过时会触发 OOM Killer。

Q4. CPU 限流比例为 35% 意味着什么?

A: 意味着在全部 CFS period 中,有 35% 的 period 把 CPU quota 用尽,导致进程无法继续执行。需要考虑提高 CPU limits 或配置 CPU burst。

Q5. 在 Docker 中,为什么 systemd cgroup driver 比 cgroupfs 更受推荐?

A: 当 systemd 作为 init 系统运行时,如果 cgroupfs 与 systemd 两个 cgroup 管理者同时存在,在资源压力大的情况下系统可能变得不稳定。使用 systemd driver 就能统一到单一的 cgroup 管理者。

Q6. CVE-2022-0492 滥用的是哪一个 cgroup 机制?

A: 滥用的是 cgroup v1 的 release_agent 机制。release_agent 是 cgroup 中最后一个进程退出时执行的程序,由于缺少权限校验,攻击者可以从容器中以宿主机权限执行任意代码。


参考资料

  • Linux Kernel Documentation: Control Group v2
  • Kubernetes Documentation: About cgroup v2
  • Red Hat: Migrating from CGroups V1 to V2
  • CFS Bandwidth Control - Linux Kernel Documentation