三、告别资源争夺:Linux cgroups 如何实现容器资源管家的公平与公正?

前言

上一篇我们聊了 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 会执行以下操作:

  1. 在 /sys/fs/cgroup 下为容器创建专属 cgroups 分组;
  2. limits.cpu: "2" → cpu 子系统 cpu.cfs_quota_us=200000(每个 100ms 周期最多用 200ms CPU);
  3. limits.memory: "2Gi" → memory 子系统 memory.limit_in_bytes=2147483648
  4. 将容器主进程 PID 写入分组的 cgroup.procs 文件,完成资源限制绑定。

4.3 架构图解:K8s 到 cgroups 的资源管控链路

五、延伸思考:cgroups 的常见误区

  1. 内存限制不是 “弹性伸缩”:memory.limit_in_bytes 是硬限制,进程达限制后要么申请失败,要么被 OOM 杀死,不会自动扩容;
  2. CPU 份额不是绝对上限:cpu.shares 仅在 CPU 繁忙时生效,空闲时进程仍可使用 100% CPU;若需绝对限制,需用 cpu.cfs_quota_us
  3. cgroups 是进程组管理:一个分组可包含多个进程,限制规则对组内所有进程生效。

六、cgroups 常见问题排错指南

6.1 实验操作失败解决方案

问题现象可能原因解决方法
无法创建 cgroups 目录(Permission denied)未使用 root 权限执行命令前加 sudo,或切换到 root 用户(sudo -i
写入内存限制后不生效系统启用了 swap,未限制 memory.memsw.limit_in_bytes同时设置交换分区上限:`echo 104857600sudo tee .../memory.memsw.limit_in_bytes`
进程无法加入 cgroups 分组PID 不存在或已终止重新启动测试进程,获取新 PID 后重试
执行 exec 命令报错(invalid argument)系统不支持 exec 替换进程直接启动 Python 脚本并加入分组:python3 memory_test.py &,再将 PID 写入 cgroup.procs

6.2 K8s 资源限制不生效排查步骤

  1. 检查 Pod 状态:确认 Pod 已正常运行(kubectl get pods 状态为 Running),未处于调度失败或重启状态;
  2. 验证节点 cgroups 配置:进入节点,查看 /sys/fs/cgroup/memory/kubepods/ 目录下是否存在该 Pod 对应的分组,且 memory.limit_in_bytes 与配置一致;
  3. 检查 kubelet 日志:通过 journalctl -u kubelet 查看日志,排查是否有 cgroups 配置失败的报错(如权限不足、目录不存在);
  4. 确认容器运行时支持:Docker、containerd 等运行时需启用 cgroups 支持,检查运行时配置文件(如 /etc/containerd/config.toml)中是否开启 SystemdCgroup = true
  5. 排查资源重叠限制:若 Pod 同时配置了 limits 和命名空间的 ResourceQuota,需确保两者不冲突(limits 不能超过 ResourceQuota 限制)。

总结

今天我们通过理论拆解、亲手实操和问题排查,彻底搞懂了 cgroups 这个 “资源管家” 的核心逻辑。它和命名空间一起,构成了容器技术的底层基石 —— 命名空间负责 “隔离”,cgroups 负责 “管控”。

绕开 Docker 直接操作 cgroups 的实验,揭示了容器资源限制的本质:没有复杂黑科技,只是对内核 cgroups 文件系统的简单配置。理解这一点后,你再排查 K8s Pod 资源超限、CPU 使用率异常等问题时,就能直击根源。

        这就是本文的全部内容,也感谢耐心看到这里的读者,我会持续更新,希望你能够多多关注,如果本文有帮组到你的话,还请三连加关注,你的支持就是我创作的最大动力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值