前言
上一篇我们聊了 Linux 命名空间,它像 “结界” 一样为容器划分了独立的运行环境。但只有隔离还远远不够 —— 试想这样一个场景:服务器上同时运行着电商核心订单容器和一个数据导出容器,突然数据导出容器因为程序 bug 疯狂申请内存,短时间内就把服务器的 16GB 内存全部占满。结果就是订单容器因内存不足被系统 kill,直接导致业务中断。
这时候就需要 Linux 控制组(cgroups)登场了。它就像服务器的 “资源管家”,能给每个容器(或进程)划定明确的资源上限,比如 “这个容器最多用 2 核 CPU、4GB 内存”,就算容器出现异常,也不会影响其他服务的正常运行。
今天这篇文章就带大家吃透 cgroups:从核心子系统拆解到亲手实操资源限制,再关联 K8s 的资源配置,最后补充常见问题排错指南,让你彻底掌握容器资源管控的底层逻辑,从 “资源差不多” 先生,变为精准控制的运维工程师。
一、先搞懂:cgroups 到底是什么?
cgroups(Control Groups)是 Linux 内核提供的一种进程资源管控技术,核心作用是对进程组进行资源限制、统计和隔离。这里的资源包括 CPU、内存、磁盘 IO、进程数量等。
如果说命名空间解决的是 “进程看不到彼此” 的隔离问题,那 cgroups 解决的就是 “进程不抢彼此资源” 的管控问题。两者结合,才构成了容器技术的两大核心基石。
用一个通俗的比喻理解:
- 服务器就像一栋公寓楼;
- 每个容器是一间住户;
- 命名空间是公寓的墙体,让住户之间互不干扰;
- cgroups 是公寓的物业,规定每户的用电量上限、用水量上限,避免某户过度使用资源影响整栋楼。
cgroups 的底层实现非常简洁,它通过虚拟文件系统(挂载在 /sys/fs/cgroup 目录)来管理配置。我们对资源的所有限制,本质上都是在修改这个目录下的文件。
二、核心拆解:cgroups 的 5 个关键子系统
cgroups 通过多个子系统(Subsystem)实现对不同资源的管控,每个子系统对应一种资源类型。下面聚焦容器和 K8s 中最常用的 5 个子系统,结合配置文件和实操命令逐一拆解。
| 子系统 | 核心作用 | 关键配置文件 | 通俗说明 |
|---|---|---|---|
| cpu | 限制 CPU 使用份额、使用时长 | cpu.shares、cpu.cfs_quota_us、cpu.cfs_period_us | 控制 “最多能用多少 CPU”“和其他进程抢 CPU 时的优先级” |
| cpuacct | 统计 CPU 使用情况 | cpuacct.usage、cpuacct.stat | 记录 “用了多少 CPU 时间”“用户态 / 内核态分别用了多少” |
| memory | 限制内存使用总量(物理内存 + 交换分区) | memory.limit_in_bytes、memory.memsw.limit_in_bytes | 给进程戴 “内存紧箍咒”,超量可能被 OOM 杀死 |
| blkio | 限制块设备(磁盘 / SSD)的 IO 速率 | blkio.throttle.read_bps_device、blkio.throttle.write_bps_device | 控制 “读 / 写磁盘的速度”,避免某进程占满磁盘 IO 导致其他服务卡顿 |
| pids | 限制进程组内的最大进程数量 | pids.max | 防止容器内出现进程爆炸(比如死循环创建进程) |
2.1 cpu 子系统:CPU 资源的 “分配器”
cpu 子系统有两种核心限制方式,适配不同场景:
- 份额分配(cpu.shares):非强制性限制,用于 CPU 竞争时的优先级分配。默认份额 1024,份额越高,获取的 CPU 时间片越多。比如 A 组 2048、B 组 1024,繁忙时 A 组会分到约 2/3 CPU 时间,空闲时两者都能用到 100% CPU。
- 绝对限制(cpu.cfs_quota_us/cpu.cfs_period_us):强制性限制使用时长。
cpu.cfs_period_us是时间周期(默认 100000 微秒 = 100ms),cpu.cfs_quota_us是该周期内最多可用 CPU 时间。比如设置quota=50000,相当于限制 CPU 使用率 50%。
快速验证:
bash
# 查看宿主机 cpu 子系统目录,确认配置文件存在
ls /sys/fs/cgroup/cpu/
2.2 memory 子系统:内存资源的 “防火墙”
memory 子系统是最常用的子系统之一,核心是限制进程组的最大内存使用量,避免 OOM 导致系统崩溃。
memory.limit_in_bytes:设置最大可用物理内存;memory.memsw.limit_in_bytes:设置物理内存 + 交换分区的总上限;memory.usage_in_bytes:实时查看该进程组的内存使用量。
这也是后续动手实验的核心配置项。
2.3 blkio 子系统:磁盘 IO 的 “调速器”
对于数据库、大数据等 IO 密集型应用,blkio 子系统能避免其占满磁盘 IO。比如限制对 /dev/sda 磁盘的写入速度为 10MB/s:
bash
# 格式:echo "设备号 速率" > blkio.throttle.write_bps_device
# 8:0 是 /dev/sda 的设备号,可通过 lsblk -f 查看
echo "8:0 10485760" | sudo tee /sys/fs/cgroup/blkio/my_cgroup/blkio.throttle.write_bps_device
2.4 pids 子系统:进程数量的 “计数器”
限制进程组内的最大进程数,防止恶意程序或 bug 导致的 “进程爆炸”。比如设置某容器最多创建 10 个进程,当进程数达到上限时,新进程无法创建。
2.5 cpuacct 子系统:CPU 使用的 “记账员”
仅负责统计,不做限制。记录进程组的 CPU 总使用时长、用户态和内核态使用时长,方便排查资源占用问题:
bash
# 查看某 cgroup 的 CPU 统计信息
cat /sys/fs/cgroup/cpuacct/my_cgroup/cpuacct.stat
三、实战高潮:亲手给进程戴 “内存紧箍咒”
下面绕过 Docker、K8s 等上层工具,直接操作 cgroups 文件系统,给进程设置 100MB 内存限制。这个实验能让你直击 cgroups 的底层本质。
3.1 实验准备
- 环境:CentOS 9/RHEL 9 或 Ubuntu 22.04(需 root 权限,cgroups 默认已启用)
- 依赖:系统自带 cgroups 虚拟文件系统,无需额外安装工具
3.2 实验目标
创建 cgroups 分组,限制组内进程最多使用 100MB 内存,验证超限制时的系统干预效果。
3.3 详细步骤(每步带注释)
步骤 1:确认 cgroups 挂载状态
先验证 cgroups 已挂载到 /sys/fs/cgroup:
bash
# 查看挂载情况
mount | grep cgroup
# 预期输出:包含 "cgroup on /sys/fs/cgroup type tmpfs",说明挂载正常
步骤 2:创建自定义 cgroups 分组
在 memory 子系统下创建名为 my_container 的分组(创建目录时,内核会自动生成默认配置文件):
bash
# 进入 memory 子系统目录
cd /sys/fs/cgroup/memory
# 创建分组目录
sudo mkdir my_container
# 查看生成的配置文件(包含 memory.limit_in_bytes 等核心文件)
ls my_container/
步骤 3:设置 100MB 内存限制
内存限制以字节为单位(100MB = 100×1024×1024 = 104857600 字节):
bash
# 写入内存限制
echo 104857600 | sudo tee /sys/fs/cgroup/memory/my_container/memory.limit_in_bytes
# 验证设置是否生效
cat /sys/fs/cgroup/memory/my_container/memory.limit_in_bytes
# 预期输出:104857600
步骤 4:启动测试进程并加入分组
启动一个后台 sleep 进程,再将其加入创建的 cgroups 分组:
bash
# 启动后台进程(运行 10000 秒)
sleep 10000 &
# 获取进程 PID($! 表示上一个后台进程的 PID)
PID=$!
echo "测试进程 PID:$PID"
# 将进程加入 cgroups 分组(cgroup.procs 文件记录组内进程 PID)
echo $PID | sudo tee /sys/fs/cgroup/memory/my_container/cgroup.procs
# 验证进程归属(输出 PID 即表示加入成功)
cat /sys/fs/cgroup/memory/my_container/cgroup.procs
步骤 5:验证内存限制效果
编写 Python 脚本疯狂申请内存,验证限制是否生效:
bash
# 创建内存测试脚本
cat > memory_test.py << EOF
import sys
import time
def consume_memory():
memory_list = []
try:
while True:
# 每次申请 10MB 内存
memory_list.append(' ' * 1024 * 1024 * 10)
print(f"已申请内存:{len(memory_list)*10}MB")
time.sleep(0.5)
except MemoryError as e:
print("内存不足,申请失败!", e)
sys.exit(1)
if __name__ == "__main__":
consume_memory()
EOF
# 替换原 sleep 进程,让测试脚本继承 PID(确保在 cgroups 分组内)
sudo exec -a memory_test $PID python3 memory_test.py
预期结果:当脚本申请内存接近 100MB 时,会输出 MemoryError 或被系统 OOM killer 终止,证明内存限制生效。
步骤 6:清理实验环境
实验完成后,删除创建的 cgroups 分组:
bash
# 若测试脚本未终止,先执行 kill $PID 终止进程
sudo rmdir /sys/fs/cgroup/memory/my_container
3.4 实验结论
cgroups 通过文件系统实现资源限制,所有上层工具(Docker、K8s)的资源配置,最终都会转化为对 /sys/fs/cgroup 目录下文件的修改。我们手动操作的过程,就是这些工具在底层执行的核心逻辑。
四、关键关联:cgroups 与 K8s 的资源配置
K8s 中 Pod 的 resources.requests 和 resources.limits 配置,本质上是通过 cgroups 实现的。K8s 会将 Pod 资源配置自动转化为节点上的 cgroups 规则。
4.1 K8s 配置示例
以 Nginx Pod 为例,配置 CPU 和内存限制:
yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-resource-demo
spec:
containers:
- name: nginx
image: nginx
resources:
requests: # 调度时的资源请求,K8s 调度器根据此分配节点
cpu: "1"
memory: "1Gi"
limits: # 资源上限,通过 cgroups 强制执行
cpu: "2"
memory: "2Gi"
4.2 配置与 cgroups 的映射关系
当 Pod 被调度到节点后,节点的 kubelet 会执行以下操作:
- 在
/sys/fs/cgroup下为容器创建专属 cgroups 分组; limits.cpu: "2"→ cpu 子系统cpu.cfs_quota_us=200000(每个 100ms 周期最多用 200ms CPU);limits.memory: "2Gi"→ memory 子系统memory.limit_in_bytes=2147483648;- 将容器主进程 PID 写入分组的
cgroup.procs文件,完成资源限制绑定。
4.3 架构图解:K8s 到 cgroups 的资源管控链路

五、延伸思考:cgroups 的常见误区
- 内存限制不是 “弹性伸缩”:
memory.limit_in_bytes是硬限制,进程达限制后要么申请失败,要么被 OOM 杀死,不会自动扩容; - CPU 份额不是绝对上限:
cpu.shares仅在 CPU 繁忙时生效,空闲时进程仍可使用 100% CPU;若需绝对限制,需用cpu.cfs_quota_us; - cgroups 是进程组管理:一个分组可包含多个进程,限制规则对组内所有进程生效。
六、cgroups 常见问题排错指南
6.1 实验操作失败解决方案
| 问题现象 | 可能原因 | 解决方法 | |
|---|---|---|---|
| 无法创建 cgroups 目录(Permission denied) | 未使用 root 权限 | 执行命令前加 sudo,或切换到 root 用户(sudo -i) | |
| 写入内存限制后不生效 | 系统启用了 swap,未限制 memory.memsw.limit_in_bytes | 同时设置交换分区上限:`echo 104857600 | sudo tee .../memory.memsw.limit_in_bytes` |
| 进程无法加入 cgroups 分组 | PID 不存在或已终止 | 重新启动测试进程,获取新 PID 后重试 | |
| 执行 exec 命令报错(invalid argument) | 系统不支持 exec 替换进程 | 直接启动 Python 脚本并加入分组:python3 memory_test.py &,再将 PID 写入 cgroup.procs |
6.2 K8s 资源限制不生效排查步骤
- 检查 Pod 状态:确认 Pod 已正常运行(
kubectl get pods状态为 Running),未处于调度失败或重启状态; - 验证节点 cgroups 配置:进入节点,查看
/sys/fs/cgroup/memory/kubepods/目录下是否存在该 Pod 对应的分组,且memory.limit_in_bytes与配置一致; - 检查 kubelet 日志:通过
journalctl -u kubelet查看日志,排查是否有 cgroups 配置失败的报错(如权限不足、目录不存在); - 确认容器运行时支持:Docker、containerd 等运行时需启用 cgroups 支持,检查运行时配置文件(如
/etc/containerd/config.toml)中是否开启SystemdCgroup = true; - 排查资源重叠限制:若 Pod 同时配置了
limits和命名空间的ResourceQuota,需确保两者不冲突(limits不能超过ResourceQuota限制)。
总结
今天我们通过理论拆解、亲手实操和问题排查,彻底搞懂了 cgroups 这个 “资源管家” 的核心逻辑。它和命名空间一起,构成了容器技术的底层基石 —— 命名空间负责 “隔离”,cgroups 负责 “管控”。
绕开 Docker 直接操作 cgroups 的实验,揭示了容器资源限制的本质:没有复杂黑科技,只是对内核 cgroups 文件系统的简单配置。理解这一点后,你再排查 K8s Pod 资源超限、CPU 使用率异常等问题时,就能直击根源。
这就是本文的全部内容,也感谢耐心看到这里的读者,我会持续更新,希望你能够多多关注,如果本文有帮组到你的话,还请三连加关注,你的支持就是我创作的最大动力!

177

被折叠的 条评论
为什么被折叠?



