你有没有好奇过:Docker 容器是怎么做到限制一个程序只能用 1G 内存、只能用 20% CPU 的?

答案其实不在 Docker 本身,而在 Linux 操作系统里 —— 它叫 Cgroups(控制组)。今天这篇文章,我们完全从零开始,不用 Docker,纯手动操作,一步步带你亲眼见证:操作系统是怎么给进程 "上枷锁" 的。

全程命令可复制,每一步都告诉你在哪个窗口敲、敲什么、会看到什么。新手友好,放心食用。


1 战前准备:先认识两个神器工具

在正式玩 Cgroups 之前,我们需要两个帮手:一个用来 "搞破坏"(制造压力),一个用来 "看监控"(观察效果)。

1.1 stress:专业的系统压力制造机

stress 就是一个专门给系统添堵的工具。你想让 CPU 跑满?想让内存飙高?想让磁盘疯狂读写?它都能一键搞定。

1.1.1 安装 stress

根据你的系统选一条命令执行即可:

Ubuntu / Debian 系统:

apt remove stress -y   # 先卸载旧版(没有也没关系)
apt install stress -y  # 安装

CentOS / 红帽系系统:

yum remove stress -y   # 先卸载旧版
yum install stress -y  # 安装
1.1.2 几个核心参数(大白话版)
参数作用实际意思
-c N产生 N 个 CPU 压力进程让 N 个核跑满 100%
-m N产生 N 个内存压力进程不断申请释放内存
--vm-bytes B每个进程分配多大内存比如 50M 就是每个进程吃 50M
-t NN 秒后自动结束防止把系统玩崩了
-q安静模式,不输出信息眼不见心不烦

1.2 pidstat:进程资源监控小能手

pidstat 可以实时查看每个进程占用了多少 CPU、多少内存、多少 IO。我们用它来验证 "限制到底生没生效"。

1.2.1 安装 pidstat

它属于 sysstat 工具包:

Ubuntu / Debian 系统:

apt remove sysstat -y
apt install sysstat -y

CentOS / 红帽系系统:

yum remove sysstat -y
yum install sysstat -y
1.2.2 常用参数(大白话版)
参数作用
-u看 CPU 使用情况
-r看内存使用情况
-C 进程名只看叫这个名字的进程
-p ALL显示所有进程

举个例子:

pidstat -u -C stress -p ALL 1 3

意思是:每秒刷新一次,总共刷 3 次,只看名字叫 stress 的进程的 CPU 占用。


2 初识 Cgroups:先看看系统里都有啥

Cgroups 不是什么神秘软件,它本质上就是 Linux 内核里的一套功能,以文件系统的形式暴露给用户。说白了,你改改文件内容,就能限制资源。

2.1 确认你的系统支持 Cgroups

操作窗口:终端窗口 A

执行命令:

cat /proc/filesystems | grep cgroup

预期看到的效果:

nodev	cgroup
nodev	cgroup2

看到 cgroup2 就说明你的系统支持第二版 Cgroups,没问题,继续往下走。

2.2 看看 Cgroups 能控制哪些资源

Cgroups 是按 "子系统" 分工的,每个子系统管一类资源。

操作窗口:终端窗口 A

执行命令:

cat /proc/cgroups

预期看到的效果(节选关键部分):

#subsys_name	hierarchy	num_cgroups	enabled
cpuset		8		2		1
cpu		2		100		1
cpuacct		2		100		1
blkio		6		98		1
memory		10		124		1
devices		12		98		1
freezer		7		2		1
pids		5		107		1

挑几个最常用的给你翻译一下:

  • cpu:管 CPU 使用时间
  • memory:管内存使用量
  • blkio:管磁盘 IO 速度
  • pids:管进程数量上限
  • freezer:可以冻结 / 解冻一整组进程

2.3 Cgroups 文件系统挂在哪

所有的配置文件都在 /sys/fs/cgroup 目录下。

操作窗口:终端窗口 A

执行命令:

mount | grep cgroup

预期看到的效果(节选):

cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup
cgroup on /sys/fs/cgroup/memory type cgroup
cgroup on /sys/fs/cgroup/blkio type cgroup
...

记住这个目录:/sys/fs/cgroup。接下来所有操作,都是在这个目录下建文件夹、改文件。

2.4 怎么看一个进程属于哪个控制组

每个进程都有自己的 Cgroups 归属,我们可以查看当前 shell 进程的归属。

操作窗口:终端窗口 A

执行命令:

cat /proc/$$/cgroup

小知识:$$ 在 shell 里代表 "当前进程的 PID"。

预期看到的效果:

11:hugetlb:/
10:memory:/user.slice
9:freezer:/
8:cpuset:/
7:perf_event:/
...
3:cpuacct,cpu:/user.slice
...

意思就是:当前这个 shell 进程,在内存子系统里属于 /user.slice 这个组,在 CPU 子系统里也属于 /user.slice 这个组。系统默认已经帮我们分好组了。


3 实战一:手动限制进程内存使用量

重头戏来了。我们现在要亲手创建一个 "内存控制组",限制里面的进程最多只能用 20M 内存,然后放一个想吃 50M 的进程进去,看看会发生什么。

3.1 第一步:创建内存控制组

Cgroups 的规则很简单:建个目录,系统就自动生成配置文件。

操作窗口:终端窗口 A(操作窗口)

# 进入内存子系统目录
cd /sys/fs/cgroup/memory

# 创建一个叫 test_memory 的控制组(就是建个文件夹)
mkdir test_memory

# 进去看看里面有啥
ll test_memory

预期看到的效果:

drwxr-xr-x 2 root root 0 ... ./
dr-xr-xr-x 7 root root 0 ... ../
-rw-r--r-- 1 root root 0 ... cgroup.procs
-rw-r--r-- 1 root root 0 ... memory.limit_in_bytes
-rw-r--r-- 1 root root 0 ... memory.usage_in_bytes
-rw-r--r-- 1 root root 0 ... memory.failcnt
...(还有一堆文件)

神奇吧?我们只是建了个空文件夹,系统自动生成了几十个配置文件。核心就两个:

  • memory.limit_in_bytes:内存上限,往里面写数字就是设限制
  • tasks:把进程 PID 写进去,这个进程就归这个组管了

3.2 第二步:设置内存上限为 20M

我们限制这个组最多使用 20MB 内存。20M 换算成字节是 20 * 1024 * 1024 = 20971520。

操作窗口:终端窗口 A

# 先算一下 20M 等于多少字节(可选,也可以直接写数字)
expr 20 \* 1024 \* 1024
# 输出:20971520

# 设置内存上限
echo "20971520" > test_memory/memory.limit_in_bytes

# 验证一下设置成功没
cat test_memory/memory.limit_in_bytes

预期看到的效果:

20971520

限制已经设好了。现在这个组里的所有进程,加起来内存不能超过 20M,超过了就会触发 OOM(内存不足)被杀掉。

3.3 第三步:启动一个吃内存的进程

我们用 stress 启动一个进程,让它尝试占用 50M 内存。

操作窗口:终端窗口 B(压力窗口,保持运行不要关)

stress -m 1 --vm-bytes 50M

预期看到的效果:

stress: info: [xxxxx] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd

程序正常运行中,没有报错。因为现在它还没被加入我们的限制组,想吃多少吃多少。

3.4 第四步:观察当前内存占用

在加限制之前,先看看它正常吃内存是什么样的。

操作窗口:终端窗口 C(监控窗口)

pidstat -r -C stress -p ALL 1 5

参数说明:-r 看内存,-C stress 只看 stress 进程,每秒刷新一次,刷 5 次。

预期看到的效果(关注 RSS 列,单位 KB):

02:47:02 PM   UID       PID  minflt/s  majflt/s     VSZ     RSS   %MEM  Command
02:47:03 PM     0     62518  483459.00      0.00   55060   15156   0.75  stress

可以看到 RSS(实际物理内存)大概在 15000 KB 左右,也就是约 15M,并且稳定运行。

3.5 第五步:把进程丢进限制组,见证奇迹

现在我们把这个 stress 工作进程的 PID,写入 tasks 文件,让它受 20M 限制的管控。

注意:stress 启动后会有两个进程 —— 一个主进程(管理用),一个工作进程(真正吃内存的)。我们要限制的是工作进程。从上面 pidstat 输出里找到那个 RSS 大的 PID,比如 62518。

操作窗口:终端窗口 A(操作窗口)

# 进入目录
cd /sys/fs/cgroup/memory

# 把工作进程 PID 写入 tasks 文件
echo 62518 >> test_memory/tasks

写入之后,立刻观察两个窗口的变化:

窗口 B(压力窗口)预期效果:

stress: FAIL: [62517] (415) <-- worker 62518 got signal 9
stress: WARN: [62517] (417) now reaping child worker processes
stress: FAIL: [62517] (451) failed run completed in 176s

进程收到了 9 号信号(SIGKILL),直接被杀死了!

窗口 C(监控窗口)预期效果: 再过几秒刷新,你会发现 PID 为 62518 的进程消失了,只剩下主进程。

这就是 Cgroups 的威力: 进程试图申请超过限制的内存,内核直接把它干掉,绝不手软。Docker 里的容器内存溢出被杀,底层就是这个机制。


4 实战二:手动限制进程 CPU 使用率

内存控制看完了,我们再来玩 CPU 控制。这次我们让一个本来跑满 100% CPU 的进程,被限制到只能用 30%。

4.1 第一步:创建 CPU 控制组

和内存一样,先建文件夹。

操作窗口:终端窗口 A(操作窗口)

# 进入 CPU 子系统目录
cd /sys/fs/cgroup/cpu

# 创建 test_cpu 控制组
mkdir test_cpu

# 看看自动生成了哪些文件
ll test_cpu

预期看到的关键文件:

  • cpu.cfs_period_us:一个调度周期的长度,默认 100000 微秒(也就是 100ms)
  • cpu.cfs_quota_us:这个组在一个周期内能用多少微秒的 CPU 时间
  • tasks:还是老规矩,写 PID 进去就归这个组管

4.2 第二步:理解 CPU 限制的计算公式

这是最核心的原理,一句话讲明白:

CPU 使用率上限 = cpu.cfs_quota_us/cpu.cfs_period_us

  • cpu.cfs_period_us 默认是 100000(100 毫秒)
  • cpu.cfs_quota_us 默认是 -1(不限制)

如果我们想限制到 30%,那就是: 100000 × 30% = 30000 微秒

所以把 cpu.cfs_quota_us 设为 30000 就行。

4.3 第三步:设置 CPU 使用率上限为 30%

操作窗口:终端窗口 A

# 设置配额为 30000 微秒
echo 30000 > test_cpu/cpu.cfs_quota_us

# 验证一下
cat test_cpu/cpu.cfs_quota_us

预期输出:

30000

4.4 第四步:启动一个跑满 CPU 的进程

用 stress 让一个 CPU 核跑满。

操作窗口:终端窗口 B(压力窗口,保持运行)

stress -c 1

预期看到的效果:

stress: info: [62576] dispatching hogs: 1 cpu, 0 io, 0 vm, 0 hdd

程序正常运行,此时这个进程应该占满一个 CPU 核心。

4.5 第五步:观察限制前的 CPU 占用

操作窗口:终端窗口 C(监控窗口)

pidstat -u -C stress -p ALL 1 5

预期看到的效果:

02:59:40 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
02:59:40 PM     0     62577   99.00    0.00    0.00    1.00   99.00     0  stress

可以看到 %CPU 接近 100%,一个核已经跑满了。记下这个工作进程的 PID(比如 62577)。

4.6 第六步:加入限制组,CPU 立刻 "降速"

操作窗口:终端窗口 A(操作窗口)

cd /sys/fs/cgroup/cpu
echo 62577 > test_cpu/tasks

写入之后,立刻看监控窗口。

窗口 C(监控窗口)预期效果:

03:12:53 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
03:12:53 PM     0     62577   30.00    0.00    0.00   70.00   30.00     0  stress

见证奇迹的时刻!CPU 使用率从接近 100% 直接掉到了 30% 左右,并且稳定维持在这个水平。

内核会严格按照你设定的配额分配时间片:每 100 毫秒里,只给这个进程 30 毫秒的运行时间,剩下 70 毫秒让它等着。所以你看到 %wait 变成了 70% 左右 —— 大部分时间在排队等 CPU。


5 恍然大悟:Docker 和 Cgroups 是什么关系

做完这两个实验,你应该能品出味儿来了。

Docker 所谓的 "容器资源限制",本质上就是:

  1. 帮你在 /sys/fs/cgroup 下面自动建文件夹
  2. 帮你把 memory.limit_in_bytes、cpu.cfs_quota_us 这些值算好写进去
  3. 帮你把容器里的进程 PID 自动写入 tasks 文件

你手动敲三四条命令完成的事,Docker 一条 docker run --memory=1g --cpus=0.3 就帮你做完了。

所以记住一句话:

真正做资源限制的不是 Docker,是 Linux 内核的 Cgroups。Docker 只是个封装好了的 "快捷操作面板"。

6 全文总结

今天我们用最朴素的方式,手动操作了 Linux Cgroups 的两大核心功能:

  1. 内存限制:修改 memory.limit_in_bytes,超了就 OOM 杀进程
  2. CPU 限制:通过 cfs_quota_us / cfs_period_us 控制使用率上限

核心操作流程永远是三步:

  • 建目录 → 创建一个控制组
  • 改文件 → 设置资源限制值
  • 写 tasks → 把进程丢进去接受管控

理解了 Cgroups,你再去学 Docker、K8s 的资源限制,就会发现:哦,原来都是换汤不换药,底层就这点东西。

希望这篇手把手的实战文章,能帮你捅破 Linux 资源控制的窗户纸。

更多推荐