◈ 夜宵吧

给长驻进程划一条内存红线

小内存机器上跑一个长驻进程,最大的不确定性是「它会不会把别的服务挤死」。

systemd 给了两个旋钮,名字很像,作用完全不同。

两个限制不是一个意思

MemoryHigh 是软限制。超过之后内核开始对这块 cgroup 施压:回收页缓存、压缩内存、让进程分配变慢。进程通常感觉不到,只是慢了一点。

MemoryMax 是硬限制。超过之后的下一步是 OOM killer,直接杀进程。

写在 drop-in 里:

[Service]
MemoryHigh=280M
MemoryMax=350M

两个数之间留了 70 MB。这段区间就是「先想办法、再动手」的缓冲:进程在这段空间里还有机会自己收缩(触发 GC、释放缓存),而不是一步跨到被杀。

撞上限之后会发生什么

如果服务配了 Restart=always,一次 OOM 会变成一场拉锯:

  1. 内存涨到 MemoryMax
  2. 被杀
  3. systemd 拉起
  4. 启动过程中又涨到 MemoryMax
  5. 回到第 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 里。

另外它管的是「总量」,不管「为什么高」。设了上限之后还是要看趋势——上限只是防止拖垮别人,不是诊断工具。

先有观测,再有限制。反过来做,你只会知道「它又被杀了」。