在Debian服务器上跑业务,最怕的不是流量突然上涨,而是某个进程像脱缰野马一样吃光内存或者占满CPU,最后逼得你只能重启。重启治标不治本,真正该做的是提前给进程套上缰绳。Debian底层用systemd管理服务,而Linux内核本身也提供了一套资源限制机制,两者配合起来,就能把失控的进程牢牢按在规定的资源范围内。

先说最直接的问题:一个PHP-FPM进程或者数据库查询把内存吃满,系统开始疯狂用swap,然后整个服务器响应变慢甚至SSH都连不上。这种情况在低配VPS上尤其常见。要解决它,核心思路有两个,一是限制单个进程或用户能使用的内存上限,二是限制进程能打开的文件数量,避免句柄泄漏拖垮系统。

用systemd管理服务的资源限制

现在Debian 11和12上,绝大多数服务都由systemd托管。systemd的资源控制能力非常强,而且配置起来比直接写ulimit脚本要规范得多。你不需要去改什么/etc/security/limits.conf然后祈祷服务启动时能读到,直接在服务的unit文件里声明就行。

先看一个常见的场景:你服务器上跑着MySQL或者MariaDB,默认情况下它可能会根据机器内存大小自动调整buffer pool,但有时候自动调整的结果并不理想,或者你希望给它一个硬性上限,防止它把内存吃得一干二净。这时候可以给服务加一个MemoryMax限制。

操作方法是先找到服务的unit文件。比如MariaDB的服务名是mariadb.service,执行下面命令查看它的完整配置:

systemctl cat mariadb.service

你会看到类似这样的输出,里面包含了[Service]段落。要添加资源限制,不建议直接改/lib/systemd/system/下的原始文件,因为包更新可能会覆盖。正确做法是创建一个override文件:

systemctl edit mariadb.service

这个命令会打开一个编辑器,让你输入覆盖配置。在里面写上:

[Service]
MemoryMax=2G
MemorySwapMax=512M
TasksMax=512
LimitNOFILE=65536

保存退出后,执行systemctl daemon-reload和systemctl restart mariadb.service,限制就生效了。MemoryMax=2G表示这个服务最多使用2GB物理内存,MemorySwapMax=512M表示它最多还能用512MB的swap,TasksMax=512限制了这个服务能创建的任务数,LimitNOFILE=65536则把文件描述符上限提到了6万多,这对于数据库这种需要大量并发连接的服务来说是必要的。

这里有一个细节要注意:MemoryMax是硬限制,一旦进程组的内存使用达到这个值,内核会直接触发OOM killer,把组内进程杀掉。所以设置的时候要留足余量,别卡得太死。比如你服务器总共4GB内存,给MariaDB设2GB是合理的,但如果你设成3.5GB,那系统自己都不够用了,等于自己给自己挖坑。

限制用户级别的资源使用

有些场景下,你不想针对某个具体的服务做限制,而是想限制某个Linux用户能使用的资源总量。比如服务器上有多个网站,每个网站用一个独立的系统用户跑PHP-FPM池,这时候限制用户级别的资源就非常合适。

Debian上传统的做法是编辑/etc/security/limits.conf,在里面加上类似这样的行:

webuser soft nproc 100
webuser hard nproc 200
webuser soft nofile 4096
webuser hard nofile 8192
webuser soft memlock 65536
webuser hard memlock 131072

soft是软限制,用户可以自己调高但不能超过hard;hard是硬限制,只有root能改。nproc是进程数,nofile是打开文件数,memlock是锁定内存的字节数。这个配置对通过PAM登录的会话有效,但对systemd启动的服务不一定生效,因为systemd默认不读取这个文件。

如果你用的是PHP-FPM,并且每个网站有独立的池配置,更直接的方法是在池配置文件里设置资源限制。Debian上PHP-FPM的池配置在/etc/php/8.2/fpm/pool.d/目录下(版本号根据实际情况调整)。打开某个池的配置文件,你会看到很多可以调的资源参数:

pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500

pm.max_children是最关键的参数,它直接决定了这个池最多能同时运行多少个PHP进程。每个PHP进程都会占用内存,假设每个进程平均占用80MB,那20个进程就是1.6GB。你可以根据服务器内存来反推这个值。pm.max_requests=500表示每个进程处理500个请求后就会重启,这能有效防止PHP进程因为内存泄漏越用越大。

但pm.max_children只能限制进程数量,不能限制单个进程的内存使用。如果某个PHP脚本写得不好,一个进程就可能吃掉500MB内存,这时候就需要在PHP层面做限制。编辑/etc/php/8.2/fpm/php.ini,找到memory_limit参数:

memory_limit = 256M

这个值限制了单个PHP脚本能使用的最大内存。设成256M对大多数网站来说够用了,如果是WordPress这种比较吃内存的应用,可以适当调高到512M。但不要无脑调大,因为memory_limit乘以pm.max_children就是最坏情况下的内存占用,你得确保这个值小于服务器可用内存。

用cgroup做精细化的资源隔离

systemd的资源限制底层就是通过cgroup实现的,但有时候你需要更细粒度的控制,比如限制一组进程的CPU使用率,或者限制某个进程的IO带宽。这时候可以直接用cgroup工具来操作。

Debian 11和12默认使用cgroup v2,挂载点在/sys/fs/cgroup。你可以手动创建cgroup目录来管理资源。比如你想限制一个备份脚本的CPU使用率,不让它把CPU占满影响正常业务:

mkdir -p /sys/fs/cgroup/backup
echo "50000 100000" > /sys/fs/cgroup/backup/cpu.max
echo "512M" > /sys/fs/cgroup/backup/memory.max

上面的命令创建了一个名为backup的cgroup,cpu.max里的"50000 100000"表示这个cgroup里的进程最多使用50%的CPU时间(50000微秒每100000微秒周期)。memory.max=512M限制了这个cgroup最多用512MB内存。然后把备份进程放进去:

echo $$ > /sys/fs/cgroup/backup/cgroup.procs

这样当前shell及其子进程就都在这个cgroup里了,资源限制立即生效。不过手动管理cgroup比较繁琐,而且重启后配置会丢失。对于临时性的资源隔离任务,比如跑一个数据迁移脚本,用这种方法很灵活。对于长期运行的服务,还是建议用systemd的MemoryMax、CPUQuota这些参数,因为systemd会自动管理cgroup的生命周期。

systemd里限制CPU使用率也很简单,在service的override文件里加一行:

CPUQuota=150%

这表示该服务最多使用1.5个CPU核心的计算能力。如果你的服务器是4核的,150%就是总CPU资源的37.5%。这个参数比直接设置CPUAffinity要灵活得多,因为CPUQuota不绑定具体核心,只是限制总的CPU时间配额。

监控和排查资源使用情况

配置完限制之后,你得知道这些限制是否真的生效了,以及当前各个服务的资源使用情况。systemd提供了几个很有用的命令。

查看某个服务的资源使用情况:

systemctl status mariadb.service

在输出的底部会有一行CGroup信息,显示这个服务当前的cgroup路径。你可以进一步查看详细的资源统计:

systemctl show mariadb.service -p MemoryCurrent -p MemoryMax -p CPUUsageNSec -p TasksCurrent

MemoryCurrent是当前内存使用量,MemoryMax是你设置的上限,CPUUsageNSec是CPU使用时间(纳秒),TasksCurrent是当前任务数。这些数据能让你直观地看到服务是否接近限制值。

还有一个更直观的工具是systemd-cgtop,它类似于top命令,但显示的是cgroup级别的资源使用:

systemd-cgtop

这个命令会实时显示各个cgroup的CPU、内存、IO使用情况,按使用量排序。当你怀疑某个服务资源占用异常时,用这个命令能快速定位问题。

另外,/proc文件系统里也有丰富的资源信息。比如查看某个进程的详细资源限制:

cat /proc/12345/limits

这会列出该进程的所有软限制和硬限制,包括文件大小、内存、进程数、打开文件数等。如果你发现某个服务的LimitNOFILE没有生效,用这个命令检查一下实际生效的值是多少,能帮你快速定位问题出在哪个环节。

常见坑和注意事项

资源限制配置中最常见的错误就是设得太紧,导致服务启动失败或者运行中频繁被杀。比如你把MariaDB的MemoryMax设成512M,但MariaDB启动时就需要加载InnoDB buffer pool,至少需要1GB以上,那服务根本起不来。所以改完配置后一定要用systemctl status看一下服务状态,如果启动失败,journalctl -xe能看到具体的错误信息。

另一个坑是LimitNOFILE设得太高,超过了系统全局的文件描述符上限。Debian默认的全局上限可以通过/proc/sys/fs/file-max查看,通常有几十万甚至上百万,一般不会成为瓶颈。但如果你在limits.conf里给某个用户设了nofile 1000000,而系统全局上限只有200000,那实际生效的会以全局上限为准。

还有一点,systemd的MemoryMax限制的是整个cgroup的内存使用,包括服务fork出来的所有子进程。这通常是我们想要的效果,但有些服务会fork出很多短生命周期的子进程,比如Nginx的worker进程,它们共享同一个cgroup,所以MemoryMax要考虑到所有worker的总和,而不是单个worker的内存。

对于IO密集型任务,比如数据库备份或者日志压缩,限制CPU和内存还不够,还需要限制IO带宽。systemd里可以用IOReadBandwidthMax和IOWriteBandwidthMax参数:

[Service]
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 30M

这限制了该服务对/dev/sda设备的读取速度最多50MB/s,写入速度最多30MB/s。这个参数在防止备份任务拖慢正常业务时非常有用。

最后强调一点,资源限制是运维的兜底手段,不是性能优化的替代品。如果一个服务频繁触发资源限制,说明要么是配置不合理,要么是代码本身有问题。正确的做法是先通过监控找出资源瓶颈,优化代码或者调整架构,然后用资源限制作为最后的保险丝。只靠限制不解决问题,只会让服务在达到限制时直接挂掉,用户看到的还是故障。