1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com

上一篇我们讲了 ulimit、limits.conf 和 systemd Limit*。这些东西可以解决很多传统的资源限制问题,比如“最多打开多少文件”“最多创建多少进程”“最多使用多少 CPU 时间”。但是,如果现在遇到的是另外一种情况呢?

一台服务器上同时跑着 Nginx、MySQL、Redis、监控程序,还有几个业务服务。

某天晚上,一个业务程序突然抽风了。

CPU 开始疯狂上涨,内存不断申请,子进程一个接一个地创建,最后整台服务器的资源被它吃得差不多了。

最惨的地方还不是这个程序自己挂了。而是:

它自己有问题,却把服务器上其他正常运行的业务也一起拖死了。

这时候,ulimit 往往就有点不够用了。

这也是今天我们要讲的主角:> Linux cgroup v2。

如果把 ulimit 理解成“给一个进程规定一些资源使用规则”,那么 cgroup 更像是:

直接给一整个业务划一个资源笼子。 不管这个业务里面有 1 个进程,还是 100 个进程,只要它们属于同一个 cgroup,就可以统一进行资源管理。今天我们就来看看,Linux 是怎么把一个“疯狂吃资源的烂进程”,关进这个笼子里的。


先想一个真实生产环境的问题

我们假设有一台生产服务器:

flowchart TB
    SERVER["生产服务器<br/>16 CPU / 32 GB RAM"]

    SERVER --> WEB["Web 服务"]
    SERVER --> DB["数据库"]
    SERVER --> MON["监控服务"]
    SERVER --> APP["业务应用"]

    APP --> BUG["程序出现 Bug"]
    BUG --> CPU["CPU 持续飙升"]
    BUG --> MEM["不断申请内存"]
    BUG --> PROC["不断创建进程"]

正常情况下,大家相安无事。Web 服务处理 Web 请求。数据库负责数据库。监控程序负责监控。业务程序负责自己的业务。

但是某一天,业务程序出现了 Bug。比如代码里面出现了类似这样的逻辑:

1
2
3
4
while true:
创建任务
申请内存
done

于是它开始疯狂消耗资源。如果服务器没有任何隔离:

flowchart LR
    APP["异常业务"] --> CPU["疯狂占 CPU"]
    APP --> MEM["疯狂吃内存"]
    APP --> TASK["疯狂创建进程"]

    CPU --> HOST["整台服务器资源紧张"]
    MEM --> HOST
    TASK --> HOST

    HOST --> WEB["Web 服务受影响"]
    HOST --> DB["数据库受影响"]
    HOST --> MON["监控服务受影响"]

这就是生产环境特别讨厌的一种情况:

一个业务出问题,最后变成整台服务器出问题。

所以我们真正想要的不是:

“这个程序绝对不能使用资源。”

而是:

“你可以使用资源,但是我得给你画一条线。”

比如:

1
2
3
CPU:最多 2 个 CPU
内存:最多 4 GB
进程/线程:最多 500 个

超过以后,你自己的业务自己承担后果。**别把隔壁 MySQL 一起拖下水。**这就是 cgroup 要解决的问题。


什么是 cgroup?

cgroup,全称是:Control Groups 中文一般叫:控制组 它是 Linux 内核提供的一套资源管理机制。简单来说:

可以把一批进程放进同一个组,然后统一对这个组进行资源控制和统计。

这里最重要的是:

cgroup 管的不是“某一个 PID”,而是一组进程。

比如:

flowchart TB
    CG["app.service cgroup"]

    CG --> P1["PID 1001<br/>主进程"]
    CG --> P2["PID 1002<br/>Worker"]
    CG --> P3["PID 1003<br/>Worker"]
    CG --> P4["PID 1004<br/>Worker"]
    CG --> P5["PID 1005<br/>Worker"]

假设这个业务突然从:

1
1 个进程

变成:

1
100 个进程

只要这些进程都属于同一个 cgroup,我们仍然可以从整个业务组的角度进行资源管理。这就是它和单纯 ulimit 的一个重要区别。


cgroup 到底能管什么?

cgroup 可以管理很多资源。比较常见的包括:

控制器主要作用
cpuCPU 调度、CPU 使用量统计
memory内存使用限制和统计
io块设备 I/O 控制
pids限制进程/线程数量
cpusetCPU 核心和 NUMA 节点控制
rdmaRDMA 相关资源控制
perf_event与性能监控相关的资源分组

如果是做 Linux 运维,前面几个尤其重要:

1
2
3
4
CPU
内存
IO
进程/线程数量

也就是说,我们可以给一个业务设置:

1
2
3
4
最多使用多少 CPU
最多使用多少内存
最多创建多少进程
磁盘 IO 资源怎么分配

cgroup v1 和 cgroup v2 到底有什么区别?

这个地方一定要注意。因为你在网上搜索 cgroup 的时候,会看到大量老文章。然后里面可能是:

1
/sys/fs/cgroup/cpu/

或者:

1
/sys/fs/cgroup/memory/

再或者一大堆:

1
2
cgcreate
cgexec

这些很多都是基于 cgroup v1 的资料。而现代Linux环境使用的是 cgroup v2。cgroup v2 和 v1 最大的变化之一,就是:

统一层级模型。

可以简单理解成:

flowchart TB
    ROOT["cgroup v2<br/>统一层级"]

    ROOT --> CPU["CPU"]
    ROOT --> MEM["Memory"]
    ROOT --> IO["IO"]
    ROOT --> PIDS["PIDs"]
    ROOT --> CPUSET["cpuset"]

    ROOT --> APP["业务 cgroup"]

    APP --> ACPU["CPU 控制"]
    APP --> AMEM["Memory 控制"]
    APP --> AIO["IO 控制"]
    APP --> APIDS["PIDs 控制"]

所有控制器都工作在同一套 cgroup 层级里面。所以:

以后看到网上特别老的 cgroup v1 教程,先别急着复制粘贴。

尤其是生产服务器。先确认:

1
2
[root@localhost ~]# mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot)

或者:

1
2
3
[root@localhost ~]# stat -fc %T /sys/fs/cgroup
cgroup2fs

很容易能看到当前使用的是 cgroup v2。


cgroup 和 systemd 到底是什么关系?

这个问题非常重要。很多人第一次学习 cgroup 的时候,会直接钻进:

1
/sys/fs/cgroup

然后开始:

1
2
3
4
mkdir
echo
echo
echo

最后把自己绕晕。其实如果你的服务器使用 systemd 管理服务,大多数情况下根本没有必要天天手动操作 /sys/fs/cgroup。因为:

systemd 本身就是 cgroup 的一个非常重要的管理者。

可以把它们理解成:

flowchart TB
    SD["systemd"]

    SD --> CG["cgroup v2"]

    CG --> SERVICE["service"]
    CG --> SCOPE["scope"]
    CG --> SLICE["slice"]

    SERVICE --> HTTPD["httpd.service"]
    SERVICE --> MYSQL["mysqld.service"]
    SERVICE --> SSHD["sshd.service"]

    SCOPE --> SSH["SSH 会话"]
    SCOPE --> OTHER["其他外部进程"]

    SLICE --> SYSTEM["system.slice"]
    SLICE --> USER["user.slice"]
    SLICE --> MACHINE["machine.slice"]

所以你可以把 systemd 想象成一个物业管理公司。cgroup 是物业手里的“资源管理系统”。而:

1
2
3
service
scope
slice

就是物业用来给不同业务分组、分房间的方式。


service、scope、slice 到底是什么?

刚才看起来有点抽象。我们分别来看。

1. service

这个最好理解。

比如:

1
2
3
4
httpd.service
sshd.service
mysqld.service
redis.service

这些都是 systemd 管理的服务。比如:

1
systemctl status httpd

看到的就是一个 service。


2. scope

scope 可以简单理解成:

不是 systemd 自己启动的,但是 systemd 可以把它纳入管理的进程组。

例如一些用户会话、外部创建的进程等。


3. slice

slice 更有意思。它不是具体的业务进程,而是:

用来给其他 service、scope 做分组的。

比如:

flowchart TB
    ROOT["systemd"]

    ROOT --> SYSTEM["system.slice"]
    ROOT --> USER["user.slice"]
    ROOT --> MACHINE["machine.slice"]

    SYSTEM --> HTTPD["httpd.service"]
    SYSTEM --> MYSQL["mysqld.service"]
    SYSTEM --> REDIS["redis.service"]

    USER --> U1000["user-1000.slice"]
    U1000 --> SESSION["session-1.scope"]

    MACHINE --> VM["虚拟机"]
    MACHINE --> CONTAINER["容器"]

怎么看服务器现在的 cgroup?

这里开始进入实战。

以后遇到:

“这个进程到底属于哪个 cgroup?”

先别猜,直接看。


使用 systemd-cgls 看完整层级

执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[root@localhost ~]# systemd-cgls
CGroup /:
-.slice
├─user.slice
│ └─user-0.slice
│ ├─session-3.scope
│ │ ├─3677 sshd-session: root [priv]
│ │ ├─4144 sshd-session: root@notty
│ │ └─4195 /usr/libexec/openssh/sftp-server
│ ├─session-1.scope
│ │ ├─3673 sshd-session: root [priv]
│ │ ├─4119 sshd-session: root@pts/0
│ │ ├─4738 -bash
│ │ ├─4789 systemd-cgls
│ │ └─4790 less
│ └─user@0.service …
│ └─init.scope
│ ├─3741 /usr/lib/systemd/systemd --user
│ └─3777 (sd-pam)
├─init.scope
│ └─1 /usr/lib/systemd/systemd --switched-root --system --deserialize=51
└─system.slice
├─irqbalance.service
│ └─1002 /usr/sbin/irqbalance
├─systemd-udevd.service …
│ └─udev
│ └─887 /usr/lib/systemd/systemd-udevd
├─dbus-broker.service
│ ├─991 /usr/bin/dbus-broker-launch --scope system --audit
│ └─998 dbus-broker --log 4 --controller 9 --machine-id 227d3258119347278234251bddccb33a --max-bytes 536870912 --max-fds 4096 --max-matches 131072 --audit
├─polkit.service
│ └─1175 /usr/lib/polkit-1/polkitd --no-debug --log-level=err
├─chronyd.service
│ └─999 /usr/sbin/chronyd -n -F 2
├─pmproxy.service
│ └─1537 /usr/libexec/pcp/bin/pmproxy -F -A
├─auditd.service
│ ├─978 /usr/sbin/auditd
│ └─980 /usr/sbin/sedispatch
├─rasdaemon.service
│ └─1003 /usr/sbin/rasdaemon -f -r
├─tuned.service
│ └─1205 /usr/bin/python3 -Es /usr/sbin/tuned -l -P

这个东西特别适合排查:

一个进程到底属于谁?

比如假设PID 12002特别能吃 CPU。你可以沿着树找到:

1
httpd.service

这时候就知道:

原来这个进程属于 httpd.service。


systemd-cgtop:cgroup 版本的 top

如果:

1
systemd-cgls

解决的是:

“你是谁?”

那么:

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# systemd-cgtop
CGroup Tasks %CPU Memory Input/s Output/s
/ 424 - 715M - -
dev-hugepages.mount - - 348K - -
dev-mqueue.mount - - 20K - -
init.scope 1 - 55.6M - -
sys-fs-fuse-connections.mount - - 8K - -
sys-kernel-config.mount - - 4K - -
sys-kernel-debug.mount - - 4K - -
sys-kernel-tracing.mount - - 4K - -

解决的就是:

“你吃了多少资源?”

这时候就非常直观了。

如果某一天你看到:

1
myapp.service     18G

那就不用猜了。

这哥们儿可能正在干大事。

当然,也可能只是业务本身就是吃内存的,所以生产环境一定要结合业务情况分析。


直接看某个 PID 属于哪个 cgroup

比如:

1
2
[root@localhost ~]# cat /proc/3673/cgroup
0::/user.slice/user-0.slice/session-1.scope

cgroup v2 可能看到:

1
2
[root@localhost ~]# cat /proc/3673/cgroup
0::/user.slice/user-0.slice/session-1.scope

这句话非常重要。它告诉你:

部分含义
0cgroup v2 的统一层级标识,0 表示使用统一层级
::cgroup v2 的格式分隔符
user.slice属于 systemd 的用户会话资源分组
user-0.sliceUID 为 0 的用户,也就是 root 用户
session-1.scoperoot 用户的第 1 个登录会话对应的 scope

cgroup 资源控制有两种思路

这里是今天非常重要的一部分。cgroup 里面的资源控制,大体可以理解成两种思路:

第一种:资源权重

意思是:

资源不够的时候,大家按照优先级分。

例如:

1
2
业务 A:CPUWeight=100
业务 B:CPUWeight=200

当 CPU 很空闲的时候:

1
2
A:想吃多少吃多少
B:想吃多少吃多少

但是 CPU 真不够用了:

1
2
A:100
B:200

B 获得的 CPU 调度份额更高。可以把它想象成食堂:

饭管够的时候,大家随便吃。

饭不够了,A 有 1 张饭票,B 有 2 张饭票,那就按照比例分。


第二种:资源硬限制

硬限制就完全不一样了。它相当于:

不管服务器现在多富裕,你最多只能吃这么多。

例如:

1
2
3
CPUQuota=200%
MemoryMax=4G
TasksMax=500

意思可以理解成:

1
2
3
4
5
6
7
8
CPU
最多 2 个 CPU

Memory
最多 4GB

Tasks
最多 500 个进程/线程

所以:

flowchart LR
    APP["业务应用"]

    APP --> CPU["CPUQuota=200%"]
    APP --> MEM["MemoryMax=4G"]
    APP --> TASK["TasksMax=500"]

    CPU --> C["CPU 使用上限"]
    MEM --> M["内存使用上限"]
    TASK --> T["进程 / 线程数量上限"]

一句话:

Weight 是“资源不够的时候怎么分”。

Quota / Max 是“你最多能用多少”。

这个区别一定要记住。


systemd 常用资源控制参数

生产环境中,最常见的一批参数可以先记住:

参数类型作用
CPUWeight=权重CPU 资源竞争时的相对权重
CPUQuota=限制CPU 使用上限
MemoryMax=限制内存使用上限
TasksMax=限制进程/线程数量上限
IOWeight=权重I/O 竞争时的相对权重
AllowedCPUs=限制限定允许使用的 CPU
AllowedMemoryNodes=限制限定允许使用的 NUMA 内存节点

其中几个最常用的,可以重点记:

1
2
3
4
CPUWeight
CPUQuota
MemoryMax
TasksMax

CPUQuota=100% 到底是多少 CPU?

这个地方非常容易被误解。

比如:

1
CPUQuota=100%

通常可以理解成:

最多使用一个 CPU 的计算时间。

如果:

1
CPUQuota=200%

就是:

最多使用两个 CPU 的计算时间。

例如服务器有:

1
16 CPU

配置:

1
CPUQuota=200%

并不是:

“占整台服务器 CPU 的 200%。”

而是:

这个服务最多消耗相当于两个 CPU 的计算资源。

所以:

1
2
3
100% ≈ 1 CPU
200% ≈ 2 CPU
400% ≈ 4 CPU

理解这个以后,后面配置就不会那么容易懵。


实战一:给 httpd 加资源限制

现在来做一个真正的生产案例。假设服务器运行httpd:

1
[root@localhost ~]# dnf install httpd -y

来启动服务

1
[root@localhost ~]# systemctl enable httpd --now

我们担心 Web 服务出现异常以后:

  • CPU 把服务器吃满
  • 内存无限增长
  • 子进程疯狂创建

于是我们决定给它设置:

1
2
3
CPU:最多 1 个 CPU
内存:最多 512 MB
任务数量:最多 200

可以使用:

1
2
3
CPUQuota=100%
MemoryMax=512M
TasksMax=200

注意,不要修改软件自带的配置文件,软件升级的适合,会覆盖,我们要用Linux里的drop-in

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# mkdir -p /etc/systemd/system/httpd.service.d/
[root@localhost ~]# cat > /etc/systemd/system/httpd.service.d/resource-limit.conf <<-EOF
[Service]

# CPU 最多使用 1 个 CPU
CPUQuota=100%

# 内存最大 512 MB
MemoryMax=512M

# httpd.service 所属 cgroup 最多允许 200 个任务
TasksMax=200
EOF

配置文件写完以后,systemd 还不知道我们修改了配置,来reload一下

1
[root@localhost ~]# systemctl daemon-reload

这个命令的作用是:

告诉 systemd:哥们儿,我刚刚改配置了,你重新看一遍。

注意:

1
[root@localhost ~]# systemctl daemon-reload

不是重启 httpd。

它只是让 systemd 重新加载配置。所以接下来还需要:

1
[root@localhost ~]# systemctl restart httpd

看看生效没:

1
2
3
4
5
6
[root@localhost ~]# systemctl show httpd -p CPUQuotaPerSecUSec
CPUQuotaPerSecUSec=1s
[root@localhost ~]# systemctl show httpd -p MemoryMax
MemoryMax=536870912
[root@localhost ~]# systemctl show httpd -p TasksMax
TasksMax=200

CPUQuotaPerSecUSec=1s这里看起来有点奇怪,但是是生效了的,这里显示的是systemd 内部换算后的形式。它这里说的是每 1 秒最多使用 1 秒的 CPU 时间,相当于最多使用 1 个 CPU 核心

现在我们的关系就变成:

flowchart TB
    HTTPD["httpd.service"]

    HTTPD --> CPU["CPUQuota=100%"]
    HTTPD --> MEM["MemoryMax=512M"]
    HTTPD --> TASK["TasksMax=200"]

    CPU --> C["最多约 1 CPU"]
    MEM --> M["最多 512 MB"]
    TASK --> T["最多 200 个任务"]

实战二:运行时临时修改资源限制

有时候你正在生产环境排查问题。不想马上编辑文件。只是想:

“先把这个服务限制一下看看。”

这时候可以使用:

1
systemctl set-property

比如:

1
2
3
4
[root@localhost ~]# systemctl set-property httpd.service MemoryMax=256M
[root@localhost ~]#
[root@localhost ~]# systemctl show httpd -p MemoryMax
MemoryMax=268435456

这个方式可以直接修改 systemd 单元的资源属性。如果只是想临时测试,不希望修改持久配置,可以:

1
systemctl set-property --runtime httpd.service MemoryMax=256M

区别可以简单记:

1
2
3
4
5
6
7
set-property
↓
修改资源属性

--runtime
↓
只针对当前运行状态

重启或者重新加载持久配置以后,这个临时设置不会作为永久配置保留下来。

这个方法非常适合:

线上排查问题的时候先试一下。

确认效果没问题以后,再把配置正式写入 drop-in。


实战三:我就想临时跑个命令,怎么办?

还有一种场景:你只是想测试一个程序。

比如:

1
sleep 10d

你不想为了它创建:

1
xxx.service

也不想写配置文件。那怎么办?可以使用:

1
systemd-run

例如:

1
2
3
4
5
systemd-run \
--unit=test-cmd \
--slice=example.slice \
--property=CPUQuota=10% \
sleep 10d

这里发生了什么?

flowchart LR
    CMD["systemd-run"]

    CMD --> UNIT["test-cmd.service<br/>瞬态单元"]
    UNIT --> SLICE["example.slice"]
    SLICE --> LIMIT["CPUQuota=10%"]
    LIMIT --> PROCESS["sleep 10d"]

也就是说:

systemd 临时给你创建了一个 unit。

这个命令结束以后,对应的瞬态单元也会被清理。

特别适合:

1
2
3
4
临时测试
压力测试
脚本测试
资源限制验证

实战四:把多个业务放进一个 slice

如果服务器上的业务越来越多:

1
2
3
4
app01
app02
app03
app04

你可能会想:

“我能不能把这几个业务统一限制?”

当然可以。这时候就轮到:

slice

出场了。比如我们创建:

1
biz-app.slice

现在来创建 slice:

1
/etc/systemd/system/biz-app.slice

内容:

1
2
3
4
5
6
[Unit]
Description=业务应用统一资源分组

[Slice]
CPUQuota=200%
MemoryMax=2G

现在这个 slice 的意思就是:

这一整个业务分组最多使用 2 个 CPU 和 2GB 内存。


让业务服务加入这个 slice,假设我们有:

1
myapp.service

创建:

1
/etc/systemd/system/myapp.service.d/10-slice.conf

内容:

1
2
[Service]
Slice=biz-app.slice

然后:

1
2
systemctl daemon-reload
systemctl restart myapp.service

现在关系变成:

flowchart TB
    SLICE["biz-app.slice<br/>CPUQuota=200%<br/>MemoryMax=2G"]

    SLICE --> APP1["myapp.service"]
    SLICE --> APP2["app-worker.service"]
    SLICE --> APP3["app-api.service"]

    APP1 --> P1["进程"]
    APP2 --> P2["进程"]
    APP3 --> P3["进程"]

这样就可以把多个业务统一纳入一个资源分组。


为什么 slice 特别适合企业环境?

想象一下。一台服务器上可能有:

1
2
3
4
5
订单系统
支付系统
日志系统
监控系统
批处理系统

如果每个业务都单独配置一堆限制,时间长了配置会非常散。这时候可以设计:

flowchart TB
    ROOT["服务器"]

    ROOT --> BIZ["biz.slice"]

    BIZ --> ORDER["order.slice"]
    BIZ --> PAY["payment.slice"]
    BIZ --> LOG["logging.slice"]

    ORDER --> O1["order-api.service"]
    ORDER --> O2["order-worker.service"]

    PAY --> P1["payment-api.service"]
    PAY --> P2["payment-worker.service"]

    LOG --> L1["fluent-bit.service"]
    LOG --> L2["log-process.service"]

然后:

1
biz.slice

控制整个业务域。下面再细分:

1
2
3
order.slice
payment.slice
logging.slice

这就开始有点企业资源隔离的味道了。


父 slice 和子 slice

这里再强调一个概念。slice 本身可以形成层级。

例如:

1
2
3
4
biz.slice
└── order.slice
├── order-api.service
└── order-worker.service

父级可以定义资源约束,子级可以进一步设置自己的资源策略。你可以把它理解成公司组织架构:

1
2
3
4
5
集团
└── 技术中心
├── Web团队
├── 数据库团队
└── 运维团队

集团先给技术中心一个总预算。下面的团队再分别分配预算。资源管理的思路也是类似的。


MemoryMax 到底意味着什么?

这个参数值得单独讲一下。

例如:

1
MemoryMax=512M

它表示:

这个 cgroup 的内存使用上限是 512MB。

注意:

它限制的是:

cgroup

而不是简单理解成:

“某一个 PID 最多 512MB。”

例如:

flowchart TB
    CG["myapp.service cgroup<br/>MemoryMax=512M"]

    CG --> P1["主进程"]
    CG --> P2["Worker 1"]
    CG --> P3["Worker 2"]
    CG --> P4["Worker 3"]

    P1 --> TOTAL["整个 cgroup 的内存使用"]
    P2 --> TOTAL
    P3 --> TOTAL
    P4 --> TOTAL

    TOTAL --> LIMIT["512 MB 上限"]

也就是说:

1
2
3
4
主进程 100M
Worker1 100M
Worker2 150M
Worker3 100M

总共:

1
450M

还没有超过限制。如果继续增长,超过 cgroup 的内存限制以后,就会进入 cgroup 的内存压力/OOM 处理路径,相关进程可能被终止。

所以:

MemoryMax 不是“内存超过以后给你报警”这么简单,而是真正的资源边界。

生产环境一定要谨慎设置。


TasksMax 是干什么的?

再来看:

1
TasksMax=200

这个参数非常适合防止:

进程/线程疯狂增长。

例如一个程序出了 Bug:

1
2
3
4
5
创建线程
创建线程
创建线程
创建线程
……

最后:

1
几千个线程

这时候不仅仅是 CPU 的问题。系统本身也可能受到影响。所以可以通过:

1
TasksMax=200

给这个服务设置任务数量上限。可以简单理解成:

你这个业务最多只能占这么多“工位”。


CPUWeight 和 CPUQuota 千万别搞混

这个地方再总结一次。假设:

1
CPUWeight=100

它不是:

“最多只能使用 100% CPU。”

这是一个非常常见的误解。

CPUWeight 是:

CPU 资源竞争时的相对权重。

而:

1
CPUQuota=100%

才是:

CPU 使用上限。

所以:

1
2
3
4
5
6
7
CPUWeight
↓
资源不够时,怎么分

CPUQuota
↓
最多能吃多少

用一句特别简单的话:

Weight 是饭不够的时候怎么分,Quota 是你最多吃几碗。


生产环境到底应该怎么配置?

现在我们把整个知识点串起来。假设发现一个业务:

1
2
3
CPU 飙高
内存暴涨
进程数量暴涨

不要第一反应:

1
2
3
CPUQuota=10%
MemoryMax=100M
TasksMax=10

然后:

“好了,资源调优完成!”

千万别这么干。正确思路应该是:

flowchart TB
    A["发现业务异常"]
    B["确认是哪种资源异常"]
    C["确认哪个服务 / 进程造成"]
    D["分析正常业务资源需求"]
    E{"选择控制方式"}

    A --> B
    B --> C
    C --> D
    D --> E

    E -->|登录会话| F["ulimit / pam_limits"]
    E -->|传统 POSIX 限制| G["systemd Limit*"]
    E -->|CPU / 内存 / IO / Tasks| H["cgroup v2"]

    F --> I["验证"]
    G --> I
    H --> I

    I --> J["观察业务"]
    J --> K["必要时调整"]

这才是生产环境真正应该做的事情。


几个常见的生产场景

场景一:Web 服务 CPU 经常打满

可以考虑:

1
CPUQuota=400%

例如:

给这个服务最多 4 个 CPU 的计算能力。

但到底设置 400%,还是 200%,还是不设置?不能拍脑袋。应该根据:

1
2
3
4
业务正常 CPU 使用量
峰值流量
服务器 CPU 核数
业务延迟要求

综合判断。


场景二:某个服务内存经常失控

可以考虑:

1
MemoryMax=4G

但是同样不能简单认为:

“给 4G 就完事了。”

如果业务正常情况下就需要:

1
3.8G

那你设置:

1
4G

实际上已经没有多少安全余量。


场景三:怀疑程序有 fork/线程失控问题

可以考虑:

1
TasksMax=500

这时候即使程序出现异常,也不至于无限制地创建任务。


场景四:多个业务需要统一限制

可以考虑:

1
slice

例如:

1
biz.slice

下面统一放:

1
2
3
order.service
payment.service
user.service

然后在 slice 层面进行统一资源管理。


不要轻易直接修改 /sys/fs/cgroup

你在网上可能会看到很多文章:

1
echo 100000000 > /sys/fs/cgroup/xxx/memory.max

这种操作并不是说完全不能用。Linux 本身就是通过这些 cgroup 文件提供底层控制接口。

但是:

如果服务器本身已经由 systemd 管理服务,通常更推荐通过 systemd 来管理。

也就是:

1
2
3
systemd
↓
cgroup v2

而不是:

1
手动修改 cgroup 文件

原因很简单。生产环境最怕:

1
2
3
4
今天改了
明天忘了
后天重启
然后发现配置没了

systemd unit + drop-in 的配置方式,更容易:

1
2
3
4
5
审计
维护
版本管理
排查
恢复

几个非常容易踩的坑

坑一:看到 cgroup v1 教程直接照抄

先确认自己的系统:

1
stat -fc %T /sys/fs/cgroup

如果:

1
cgroup2

那就按照 cgroup v2 的方式学习。


坑二:把 CPUWeight 当成 CPU 上限

错误理解:

1
CPUWeight=100

“最多只能使用 100% CPU。”

不是。它是权重。真正的 CPU 硬限制是:

1
CPUQuota=

坑三:只改配置,不验证

例如:

1
vim /etc/systemd/system/httpd.service.d/10-resource.conf

然后:

“好了。”

不行。至少检查:

1
systemctl show httpd -p MemoryMax

以及:

1
systemctl show httpd -p TasksMax

还可以:

1
systemctl status httpd

配合:

1
systemd-cgtop

观察实际效果。


坑四:给生产服务设置过低的 MemoryMax

例如一个业务平时需要:

1
800M

你突然:

1
MemoryMax=512M

然后业务开始 OOM。最后:

“奇怪,为什么系统调优以后业务挂了?”

这个锅可不能让老李背。资源限制本身没有问题,问题是限制值不能乱设置。


坑五:只限制 CPU,不管进程数量

有时候业务的问题根本不是 CPU。可能是:

1
2
3
fork 炸了
线程炸了
连接数炸了

所以真正排查的时候,要看:

1
2
3
4
CPU
Memory
Tasks
IO

而不是看到 CPU 高,就只配置:

1
CPUQuota=

一个比较完整的企业资源隔离案例

最后我们来做一个稍微完整一点的案例。

假设:

1
2
3
4
5
6
服务器:16 CPU / 32GB RAM

业务:
订单服务
支付服务
日志服务

现在我们希望:

1
2
3
4
5
6
7
订单 + 支付
↓
业务应用资源池

日志
↓
单独资源池

可以设计成:

flowchart TB
    HOST["生产服务器<br/>16 CPU / 32GB"]

    HOST --> BIZ["biz.slice<br/>业务应用资源池"]
    HOST --> LOG["logging.slice<br/>日志资源池"]

    BIZ --> ORDER["order.service"]
    BIZ --> PAY["payment.service"]

    LOG --> FLUENT["fluent-bit.service"]
    LOG --> LOGPROC["log-process.service"]

    BIZ --> BCPU["CPU / Memory 总边界"]
    LOG --> LCPU["CPU / Memory 总边界"]

例如:

1
2
3
4
5
# biz.slice

[Slice]
CPUQuota=800%
MemoryMax=16G

意思是:

订单和支付这整个业务资源池,最多使用约 8 个 CPU 和 16GB 内存。

而日志业务可以单独:

1
2
3
4
5
# logging.slice

[Slice]
CPUQuota=200%
MemoryMax=4G

这样即使日志处理程序突然抽风:

1
2
日志暴增
日志处理程序疯狂占资源

它也不会轻易把整个服务器吃干净。

这就是 cgroup 真正有价值的地方:

不是为了让某个程序跑得更快,而是为了让整个系统在某个程序出问题的时候,还能活下来。


cgroup 其实解决的是“资源边界”问题

学到这里,你会发现:

1
2
3
ulimit
systemd Limit*
cgroup v2

其实是逐渐往更完整的资源管理方向发展的。可以简单理解成:

flowchart LR
    U["ulimit<br/>当前 Shell / 进程限制"]
    P["pam_limits<br/>登录会话限制"]
    S["systemd Limit*<br/>POSIX Resource Limits"]
    C["cgroup v2<br/>服务级资源控制"]

    U --> P
    P --> S
    S --> C

但是这里不要理解成:

“后面的东西一定比前面的高级,所以前面的都不用了。”

不是这么回事。它们解决的问题不同。

比如:

1
我要限制打开文件数

可能:

1
LimitNOFILE=

就够了。如果:

1
我要限制一个业务最多使用多少内存

那就应该考虑:

1
MemoryMax=

如果:

1
我要把多个服务放进一个资源池

那么:

1
slice + cgroup

就更合适。


生产环境最佳实践

最后把今天的内容总结成几条真正能拿去工作的经验。

1. 先确认系统使用的是哪一代 cgroup

不要看到网上教程就复制。先看:

1
stat -fc %T /sys/fs/cgroup

2. systemd 管理的服务优先考虑 systemd

如果服务本身就是:

1
2
3
httpd.service
mysqld.service
redis.service

那么优先考虑:

1
systemd 的资源控制

而不是上来就手动修改:

1
/sys/fs/cgroup

3. Weight 和 Quota 是两回事

记住:

1
2
3
4
5
6
7
CPUWeight
↓
资源竞争时怎么分

CPUQuota
↓
最多能用多少

4. MemoryMax 是真正的资源边界

例如:

1
MemoryMax=4G

意味着这个 cgroup 的内存使用不能无限增长。超过限制以后,可能触发 cgroup 的内存回收和 OOM 处理。所以:

不要为了“防止程序吃内存”就随便给一个特别小的值。


5. TasksMax 很适合防止任务数量失控

如果怀疑某个业务存在:

1
2
fork 爆炸
线程爆炸

可以考虑:

1
TasksMax=

6. 多个业务需要统一管理,就考虑 slice

例如:

1
2
3
4
biz.slice
├── order.service
├── payment.service
└── user.service

统一做资源管理。


7. 修改以后一定验证

常用工具:

1
2
3
4
5
systemd-cgls
systemd-cgtop
systemctl show
systemctl status
cat /proc/<PID>/cgroup

不要只看配置文件。真正应该确认的是:

systemd 到底有没有加载?cgroup 到底有没有生效?业务实际表现怎么样?


最后总结

今天这篇其实就解决了一个问题:

如果服务器上某个业务突然变成“资源黑洞”,我们怎么把它关进笼子里?

答案就是:

1
cgroup v2

它可以把一批进程放到一个资源控制组里面,然后统一管理:

1
2
3
4
CPU
Memory
IO
Tasks

而在现代 Linux 服务器上,如果使用 systemd 管理服务,我们通常不需要天天手工操作 /sys/fs/cgroup。更常见的方式是:

flowchart TB
    SYSTEMD["systemd"]

    SYSTEMD --> SERVICE["service"]
    SYSTEMD --> SLICE["slice"]
    SYSTEMD --> SCOPE["scope"]

    SERVICE --> CONTROL["cgroup v2 资源控制"]
    SLICE --> CONTROL
    SCOPE --> CONTROL

    CONTROL --> CPU["CPU"]
    CONTROL --> MEMORY["Memory"]
    CONTROL --> IO["IO"]
    CONTROL --> TASKS["Tasks"]

然后根据实际需求选择:

1
2
3
4
5
6
7
CPUWeight
CPUQuota
MemoryMax
TasksMax
IOWeight
AllowedCPUs
AllowedMemoryNodes

最后再记住一个生产环境最重要的思路:

flowchart TB
    A["发现问题"] --> B["找到资源瓶颈"]
    B --> C["找到资源消耗者"]
    C --> D["分析正常资源需求"]
    D --> E["设置合理边界"]
    E --> F["验证 cgroup 是否生效"]
    F --> G["观察业务"]
    G --> H["持续调整"]

而不是:

1
2
3
4
5
6
7
8
9
10
11
12
13
发现 CPU 高
↓
CPUQuota=10%

发现内存高
↓
MemoryMax=100M

发现进程多
↓
TasksMax=10

然后祈祷业务别挂

这才是 Linux 性能调优和“乱改参数”最大的区别。 性能调优不是:

参数越大越好,也不是限制越狠越好。

真正的生产调优是:

先知道问题在哪里,再决定资源边界应该画在哪里。


参考手册

1
2
3
4
5
6
7
man systemctl
man systemd.resource-control
man systemd.slice
man systemd.scope
man systemd-run
man systemd-cgls
man systemd-cgtop