掌握 OOM 策略:调优 Systemd 对内存不足事件的响应

学习使用 systemd 控制 Linux 的 OOM 杀手行为。本指南探讨了 `OOMScoreAdjust` 和 `OOMPolicy` 指令,通过影响低内存条件下哪些进程被终止来保护关键服务。掌握 systemd 的 OOM 调优,提升系统稳定性和弹性。

掌握 OOM 策略:调优 Systemd 对内存不足事件的响应

内存不足故障很少发生在方便的时候。批量导入遇到比平时大的文件、服务整夜泄漏内存、备份与流量高峰重叠、或部署使工作进程数量翻倍。当 Linux 无法为内存分配释放足够内存时,内核可能会调用 OOM 杀手并终止一个进程,以便机器继续运行。

令人不安的是,默认的受害者可能不是你选择的服务。在共享主机上,你可能更希望可重试的队列工作者先于主 API 死亡。在数据库服务器上,你可能希望 SSH 和监控保持存活,以便你能恢复机器。Systemd 为此类决策提供了两个旋钮:OOMScoreAdjust=OOMPolicy=

OOMScoreAdjust= 影响哪个进程被选中。OOMPolicy= 控制服务中的进程被杀死后 systemd 做什么。它们解决不同的问题,混淆它们会导致糟糕的运维手册。

内核在评分什么

每个 Linux 进程都有一个 OOM 分数,可在 /proc/<pid>/oom_score 查看。分数越高,进程越可能成为 OOM 受害者。内核从内存使用和其他上下文推导出该分数,然后应用来自 /proc/<pid>/oom_score_adj 的调整值。

Systemd 的 OOMScoreAdjust= 为其启动的进程写入该调整值。范围是 -10001000

  • -1000 提供最强保护,并有效禁用该进程的 OOM 杀死。
  • 负值使进程不太可能被杀死。
  • 正值使进程更可能被杀死。
  • 0 保持调整中立。

最安全的方法通常不是“保护所有重要的东西”。如果每个服务都受到保护,当主机内存已经不足时,内核的可选方案就更少。保护少数服务,并使可丢弃的工作更容易被杀死。

对于主要 API 服务,适度的调整通常就足够了:

[Service]
OOMScoreAdjust=-300

对于可以重试作业的队列工作者:

[Service]
OOMScoreAdjust=500

该工作者在内存压力下可能先死,但这正是重点。失败的作业可以回到队列。数据库宕机或主机不可达是更大的事故。

OOMPolicy 实际做什么

OOMPolicy= 并不将单元标记为“关键”,也不选择第一个要杀死的进程。支持的值有 continuestopkill

  • continue:systemd 记录 OOM 事件,如果还有任何进程存在,则保持单元运行。
  • stop:systemd 记录事件并干净地停止单元。
  • kill:如果单元中的一个进程被 OOM 杀死,该单元中的其余进程将作为一个组被杀死。

使用此设置来避免半死不活的服务。如果多进程 Web 服务丢失了一个工作者并在故障状态下继续接受流量,continue 可能会隐藏故障。OOMPolicy=kill 使故障明显,并让 Restart=on-failure 以干净状态恢复服务。

[Service]
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

对于带有辅助进程的批处理作业,stop 可能对剩余进程不那么突然:

[Service]
OOMPolicy=stop

内核选择的进程已经消失。stop 只影响 systemd 对服务其余部分的操作,所以不要依赖它作为优雅的保存点。长时间运行的作业应该自己检查点。

实用的调优模式

首先将服务分为三组。

第一,识别保持主机可恢复的服务:SSH、网络、监控和主要工作负载。只给最重要的服务适度的负调整。

第二,识别可以重试的服务:工作者、导入器、报告生成器、图像处理器、缓存预热器、开发辅助工具。给这些服务正调整。

第三,决定每个服务是否可以在一个进程被杀死后安全地继续运行。如果不能,使用 OOMPolicy=kill 和重启策略。

一个实际的工作者覆盖可能如下:

# /etc/systemd/system/image-worker.service.d/oom.conf
[Service]
OOMScoreAdjust=500
OOMPolicy=kill
Restart=on-failure
RestartSec=10s

一个主要的应用服务可能如下:

# /etc/systemd/system/api.service.d/oom.conf
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

我会避免使用 OOMScoreAdjust=-1000,除非你已经测试过故障模式。如果那个受保护的服务正是泄漏内存的那个,机器仍然需要一种恢复方式。

应用和验证更改

使用 drop-in 而不是编辑打包的单元文件:

sudo systemctl edit api.service

保存覆盖后,重新加载 systemd 并重启服务:

sudo systemctl daemon-reload
sudo systemctl restart api.service

检查合并后的单元和 systemd 看到的值:

systemctl cat api.service
systemctl show api.service -p OOMPolicy -p OOMScoreAdjust

然后检查运行中的进程:

PID=$(systemctl show api.service -p MainPID --value)
cat /proc/$PID/oom_score_adj
cat /proc/$PID/oom_score

oom_score_adj 应该匹配你配置的调整值。oom_score 会随着进程使用更多或更少内存而变化。

事故发生后,检查单元日志和内核日志:

journalctl -u api.service --since "1 hour ago"
journalctl -k --since "1 hour ago" | grep -i oom

在使用 systemd-oomd 的系统上,还要检查:

systemctl status systemd-oomd
oomctl

OOM 策略不是容量规划

OOM 调优是最后一道防线。你仍然需要内存限制、警报和足够的余量应对正常峰值。对于边界可预测的服务,考虑 cgroup 内存控制:

[Service]
MemoryHigh=1500M
MemoryMax=2G

MemoryHigh= 在硬限制之前施加压力。MemoryMax= 是上限。确切行为取决于 systemd 版本和 cgroup 设置,但操作思路很简单:在一个服务消耗整个主机之前限制它。

交换分区也需要同样的考虑。没有交换可能使短暂峰值变成突然的 OOM 杀死。太多慢速交换可能使主机存活但延迟变得无用。将 OOM 策略与交换、内存限制、重启行为和警报一起审查。

示例:一个主机,三个服务

假设一个小型生产主机运行一个 API、一个 Redis 缓存和一个后台报告工作者。报告工作者很有用,但可以重试工作。Redis 改善延迟,但应用程序仍然可以通过访问数据库来服务一些请求。API 是面向客户的服务。

一个合理的初步方案可能是:

# api.service
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
# redis.service drop-in,如果这个 Redis 实例只是缓存
[Service]
OOMScoreAdjust=0
OOMPolicy=kill
# report-worker.service
[Service]
OOMScoreAdjust=600
OOMPolicy=kill
Restart=on-failure

这并不能保证工作者在所有情况下都先死,但它明确了你的意图。如果报告工作者增长过大,它更容易成为目标。如果 API 丢失了它的一个进程,systemd 会杀死其余进程并干净地重启它。如果 Redis 只是缓存,你可以选择不大力保护它;如果 Redis 是你的主要数据存储,你会做出不同的决定。

这就是为什么 OOM 策略应该与服务角色绑定,而不是产品名称。“Redis”并不自动是关键或可丢弃的。“我们可以重建的缓存”和“会话状态的唯一副本”是不同的操作对象。

在不造成灾难的情况下测试

你不需要崩溃生产服务器来了解设置是否已应用。从检查开始:

systemctl show report-worker.service -p OOMScoreAdjust -p OOMPolicy
systemctl status report-worker.service

然后检查运行中的进程:

PID=$(systemctl show report-worker.service -p MainPID --value)
cat /proc/$PID/oom_score_adj

对于更深入的测试,使用具有相同 systemd 版本和 cgroup 模式的暂存主机或可丢弃的虚拟机。在那里运行受控的内存压力工具,而不是在共享的生产机器上。目标是确认大致行为:工作者更容易被杀死,主要服务不会半死不活,重启行为在日志中可见。

如果你使用容器,以你部署的相同形态进行测试。直接在 systemd 下运行的服务的行为与在具有自己内存限制的容器内的进程不完全相同。内核可能在主机全局内存不足之前就强制执行容器限制。在这种情况下,你的容器运行时、Kubernetes 或 cgroup 设置可能是决定什么先死的第一层。

事后分析事故

OOM 事件发生后,避免直接跳到“我们需要更多 RAM”。有时确实需要。有时缓存忘记了 TTL。有时部署改变了工作者并发数。有时持久化或备份活动导致写时复制内存激增。

查找三件事:

journalctl -k --since "2026-05-24 01:00" | grep -i oom
journalctl -u api.service --since "2026-05-24 01:00"
systemctl show api.service -p Result -p NRestarts

内核日志通常告诉你哪个进程被杀死。单元日志告诉你 systemd 如何反应。重启计数器告诉你服务是否干净恢复或反复重启。

然后将被杀死进程与你预期的优先级进行比较。如果受保护的服务在可丢弃的工作者之前死亡,检查工作者是否实际在你调优的单元下运行,覆盖是否已加载,以及是否有其他内存限制先触发。如果选中的受害者符合策略但事故仍然伤害了用户,你的服务分类可能需要改变。

记录原因,而不仅仅是值

OOM 设置很容易被忘记,因为它们安静地存在于单元 drop-in 中,直到糟糕的一天。在覆盖或你的基础设施仓库中留下简短注释,解释调整的原因。

[Service]
# 可重试的队列工作者。在主机压力下优先杀死此服务而不是 api.service。
OOMScoreAdjust=600
OOMPolicy=kill

该注释在事故审查期间节省时间。没有它,有人可能看到正的 OOM 分数并“修复”回零,而没有意识到这是一个有意的优先级决策。

还要记录你上次审查设置的时间。服务可能随时间改变角色。曾经处理可丢弃缩略图的工作者后来可能处理支付、导出或客户可见的作业。OOM 策略应该跟随当前风险,而不是服务的原始目的。

常见错误配置

一个错误配置是同时保护数据库、API、工作者、缓存、日志传输器和监控代理。这感觉谨慎,但它给内核的选择更少。确定优先级。

另一个错误配置是在不能容忍丢失子进程的服务上设置 OOMPolicy=continue。进程管理器、Web 服务器或自定义守护进程可能即使在部分工作负载消失后仍保持单元活动。如果你的负载均衡器只检查端口是否开放,流量可能继续流向降级的服务。

第三个错误配置是正调整而没有重试行为。如果你让一个服务容易被杀死,确保杀死它是可接受的。对于队列工作者,这意味着作业只在成功处理后确认。对于批处理作业,这意味着检查点。对于缓存预热器,这意味着缓存可以在以后重建。

最后,避免仅用自动重启来隐藏 OOM 事件。重启泄漏内存的服务可能争取时间,但也可能造成循环:内存攀升,服务死亡,用户看到周期性故障。添加关于重启次数和内存增长的警报,而不仅仅是进程状态。

简短运维手册

当你调优真实服务器时,使用可重复的检查清单:

  1. 列出恢复和用户流量所需的服务。
  2. 列出可以首先被杀死的可重试服务。
  3. 为可丢弃的工作添加正的 OOMScoreAdjust 值。
  4. 只为少数值得保护的服务添加适度的负值。
  5. 对不应部分运行的服务使用 OOMPolicy=kill
  6. 通过 systemctl show/proc 验证应用的值。
  7. 在 OOM 事件发生之前对内存压力发出警报。

目标不是让 OOM 事件无害。目标是让它们可理解。OOMScoreAdjust= 帮助选择受害者。OOMPolicy= 帮助定义单元其余部分发生什么。它们一起在内存已经耗尽时给你更可预测的故障顺序。