Linux 性能调优系列 (八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
1 | 作者:李晓辉 |
前面我们学习了内核可调项和 TuneD 调优工具,这些主要是在操作系统层面调整系统行为。但是实际生产环境还有一种很常见的情况。一台服务器上可能同时跑着很多业务,比如 Web 服务、数据库、监控程序、日志程序……
如果其中某一个程序突然出现异常,疯狂占用 CPU、创建大量进程、打开海量文件,甚至不断申请系统资源,那么它很可能会把其他业务一起拖垮。这时候我们就需要给进程或者服务设置一些资源上限。
你可以把它理解成:
服务器就像一栋办公楼,每个业务就是一个公司。
如果某家公司突然疯狂占用会议室、电话线路、办公桌,其他公司自然就没地方用了。
所以管理员会给它规定:
“你最多只能用这么多资源,超过这个范围就不允许继续申请。”
Linux 中的资源限制,就是在做类似的事情。
什么是 POSIX 资源限制?
在 Linux 中,一个进程能够使用多少系统资源,并不是完全没有限制的。系统可以通过 POSIX resource limits,也就是 POSIX 资源限制,对进程能够使用的部分资源设置上限。
例如:
- 最多可以打开多少文件
- 最多可以创建多少进程
- 最多可以使用多少 CPU 时间
- 栈空间最大是多少
- core dump 最大是多少
Linux中常见的两种操作方式是:
| 方式 | 主要作用对象 | 典型用途 |
|---|---|---|
ulimit | 当前 Shell 及其子进程 | 临时测试、用户登录会话 |
systemd Limit | systemd 管理的服务进程 | 给后台服务设置资源限制 |
这里大家先记住一个非常重要的区别:
ulimit更偏向“用户登录会话”;systemd 更适合“后台服务”。
比如:
你 SSH 登录服务器之后,在命令行里运行一个程序,这时候可以使用 ulimit。但是如果是 Nginx、MySQL、Redis 这种由 systemd 管理的后台服务,就应该考虑在对应的 systemd service 中设置限制。而如果以后需要对 CPU、内存等资源进行更加完整的服务级隔离,就会涉及 cgroup v2,这个我们后面再讲。
ulimit 命令详解
ulimit 是 Bash 的内置命令,用于查看或者修改当前 Shell 的资源限制。
这里一定要注意:
ulimit修改的是当前 Shell 进程的资源限制。
而 Shell 启动出来的子进程,会继承这些限制。
所以可以简单理解成:
1 | 当前 Shell |
如果我们在这个 Shell 中设置了资源限制,那么从这个 Shell 启动的子进程通常也会继承相应限制。
这也是为什么:
直接在终端执行
ulimit,通常只影响当前会话,不会影响其他 SSH 会话。
soft limit 和 hard limit
ulimit 中有两个非常重要的概念:
soft limit:软限制
软限制可以理解成:
当前真正生效的限制值。
普通用户可以在权限允许的范围内调整自己的 soft limit,但不能超过 hard limit。
hard limit:硬限制
hard limit 可以理解成:
这个资源允许设置的最高上限。
普通用户不能把自己的 hard limit 随便调高。
所以可以简单记成:
1 | hard limit |
例如:
1 | soft = 100 |
那么当前实际限制是 100。用户可以把 soft 从 100 调到 150,但不能调到 300,因为已经超过 hard limit。
不同资源达到限制之后的具体行为并不完全一样,所以不要简单理解成“所有限制达到以后都会被杀掉”,也就是说**
ulimit的不同资源限制,达到上限后,程序的反应是不一样的,并不是所有情况都会直接把进程杀掉。**。
| 资源 | 参数 | 达到限制后的典型行为 |
|---|---|---|
| CPU 时间 | -t / LimitCPU | 超过限制后,进程可能收到 SIGXCPU,继续超限可能被终止 |
| 打开文件数 | -n / LimitNOFILE | 不会直接杀进程,程序继续运行,但再打开文件可能失败 |
| 用户进程数 | -u / LimitNPROC | 创建新进程/线程可能失败,已有进程通常不会因此被杀掉 |
| Core 文件大小 | -c / LimitCORE | Core Dump 文件可能被截断或无法生成,并不是杀掉原进程 |
| 文件大小 | -f / LimitFSIZE | 继续写文件可能失败,进程不一定被直接杀掉 |
举个最容易理解的例子:nofile,假设:
1 | ulimit -n 100 |
意思是:
当前进程最多可以同时打开 100 个文件描述符。
假设一个程序已经打开了 100 个文件,这时候它又想打开第 101 个:
1 | 打开第 101 个文件 |
并不是 systemd 或内核看到达到 100,就直接把这个进程杀掉。
例如程序处理得比较好:
1 | 打开文件失败 |
处理得不好,也可能:
1 | 打开文件失败 |
所以这里真正需要理解的是:
资源限制更像是在资源入口处设置了一道“门槛”,超过门槛以后,具体发生什么,要看这个资源的语义以及应用程序如何处理失败。
那真正需要限制内存怎么办?
如果你的场景是:
“服务器上运行一个 systemd 服务,我不希望它把整台机器的内存吃光。”
这时候应该使用 systemd 的:
1 | MemoryMax= |
例如:
1 | [Service] |
可以把它理解成:
1 | 服务器 16 GB RAM |
systemd 会利用 cgroup 对服务进行资源控制,至于这个,我下面会介绍,这里先理解就行。
交互式操作 ulimit
先来看当前 Shell 的资源限制。
使用:
1 | [root@localhost ~]# ulimit -a |
可以查看当前 Shell 的各种资源限制。如果想明确查看 soft limit:
1 | [root@localhost ~]# ulimit -S -a |
如果想查看 hard limit:
1 | [root@localhost ~]# ulimit -H -a |
其中:
1 | -S |
表示 soft limit。
1 | -H |
表示 hard limit。
如果设置的时候不指定 -S 或 -H,需要特别注意命令的具体行为和当前权限,不要想当然地认为它只修改 soft limit。
你可能会发现,很多参数的输出完全一样。这并不代表 Soft 和 Hard 没有区别,而是因为这些资源当前设置的 Soft 和 Hard 值刚好相同。真正有区别的地方,可以看下面几个例子:
open files(
-n)- Soft:
1024 - Hard:
524288 - 说明:当前 Shell 最多打开
1024个文件,但 Soft limit 最多可以提高到524288。
- Soft:
stack size(
-s)- Soft:
8192 KB - Hard:
unlimited - 说明:当前限制是
8192 KB,但 Hard limit 没有设置上限。
- Soft:
pending signals(
-i)- Soft:
30400 - Hard:
30400 - 说明:Soft 和 Hard 当前设置成了一样的值,所以两边显示相同。
- Soft:
CPU time(
-t)- Soft:
unlimited - Hard:
unlimited - 说明:Soft 和 Hard 都没有设置 CPU 时间限制,所以两边显示相同。
- Soft:
因此大家可以简单理解:
- Soft limit:当前真正生效的资源限制。
- Hard limit:Soft limit 能够提升到的最高边界。
- 如果 Soft 和 Hard 本身设置成一样,执行
ulimit -S -a和ulimit -H -a看起来就会完全一样。 - 如果两者不同,就能明显看到区别。
例如:
1 | open files (-n) |
这就意味着:
现在只能打开 1024 个文件,但可以把 Soft limit 调高,最高不能超过 524288。
实操案例:查看单项资源限制
比如我们想看看:
当前 Shell 允许进程累计使用多少 CPU 时间。
可以执行:
1 | [root@localhost ~]# ulimit -S -t |
这里的:
1 | unlimited |
表示当前没有设置 CPU 时间上限。
为什么重新打开 SSH 窗口就失效?
如果我们刚才执行的是:
1 | ulimit -S -t 60 |
它修改的是:
当前 Shell 的资源限制。
比如:
1 | SSH窗口 A |
我们只在 Shell A 中执行了:
1 | ulimit -S -t 60 |
那么 Shell B 并不会自动继承这个限制。
所以:
直接执行
ulimit,非常适合临时测试,但不适合作为长期配置方案。
ulimit 如何持久化?
刚才我们直接执行:
1 | ulimit -S -t 60 |
退出当前 Shell 后,这个配置就没有了。
如果希望:
用户每次登录服务器时,都自动获得指定的资源限制。
那么就需要使用 PAM 的 pam_limits 机制。Linux中常见的配置文件是:
1 | /etc/security/limits.conf |
其中更推荐把自己的配置放在:
1 | /etc/security/limits.d/ |
这样做的好处是:
不需要直接修改系统主配置文件,自己的配置更加独立,也方便后期维护。
limits.conf 配置格式
配置格式是:
1 | <domain> <type> <item> <value> |
分别表示:
| 参数 | 含义 |
|---|---|
domain | 对哪个用户或用户组生效 |
type | soft 或 hard |
item | 要限制的资源 |
value | 限制值 |
例如:
1 | @managers hard maxlogins 3 |
假设服务器上有一个managers用户组。现在公司的要求是:
managers 组中的用户最多同时建立 3 个登录会话。
我们可以创建:
1 | /etc/security/limits.d/managers.conf |
内容:
1 | @managers hard maxlogins 3 |
这样,当属于这个组的用户进行 PAM 登录时,就会受到这个限制。
可以把它理解成:
公司办公楼规定:这个部门最多只能同时占用 3 个工位。
第 4 个登录请求就会受到限制。
一个非常重要的坑:limits.conf 不管 systemd 服务
这里是这篇文章非常重要的一个知识点。很多初学者会有这样的想法:
“我把
/etc/security/limits.d/里面的nofile调大了,那 Nginx、MySQL 应该也跟着变大了吧?”
不一定,而且对于 systemd 直接启动的后台服务,通常不是这么生效的。
原因很简单:
1 | SSH登录用户 |
而 systemd 启动后台服务,并不是通过这种 PAM 登录会话启动的。
所以:
1 | /etc/security/limits.conf |
主要针对 PAM 登录会话。而 systemd 管理的后台服务,要从 systemd 自己的配置入手。
使用 systemd 给服务设置资源限制
systemd 不仅负责:
1 | 启动服务 |
它还可以参与:
1 | 资源管理 |
现在大家对systemd 的资源管理推荐程度也比较高,其资源控制底层会结合 cgroup 等机制管理服务进程。所以,如果我们的需求是:
“我要限制一个后台服务。”
那么相比修改用户的 limits.conf,更应该考虑 systemd 的服务级配置。
systemd 的 Limit* 参数
在 systemd service 的[Service]部分,可以使用很多Limit参数。
这些参数对应 Linux 的 POSIX resource limit,也就是我们前面讲的 ulimit / setrlimit 这一套限制机制。
常见的对应关系如下:
| systemd 参数 | ulimit 选项 | 作用 |
|---|---|---|
LimitCPU= | -t | CPU 时间限制 |
LimitNOFILE= | -n | 最大打开文件描述符数量 |
LimitNPROC= | -u | 用户进程数量限制 |
LimitSTACK= | -s | 进程栈大小 |
LimitCORE= | -c | core dump 文件大小 |
systemd 修改配置的正确姿势
这里还有一个生产环境非常重要的习惯。不要直接修改软件包安装的:
1 | /usr/lib/systemd/system/xxx.service |
因为软件升级的时候,这些文件可能被软件包更新。更推荐使用:
1 | /etc/systemd/system/<服务名>.service.d/ |
创建 drop-in 配置片段。
这样可以:
不修改原始 service 文件,只额外增加自己的配置。
而 /etc/systemd/system/ 的优先级高于 /usr/lib/systemd/system/。
实操案例:给 sshd 服务设置 100M 内存上限
假设现在我们希望限制 sshd 服务:
sshd 服务及其 cgroup 中的进程,内存使用量最多为 100MB。
首先创建 sshd 的 drop-in 配置目录:
1 | [root@localhost ~]# mkdir -p /etc/systemd/system/sshd.service.d |
然后创建配置文件:
1 | cat > /etc/systemd/system/sshd.service.d/10-memorylimits.conf <<EOF |
配置完成后,让 systemd 重新读取配置:
1 | systemctl daemon-reload |
然后重启 sshd 服务:
1 | systemctl restart sshd |
这样,新的 sshd 服务进程就会受到 MemoryMax=100M 的内存限制。
查看配置是否生效可以使用:
1 | [root@localhost ~]# systemctl cat sshd |
或者
[root@localhost ~]# systemctl show sshd -p MemoryMax
MemoryMax=104857600
1 |
|
⚠️ 实际生产环境要特别注意
这里大家不要简单理解成:
“sshd 自己最多只能使用 100MB。”
更准确地说:
MemoryMax=100M限制的是sshd.service对应 cgroup 的内存使用上限。
如果该 cgroup 中的内存使用超过限制,systemd/cgroup 会采取内存回收和 OOM 处理机制,相关进程可能被终止。
因此,生产环境中不要随便给 sshd 设置一个过低的 MemoryMax。否则可能导致 SSH 服务异常,甚至影响远程登录和运维。
这个案例主要是为了帮助我们理解:
1 | systemd |
这也是现代 Linux 中使用 systemd 管理服务资源时比较典型的思路,具体的值大家还是要理性设置,不要让我老李背锅。
为什么需要 daemon-reload?很多刚接触 systemd 的同学会问:
“我都把文件改好了,为什么还要
daemon-reload?”
原因是:
systemd 本身需要重新读取 unit 配置。
所以我们修改 service 配置后,一般按照这个流程:
1 | 修改配置文件 |
注意:
1 | daemon-reload |
只是让 systemd 重新读取配置。
它不会自动把已经运行的服务进程全部按照新的限制重新启动。
因此这里还需要:
1 | systemctl restart xxx.service |
ulimit 和 systemd Limit* 到底怎么选?
到这里,我们把前面的内容串起来。
| 方式 | 主要生效对象 | 配置方式 | 适合场景 |
|---|---|---|---|
ulimit | 当前 Shell 及其子进程 | 命令行 | 临时测试 |
pam_limits | PAM 登录会话 | limits.conf / limits.d | SSH 等用户登录会话 |
systemd Limit* | systemd 服务的进程 | service / drop-in | 后台服务的 POSIX 资源限制 |
| systemd cgroup | service 对应的资源组 | MemoryMax、CPUQuota、TasksMax 等 | 更完整的服务级资源控制 |
这里特别注意:
systemd 的资源管理并不只有
Limit*。
例如真正要控制一个服务:
1 | 最多使用多少内存 |
生产环境应该怎么做?
实际生产环境中,不建议一上来就给所有服务设置一大堆限制。更合理的思路是:
第一步:先明确问题
例如发现:
1 | Too many open files |
再去研究:
1 | nofile |
而不是:
“网上有人说 Linux 性能调优必须把所有参数都调大,那我也全部调大。”
第二步:根据进程启动方式选择方案
如果是:
1 | SSH登录 |
可以考虑:
1 | ulimit |
如果是:
1 | systemd |
那么优先考虑:
1 | systemd service |
对应的:
1 | Limit* |
或者后续学习的:
1 | cgroup |
第三步:先临时测试,再持久化
这个思路和前面的内核参数调优是一致的。先测试:
1 | ulimit |
或者临时修改 service 配置。
确认业务没有异常以后,再正式写入:
1 | /etc/security/limits.d/ |
或者:
1 | /etc/systemd/system/<service>.service.d/ |
第四步:修改后一定验证
比如修改 systemd 配置后,可以检查:
1 | systemctl show example.service | grep '^Limit' |
也可以针对具体参数查看:
1 | systemctl show example.service -p LimitNOFILE |
确认 systemd 当前真正加载的值,而不是只看自己写入的配置文件。
最后总结一下
这一篇我们主要解决一个问题:
如果某个进程或者服务太能“吃资源”,怎么给它设置一个边界?
先记住三个东西:
1 | ulimit |
它们分别解决不同场景。
| 场景 | 优先考虑 |
|---|---|
| 临时测试当前 Shell | ulimit |
| SSH 用户登录会话 | limits.conf / limits.d |
| systemd 后台服务 | systemd Limit* |
| 服务级 CPU、内存、任务数等资源控制 | systemd + cgroup |
还有一个非常重要的经验:
不要看到资源限制就全部设置成一个很大的数字。
例如:
1 | LimitNOFILE=999999 |
并不代表性能一定更好。资源限制本质上是在:
系统稳定性、业务需求和资源利用率之间找一个合适的边界。
真正做生产调优时,应该先知道:
1 | 现在出了什么问题? |
而不是:
1 | 复制一堆参数 |
参考手册
1 | man bash |
下一篇预告:如果只是使用
ulimit和Limit*,我们解决的主要还是传统 POSIX resource limits。真正想做到**“这个服务最多用多少 CPU、多少内存、最多创建多少任务”**,就需要进一步学习 Linux 的 cgroup v2,以及 systemd 如何通过 cgroup 对服务进行资源控制。
