这篇是服务器运维笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。
进程突然消失,日志里没有任何异常,systemctl status 显示 killed 或者干脆 inactive (dead)——这时候最容易被误导的方向是「程序有 bug 崩了」。实际上很可能是内核把它杀了。
我在一台 3.8 GB 内存的小机器上排查过好几次。最近一次的真实记录:
[529021.651374] Out of memory: Killed process 1461869 (python3) total-vm:2542300kB,
anon-rss:2529504kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:5000kB oom_score_adj:0一个 python3 进程吃到了 2.5 GB anon-rss,然后被内核杀掉。这台机器总共 3835 MB。
这篇讲三件事:怎么确认是 OOM、那行日志每个字段什么意思、以及怎么让它别再发生。
一、先确认到底是不是 OOM
第一件事永远是查内核日志,不是查应用日志。
dmesg | grep -i "killed process"实测这台机器的输出(最近 3 条):
[529021.651374] Out of memory: Killed process 1461869 (python3) total-vm:2542300kB, anon-rss:2529504kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:5000kB oom_score_adj:0
[529099.143488] Out of memory: Killed process 1462964 (python3) total-vm:2285420kB, anon-rss:2272596kB, file-rss:0kB, UID:0 pgtables:4504kB oom_score_adj:0
[960123.301888] Out of memory: Killed process 281498 (python3) total-vm:2296232kB, anon-rss:2132456kB, file-rss:4kB, shmem-rss:0kB, UID:0 pgtables:4232kB oom_score_adj:0数一下次数:
dmesg | grep -ci "out of memory"4四次。 全是 python3,全是 2.1~2.5 GB。
踩坑点:dmesg 在某些发行版上普通用户读不到,会输出空。这时要么 sudo dmesg,要么读持久化日志:
# systemd 系统
journalctl -k | grep -i "killed process"
# 或按时间窗口
journalctl -k --since "1 day ago" | grep -i "out of memory"dmesg 是环形缓冲区,重启就没了。如果机器刚重启过,dmesg 里查不到,但 journalctl -k 可能还在(取决于 journald 的持久化配置)。
二、那行日志每个字段什么意思
以这条为例:
Out of memory: Killed process 1461869 (python3) total-vm:2542300kB, anon-rss:2529504kB,
file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:5000kB oom_score_adj:0| 字段 | 含义 | 怎么用 |
|---|---|---|
1461869 | PID | 已经不存在了,只能用来对日志时间 |
(python3) | 进程名 | 先确认是不是你的业务进程 |
total-vm | 虚拟地址空间总量 | 通常没用,虚拟内存可以远超物理内存 |
anon-rss | 匿名页常驻内存 | 这才是真实占用,看这个 |
file-rss | 文件映射的常驻页 | 一般很小,代码段和 mmap 的文件 |
shmem-rss | 共享内存常驻页 | 用了 shm/大页的会很大 |
pgtables | 页表本身占的内存 | 内存碎片化严重时会异常大 |
oom_score_adj | 优先级调整值 | -1000 到 1000,越大越容易被杀 |
关键判断:anon-rss 接近 total-vm,说明是真实的内存分配(堆上的数据),不是 mmap 映射出来的虚拟空间。上面三条都是这样(2.5 GB vs 2.54 GB),说明程序真的把 2.5 GB 数据读进内存了。
如果反过来,total-vm 很大但 anon-rss 很小,那是正常的——很多运行时(JVM、Go、Python)会预留大量虚拟地址空间。
pgtables:5000kB 也值得注意。5 MB 页表意味着进程有大量分散的内存页,通常是内存碎片。这种进程往往不是「用得多」,而是「用得散」。
三、内核是依据什么挑中它的
oom_score 决定了谁被杀。每个进程都有:
for f in /proc/self/oom_score /proc/self/oom_score_adj /proc/self/oom_adj; do
printf "%-28s " $f; cat $f
done实测:
/proc/self/oom_score 666
/proc/self/oom_score_adj 0
/proc/self/oom_adj 0oom_score(0~1000)是内核算出来的,主要由内存占用比例决定;oom_score_adj(-1000~1000)是你可以调的偏移量;oom_adj是老的、已废弃的接口,别用。
找当前最容易被杀的进程:
for p in /proc/[0-9]*; do
[ -r "$p/oom_score" ] || continue
printf "%5s %8s %s\n" "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/oom_score_adj 2>/dev/null)" "$(cat $p/comm 2>/dev/null)"
done | sort -rn | head -10这个排序就是内核的候选名单顺序。排第一的基本就是下次会被杀的。
四、让它别再发生:四种手段,按性价比排
手段 1:给关键进程设 oom_score_adj = -1000(最省事)
# 临时生效
echo -1000 > /proc/$(pidof sshd)/oom_score_adj
# systemd 服务里永久生效
systemctl edit myservice在打开的编辑器里写:
[Service]
OOMScoreAdjust=-1000注意:-1000 表示「永不杀」。⛔ 别给业务进程设 -1000——那只是把被杀的对象换成了别人(可能是 sshd 或数据库)。这个值应该留给 sshd、systemd 本身这类「死了就登不上机器」的进程。
手段 2:用 cgroup 给服务设内存上限(最推荐)
systemd 245 支持 MemoryMax,超了只杀这个 cgroup 里的进程,不影响整机。
先看当前值:
systemctl show -p MemoryMax -p MemoryHigh -p OOMPolicy nginx实测(默认全是无限):
OOMPolicy=stop
MemoryHigh=infinity
MemoryMax=infinity设置上限:
systemctl edit myservice[Service]
MemoryMax=512M
MemoryHigh=400M
OOMPolicy=stopMemoryHigh是软限制:超过后内核会强力回收(让进程变慢),但不杀;MemoryMax是硬限制:超过直接杀,且只杀这个 cgroup 内的;OOMPolicy=stop表示 cgroup 内 OOM 时把整个服务停掉(而不是只杀一个进程,留下半死不活的状态)。
这是最该用的手段,因为它把「整机 OOM」变成了「单个服务重启」,故障范围小了一个数量级。
手段 3:加 swap(有争议,但对小机器有用)
free -m实测这台机器:
total used free shared buff/cache available
Mem: 3835 1283 260 9 2291 2277
Swap: 0 0 0Swap 是 0。 没有 swap 意味着内存一旦用满,内核只有一条路:杀进程。
加一个 2 GB 的 swap 文件:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab验证:
swapon --show
free -m踩坑点:swap 不是万能药。如果进程是持续增长的(内存泄漏),加 swap 只是把「立刻被杀」变成「慢慢变卡,最后连 swap 也用满还是被杀」。swap 的正确用途是吸收突发峰值,不是掩盖泄漏。
另外 vm.swappiness 默认 60,对小内存机器建议调到 10~20,让内核尽量不把热数据换出去:
sysctl -w vm.swappiness=10手段 4:找根因(唯一真正的解法)
上面三条都是「控制损失」,不解决「为什么它要吃 2.5 GB」。
Python 进程吃到 2.5 GB,最常见的四个原因:
| 原因 | 特征 | 怎么查 | |
|---|---|---|---|
| 一次性读入大文件 | anon-rss 突然涨到文件大小量级 | 查代码里有没有 read() / readlines() | |
| 无界缓存/字典 | 缓慢单调增长,永不回落 | 加 len(cache) 日志 | |
| 生成器误用成 list | list(gen) 把整个结果集物化 | 搜 list( 包生成器的地方 | |
| 子进程/线程泄漏 | 进程数或线程数持续涨 | `ls /proc/PID/task \ | wc -l` |
看进程内存的时间序列(不用装任何工具):
PID=$(pidof myservice)
while true; do
echo "$(date +%H:%M:%S) $(awk '/VmRSS/{print $2, $3}' /proc/$PID/status)"
sleep 5
doneVmRSS 单调上涨且不回落 = 泄漏。锯齿形(涨到某个值又掉下来)= 正常的批量处理峰值。
五、怎么验证做对了
改完配置,做一次受控压测,别等生产环境再出问题。
# 1. 确认上限已生效
systemctl show -p MemoryMax -p MemoryHigh -p OOMPolicy myservice
# 2. 确认 swap 已启用
swapon --show
free -m
# 3. 观察 PSI(内存压力),这是比 free 更准的指标
cat /proc/pressure/memory实测这台机器的 PSI:
some avg10=0.00 avg60=0.00 avg300=0.00 total=138645986
full avg10=0.00 avg60=0.00 avg300=0.00 total=59592289some = 至少有一个任务因为等内存而卡住的时间占比;full = 所有任务都卡住的时间占比。full 长期大于 0 就说明内存真的不够了。
这是比 free 好用得多的指标:free 里的 available 再大,只要 PSI 的 full 在涨,就说明实际有压力;反过来 free 看着很少但 buff/cache 可回收、PSI 为 0,那就是健康的。
第 4 步,故意触发一次,确认隔离有效:
# 在一个 cgroup 里跑一个吃内存的程序,确认只有它被杀
systemd-run --scope -p MemoryMax=100M python3 -c "a=bytearray(200*1024*1024)"实测输出:
Running scope as unit: run-r258b23d3d64041bc9f1ce1255cf99392.scope
Killed一个 200 MB 的分配,被 100 MB 的硬上限立刻截断,只杀这一个 scope——机器上其它服务完全不受影响。这就算验证通过了。
对比一下不设上限时会发生什么:同样的分配会真的吃满 200 MB,如果整机剩余内存不够,内核就要在全局挑一个进程杀——可能就是你的数据库或 sshd。
另外看一眼这台机器当前的候选名单(oom_score 降序前 5):
for p in /proc/[0-9]*; do
[ -r "$p/oom_score" ] || continue
printf "%5s %8s %s\n" "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/oom_score_adj 2>/dev/null)" "$(cat $p/comm 2>/dev/null)"
done | sort -rn | head -5实测:
734 0 python3
708 0 AdGuardHome
706 0 python3
671 0 php-fpm
671 0 php-fpm排第一的 python3 就是下一次最可能被杀的。这份名单比任何监控面板都直接——它告诉你「如果再 OOM 一次,谁先死」。
六、完整流程回顾
# 1. 确认是不是 OOM(不是查应用日志)
dmesg | grep -i "killed process"
journalctl -k --since "1 day ago" | grep -i "out of memory"
# 2. 读那行日志:重点看 anon-rss,不是 total-vm
# anon-rss ≈ total-vm ⇒ 真实内存分配,不是虚拟空间
# 3. 看谁最容易被杀
for p in /proc/[0-9]*; do printf "%5s %s\n" "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/comm 2>/dev/null)"; done | sort -rn | head
# 4. 保护关键进程(只给 sshd 这种)
echo -1000 > /proc/$(pidof sshd)/oom_score_adj
# 5. 给业务服务设硬上限(最推荐)
systemctl edit myservice # MemoryMax=512M / OOMPolicy=stop
# 6. 有 swap 吗?没有就加,但别拿它掩盖泄漏
free -m; swapon --show
# 7. 用 PSI 判断真实压力
cat /proc/pressure/memory
# 8. 找根因:VmRSS 是否单调上涨
awk '/VmRSS/{print $2, $3}' /proc/$(pidof myservice)/statusOOM 不是一个「需要修 bug」的故障,它是一个容量与隔离问题。绝大多数「进程莫名消失」都能靠第 5 步(cgroup 硬上限)从「整机雪崩」降级成「单服务重启」——这是投入产出比最高的一步。