Linux 性能调优系列(九): cgroup v2实战:防止一个烂进程干崩整台服务器
1 | 作者:李晓辉 |
上一篇我们讲了 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 | while true: |
于是它开始疯狂消耗资源。如果服务器没有任何隔离:
flowchart LR
APP["异常业务"] --> CPU["疯狂占 CPU"]
APP --> MEM["疯狂吃内存"]
APP --> TASK["疯狂创建进程"]
CPU --> HOST["整台服务器资源紧张"]
MEM --> HOST
TASK --> HOST
HOST --> WEB["Web 服务受影响"]
HOST --> DB["数据库受影响"]
HOST --> MON["监控服务受影响"]这就是生产环境特别讨厌的一种情况:
一个业务出问题,最后变成整台服务器出问题。
所以我们真正想要的不是:
“这个程序绝对不能使用资源。”
而是:
“你可以使用资源,但是我得给你画一条线。”
比如:
1 | CPU:最多 2 个 CPU |
超过以后,你自己的业务自己承担后果。**别把隔壁 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 可以管理很多资源。比较常见的包括:
| 控制器 | 主要作用 |
|---|---|
cpu | CPU 调度、CPU 使用量统计 |
memory | 内存使用限制和统计 |
io | 块设备 I/O 控制 |
pids | 限制进程/线程数量 |
cpuset | CPU 核心和 NUMA 节点控制 |
rdma | RDMA 相关资源控制 |
perf_event | 与性能监控相关的资源分组 |
如果是做 Linux 运维,前面几个尤其重要:
1 | CPU |
也就是说,我们可以给一个业务设置:
1 | 最多使用多少 CPU |
cgroup v1 和 cgroup v2 到底有什么区别?
这个地方一定要注意。因为你在网上搜索 cgroup 的时候,会看到大量老文章。然后里面可能是:
1 | /sys/fs/cgroup/cpu/ |
或者:
1 | /sys/fs/cgroup/memory/ |
再或者一大堆:
1 | cgcreate |
这些很多都是基于 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 | [root@localhost ~]# mount | grep cgroup |
或者:
1 | [root@localhost ~]# stat -fc %T /sys/fs/cgroup |
很容易能看到当前使用的是 cgroup v2。
cgroup 和 systemd 到底是什么关系?
这个问题非常重要。很多人第一次学习 cgroup 的时候,会直接钻进:
1 | /sys/fs/cgroup |
然后开始:
1 | mkdir |
最后把自己绕晕。其实如果你的服务器使用 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 | service |
就是物业用来给不同业务分组、分房间的方式。
service、scope、slice 到底是什么?
刚才看起来有点抽象。我们分别来看。
1. service
这个最好理解。
比如:
1 | httpd.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 | [root@localhost ~]# systemd-cgls |
这个东西特别适合排查:
一个进程到底属于谁?
比如假设PID 12002特别能吃 CPU。你可以沿着树找到:
1 | httpd.service |
这时候就知道:
原来这个进程属于 httpd.service。
systemd-cgtop:cgroup 版本的 top
如果:
1 | systemd-cgls |
解决的是:
“你是谁?”
那么:
1 | [root@localhost ~]# systemd-cgtop |
解决的就是:
“你吃了多少资源?”
这时候就非常直观了。
如果某一天你看到:
1 | myapp.service 18G |
那就不用猜了。
这哥们儿可能正在干大事。
当然,也可能只是业务本身就是吃内存的,所以生产环境一定要结合业务情况分析。
直接看某个 PID 属于哪个 cgroup
比如:
1 | [root@localhost ~]# cat /proc/3673/cgroup |
cgroup v2 可能看到:
1 | [root@localhost ~]# cat /proc/3673/cgroup |
这句话非常重要。它告诉你:
| 部分 | 含义 |
|---|---|
0 | cgroup v2 的统一层级标识,0 表示使用统一层级 |
:: | cgroup v2 的格式分隔符 |
user.slice | 属于 systemd 的用户会话资源分组 |
user-0.slice | UID 为 0 的用户,也就是 root 用户 |
session-1.scope | root 用户的第 1 个登录会话对应的 scope |
cgroup 资源控制有两种思路
这里是今天非常重要的一部分。cgroup 里面的资源控制,大体可以理解成两种思路:
第一种:资源权重
意思是:
资源不够的时候,大家按照优先级分。
例如:
1 | 业务 A:CPUWeight=100 |
当 CPU 很空闲的时候:
1 | A:想吃多少吃多少 |
但是 CPU 真不够用了:
1 | A:100 |
B 获得的 CPU 调度份额更高。可以把它想象成食堂:
饭管够的时候,大家随便吃。
饭不够了,A 有 1 张饭票,B 有 2 张饭票,那就按照比例分。
第二种:资源硬限制
硬限制就完全不一样了。它相当于:
不管服务器现在多富裕,你最多只能吃这么多。
例如:
1 | CPUQuota=200% |
意思可以理解成:
1 | CPU |
所以:
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 | CPUWeight |
CPUQuota=100% 到底是多少 CPU?
这个地方非常容易被误解。
比如:
1 | CPUQuota=100% |
通常可以理解成:
最多使用一个 CPU 的计算时间。
如果:
1 | CPUQuota=200% |
就是:
最多使用两个 CPU 的计算时间。
例如服务器有:
1 | 16 CPU |
配置:
1 | CPUQuota=200% |
并不是:
“占整台服务器 CPU 的 200%。”
而是:
这个服务最多消耗相当于两个 CPU 的计算资源。
所以:
1 | 100% ≈ 1 CPU |
理解这个以后,后面配置就不会那么容易懵。
实战一:给 httpd 加资源限制
现在来做一个真正的生产案例。假设服务器运行httpd:
1 | [root@localhost ~]# dnf install httpd -y |
来启动服务
1 | [root@localhost ~]# systemctl enable httpd --now |
我们担心 Web 服务出现异常以后:
- CPU 把服务器吃满
- 内存无限增长
- 子进程疯狂创建
于是我们决定给它设置:
1 | CPU:最多 1 个 CPU |
可以使用:
1 | CPUQuota=100% |
注意,不要修改软件自带的配置文件,软件升级的适合,会覆盖,我们要用Linux里的drop-in
1 | [root@localhost ~]# mkdir -p /etc/systemd/system/httpd.service.d/ |
配置文件写完以后,systemd 还不知道我们修改了配置,来reload一下
1 | [root@localhost ~]# systemctl daemon-reload |
这个命令的作用是:
告诉 systemd:哥们儿,我刚刚改配置了,你重新看一遍。
注意:
1 | [root@localhost ~]# systemctl daemon-reload |
不是重启 httpd。
它只是让 systemd 重新加载配置。所以接下来还需要:
1 | [root@localhost ~]# systemctl restart httpd |
看看生效没:
1 | [root@localhost ~]# systemctl show httpd -p CPUQuotaPerSecUSec |
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 | [root@localhost ~]# systemctl set-property httpd.service MemoryMax=256M |
这个方式可以直接修改 systemd 单元的资源属性。如果只是想临时测试,不希望修改持久配置,可以:
1 | systemctl set-property --runtime httpd.service MemoryMax=256M |
区别可以简单记:
1 | set-property |
重启或者重新加载持久配置以后,这个临时设置不会作为永久配置保留下来。
这个方法非常适合:
线上排查问题的时候先试一下。
确认效果没问题以后,再把配置正式写入 drop-in。
实战三:我就想临时跑个命令,怎么办?
还有一种场景:你只是想测试一个程序。
比如:
1 | sleep 10d |
你不想为了它创建:
1 | xxx.service |
也不想写配置文件。那怎么办?可以使用:
1 | systemd-run |
例如:
1 | systemd-run \ |
这里发生了什么?
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 | 临时测试 |
实战四:把多个业务放进一个 slice
如果服务器上的业务越来越多:
1 | app01 |
你可能会想:
“我能不能把这几个业务统一限制?”
当然可以。这时候就轮到:
slice
出场了。比如我们创建:
1 | biz-app.slice |
现在来创建 slice:
1 | /etc/systemd/system/biz-app.slice |
内容:
1 | [Unit] |
现在这个 slice 的意思就是:
这一整个业务分组最多使用 2 个 CPU 和 2GB 内存。
让业务服务加入这个 slice,假设我们有:
1 | myapp.service |
创建:
1 | /etc/systemd/system/myapp.service.d/10-slice.conf |
内容:
1 | [Service] |
然后:
1 | systemctl daemon-reload |
现在关系变成:
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 | 订单系统 |
如果每个业务都单独配置一堆限制,时间长了配置会非常散。这时候可以设计:
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 | order.slice |
这就开始有点企业资源隔离的味道了。
父 slice 和子 slice
这里再强调一个概念。slice 本身可以形成层级。
例如:
1 | biz.slice |
父级可以定义资源约束,子级可以进一步设置自己的资源策略。你可以把它理解成公司组织架构:
1 | 集团 |
集团先给技术中心一个总预算。下面的团队再分别分配预算。资源管理的思路也是类似的。
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 | 主进程 100M |
总共:
1 | 450M |
还没有超过限制。如果继续增长,超过 cgroup 的内存限制以后,就会进入 cgroup 的内存压力/OOM 处理路径,相关进程可能被终止。
所以:
MemoryMax 不是“内存超过以后给你报警”这么简单,而是真正的资源边界。
生产环境一定要谨慎设置。
TasksMax 是干什么的?
再来看:
1 | TasksMax=200 |
这个参数非常适合防止:
进程/线程疯狂增长。
例如一个程序出了 Bug:
1 | 创建线程 |
最后:
1 | 几千个线程 |
这时候不仅仅是 CPU 的问题。系统本身也可能受到影响。所以可以通过:
1 | TasksMax=200 |
给这个服务设置任务数量上限。可以简单理解成:
你这个业务最多只能占这么多“工位”。
CPUWeight 和 CPUQuota 千万别搞混
这个地方再总结一次。假设:
1 | CPUWeight=100 |
它不是:
“最多只能使用 100% CPU。”
这是一个非常常见的误解。
CPUWeight 是:
CPU 资源竞争时的相对权重。
而:
1 | CPUQuota=100% |
才是:
CPU 使用上限。
所以:
1 | CPUWeight |
用一句特别简单的话:
Weight 是饭不够的时候怎么分,Quota 是你最多吃几碗。
生产环境到底应该怎么配置?
现在我们把整个知识点串起来。假设发现一个业务:
1 | CPU 飙高 |
不要第一反应:
1 | CPUQuota=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 | 业务正常 CPU 使用量 |
综合判断。
场景二:某个服务内存经常失控
可以考虑:
1 | MemoryMax=4G |
但是同样不能简单认为:
“给 4G 就完事了。”
如果业务正常情况下就需要:
1 | 3.8G |
那你设置:
1 | 4G |
实际上已经没有多少安全余量。
场景三:怀疑程序有 fork/线程失控问题
可以考虑:
1 | TasksMax=500 |
这时候即使程序出现异常,也不至于无限制地创建任务。
场景四:多个业务需要统一限制
可以考虑:
1 | slice |
例如:
1 | biz.slice |
下面统一放:
1 | order.service |
然后在 slice 层面进行统一资源管理。
不要轻易直接修改 /sys/fs/cgroup
你在网上可能会看到很多文章:
1 | echo 100000000 > /sys/fs/cgroup/xxx/memory.max |
这种操作并不是说完全不能用。Linux 本身就是通过这些 cgroup 文件提供底层控制接口。
但是:
如果服务器本身已经由 systemd 管理服务,通常更推荐通过 systemd 来管理。
也就是:
1 | systemd |
而不是:
1 | 手动修改 cgroup 文件 |
原因很简单。生产环境最怕:
1 | 今天改了 |
systemd unit + drop-in 的配置方式,更容易:
1 | 审计 |
几个非常容易踩的坑
坑一:看到 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 | fork 炸了 |
所以真正排查的时候,要看:
1 | CPU |
而不是看到 CPU 高,就只配置:
1 | CPUQuota= |
一个比较完整的企业资源隔离案例
最后我们来做一个稍微完整一点的案例。
假设:
1 | 服务器:16 CPU / 32GB RAM |
现在我们希望:
1 | 订单 + 支付 |
可以设计成:
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 | # biz.slice |
意思是:
订单和支付这整个业务资源池,最多使用约 8 个 CPU 和 16GB 内存。
而日志业务可以单独:
1 | # logging.slice |
这样即使日志处理程序突然抽风:
1 | 日志暴增 |
它也不会轻易把整个服务器吃干净。
这就是 cgroup 真正有价值的地方:
不是为了让某个程序跑得更快,而是为了让整个系统在某个程序出问题的时候,还能活下来。
cgroup 其实解决的是“资源边界”问题
学到这里,你会发现:
1 | ulimit |
其实是逐渐往更完整的资源管理方向发展的。可以简单理解成:
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 | httpd.service |
那么优先考虑:
1 | systemd 的资源控制 |
而不是上来就手动修改:
1 | /sys/fs/cgroup |
3. Weight 和 Quota 是两回事
记住:
1 | CPUWeight |
4. MemoryMax 是真正的资源边界
例如:
1 | MemoryMax=4G |
意味着这个 cgroup 的内存使用不能无限增长。超过限制以后,可能触发 cgroup 的内存回收和 OOM 处理。所以:
不要为了“防止程序吃内存”就随便给一个特别小的值。
5. TasksMax 很适合防止任务数量失控
如果怀疑某个业务存在:
1 | fork 爆炸 |
可以考虑:
1 | TasksMax= |
6. 多个业务需要统一管理,就考虑 slice
例如:
1 | biz.slice |
统一做资源管理。
7. 修改以后一定验证
常用工具:
1 | systemd-cgls |
不要只看配置文件。真正应该确认的是:
systemd 到底有没有加载?cgroup 到底有没有生效?业务实际表现怎么样?
最后总结
今天这篇其实就解决了一个问题:
如果服务器上某个业务突然变成“资源黑洞”,我们怎么把它关进笼子里?
答案就是:
1 | cgroup v2 |
它可以把一批进程放到一个资源控制组里面,然后统一管理:
1 | CPU |
而在现代 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 | CPUWeight |
最后再记住一个生产环境最重要的思路:
flowchart TB
A["发现问题"] --> B["找到资源瓶颈"]
B --> C["找到资源消耗者"]
C --> D["分析正常资源需求"]
D --> E["设置合理边界"]
E --> F["验证 cgroup 是否生效"]
F --> G["观察业务"]
G --> H["持续调整"]而不是:
1 | 发现 CPU 高 |
这才是 Linux 性能调优和“乱改参数”最大的区别。 性能调优不是:
参数越大越好,也不是限制越狠越好。
真正的生产调优是:
先知道问题在哪里,再决定资源边界应该画在哪里。
参考手册
1 | man systemctl |
