给长驻进程划一条内存红线
小内存机器上跑一个长驻进程,最大的不确定性是「它会不会把别的服务挤死」。
systemd 给了两个旋钮,名字很像,作用完全不同。
两个限制不是一个意思
MemoryHigh 是软限制。超过之后内核开始对这块 cgroup 施压:回收页缓存、压缩内存、让进程分配变慢。进程通常感觉不到,只是慢了一点。
MemoryMax 是硬限制。超过之后的下一步是 OOM killer,直接杀进程。
写在 drop-in 里:
[Service]
MemoryHigh=280M
MemoryMax=350M
两个数之间留了 70 MB。这段区间就是「先想办法、再动手」的缓冲:进程在这段空间里还有机会自己收缩(触发 GC、释放缓存),而不是一步跨到被杀。
撞上限之后会发生什么
如果服务配了 Restart=always,一次 OOM 会变成一场拉锯:
- 内存涨到
MemoryMax - 被杀
- systemd 拉起
- 启动过程中又涨到
MemoryMax - 回到第 2 步
从外面看,这不是「内存不足」,而是「这个服务偶尔重启」。日志里才会出现 OOM 的记录。
这种循环比单纯的崩溃更糟:每次重启都要重新加载数据、重建连接,CPU 也一起被拖住。
上限怎么定
一开始我以为是「按稳态、留两倍」。这台机器上的实测把这个说法推翻了。
这是站点进程跑了 25 小时之后的数据:
| 指标 | 值 | 说明 |
|---|---|---|
MemoryCurrent |
36 MB | 只有日常页面访问时的常驻 |
MemoryPeak |
60 MB | 25 小时内到过的最高点 |
ps 里的 RSS |
83 MB | 含共享页,比 cgroup 记的大 |
MemoryHigh |
260 MB | 我最终设的值 |
MemoryMax |
340 MB | 硬线 |
稳态 36 MB,留两倍是 72 MB。按那个数设,下面这件事会直接触发 OOM:
管理员打开访问日志页面时,会瞬时把 121 MB 的城市库读进内存。
那条路径每天可能只走一次,也可能一周不走一次。它不体现在 MemoryCurrent 里,也不体现在「跑了 25 小时的峰值」里——因为这段时间根本没人打开过那个页面。
所以正确的口径不是稳态,也不是历史峰值,而是最坏路径:
- 把「一次请求最坏会多读多少」列出来(这里是一个 121 MB 的 mmap 文件)
- 稳态 + 最坏增量 = 软限制的下界(36 + 121 ≈ 157 MB,留出回收空间后定 260 MB)
- 硬限制再往上留一档(340 MB),给 GC 和突发的分配
MemoryPeak 只有一个用处:证明你设的线还没被碰过。它证明不了你的线够不够高,因为没走过的路径不会留下峰值。
观察:
systemctl show <unit> -p MemoryCurrent -p MemoryPeak
cat /sys/fs/cgroup/system.slice/<unit>/memory.events
memory.events 里的 oom 和 oom_kill 计数是最直接的证据。这两个数如果一直在涨,说明上限设错了,或者进程真有泄漏。
注意 MemoryPeak 不是所有 systemd 版本都暴露成属性。cgroup v2 上直接读文件更可靠:
cat /sys/fs/cgroup/system.slice/<unit>/memory.peak
cat /sys/fs/cgroup/system.slice/<unit>/memory.events
这台机器现在的四个计数全是 0:
low 0
high 0
max 0
oom 0
oom_kill 0
high 0 的意思是:连「超过软限制、开始施压」都没发生过一次。线设对了就是这样——它安静到你会怀疑它没生效。
一个反例
MemoryMax 不要设成「刚好够用」。
刚好够用的意思是:正常运行时占用 95% 的上限。这样任何一次突发——一次大批量读取、一次 GC 延迟——都会直接触发硬限制。
界线要设在「正常运行时不会碰到,但真出问题能兜住」的位置。
这台机器上的实际配置
drop-in 里最终是这两行,外加一段注释说明为什么从 220 改到 260:
# 地理库改成按需加载后,闲时 RSS ~64 MB;
# 管理员打开访问日志会瞬时读入 121 MB 的 city.mmdb,峰值约 220 MB。
# 原来的 MemoryHigh=220M 正好卡在这个峰值上,会触发回收抖动。
[Service]
MemoryHigh=260M
MemoryMax=340M
上一版是 MemoryHigh=220M,看起来很有余量(是稳态的六倍),但它正好等于最坏路径的峰值。
卡在峰值上的后果不是被杀,是「每次打开那个页面都触发一轮内存回收」——页面能出来,只是那次请求会慢得没有规律,而且你查不出原因:oom_kill 是 0,日志里什么都没有。
这种「不报错只是变慢」的配置错误,比 OOM 更难查。所以软限制要比最坏路径再高一档,硬限制比软限制再高一档。
不是万能的
systemd 的内存限制是按 cgroup 算的,只管住被它启动的进程树。
如果进程自己 fork 出去、或者通过别的机制派生了进程,那部分可能不在同一个 cgroup 里。
另外它管的是「总量」,不管「为什么高」。设了上限之后还是要看趋势——上限只是防止拖垮别人,不是诊断工具。
先有观测,再有限制。反过来做,你只会知道「它又被杀了」。