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

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优

上一篇我们讲了 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