目录
- 什么是 cgroup
- cgroup v1 vs v2
- 主要控制器深度解析
- Docker 与 cgroup
- Kubernetes 与 cgroup
- cgroup 实战排障
- cgroup 与安全
- 总结
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 的关系
容器技术的两大支柱是 Namespace 和 cgroup。
+-------------------------------------------+
| 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 v1 | cgroup v2 |
|---|---|---|
| Hierarchy | 按控制器各自独立 | 单一统一(Unified) |
| 挂载点 | /sys/fs/cgroup/cpu/、/sys/fs/cgroup/memory/ 等 | 只有 /sys/fs/cgroup/ 一个 |
| 进程归属 | 每个控制器可归属不同的组 | 所有控制器归属同一个组 |
| PSI (Pressure Stall Info) | 不支持 | 支持 |
| Threaded mode | 不支持 | 支持 |
| 子树委派 | 有限 | 完整的 delegation 模型 |
| Memory QoS | 只有 memory.limit | memory.min/low/high/max 四级 |
| IO 控制 | blkio(不支持 buffered IO) | io(支持 buffered IO) |
| CPU burst | 不支持(需要内核补丁) | 支持(kernel 5.14+) |
v2 迁移兼容性
| 软件 | 开始支持 cgroup v2 的版本 |
|---|---|
| systemd | 236+ |
| Docker | 20.10+ |
| containerd | 1.4+ |
| Kubernetes | 1.25+(GA) |
| Podman | 原生支持 |
| RHEL | 9+(默认值) |
| Ubuntu | 21.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
현재 단락 (1/554)
1. 什么是 cgroup