2G 内存被 OOM 杀了 5 次,swap 却空着 94%:swappiness=0 挖的坑(2026 实操)

这台 2 核 1.9G 的机器跑着 WordPress、MySQL、Redis,还有几个 Docker 服务,74 天没重启,看着挺稳。今天翻 dmesg 才发现,mysqld 被内核 OOM killer 杀了 5 次,最近一次在 9 月 2 号凌晨。

被杀的时候内存确实紧张,奇怪的是另一个数字:那 5 次现场记录里,Free swap 分别是 1,978,680 kB、1,981,496 kB、1,981,752 kB、1,588,948 kB,2G 的 swap 空着 94% 上下,内核手上有的是地方放东西,还是选了杀进程。原因就写在 /etc/sysctl.conf 第 1 行:vm.swappiness = 0。

什么时候该查这个

小内存 VPS(1G 到 2G 这档)跑 MySQL 或者别的吃内存的服务,机器上有 swap,但 free -m 里 Swap 那行的 used 常年在低位,日志里偶尔冒出 OOM。本站环境是 CentOS 7、内核 6.5.9-1.el7.elrepo.x86_64、MySQL 5.7.40,swap 是面板建的 2G 文件 /www/swap。需要 root,内核要在 3.5 以上,下面会说为什么。

第一步:分清内存不够,还是内核不肯动手

free -m
swapon --show
sysctl vm.swappiness
dmesg -T | grep -c "Out of memory"

本站输出:

              total        used        free      shared  buff/cache   available
Mem:           1887         890          83         221         914         592
Swap:          2047         125        1922

NAME      TYPE SIZE  USED PRIO
/www/swap file   2G 125.3M   -2

vm.swappiness = 0
5

swap 有 2G,used 只有 125M,OOM 却发生了 5 次。这就不是内存崩了,是内核严格按你写的参数办事。

第二步:读 OOM 那一刻的内存账

每次 OOM,内核都会把当时各内存分区的状态打进 dmesg,重点看 inactive_anon 和 Free swap 两项:

dmesg -T | grep "Out of memory: Killed"
dmesg -T | grep -E "inactive_anon|Free swap"

9 月 2 号 00:57 那次:

Node 0 DMA32 free:44180kB ... active_anon:276668kB
  inactive_anon:1344608kB active_file:0kB inactive_file:29240kB
Free swap  = 1588948kB
Total swap = 2097148kB

非活跃匿名页堆了 1.28G,这些页随时可以换出去;可回收的文件缓存只剩 28M,早被榨干了;swap 里躺着 1.5G 空闲。内核宁肯干掉一个占 186M 的进程,也不碰那 1.28G。

这不是 bug,是 Linux 3.5 之后改了行为。内核 commit fe35004(mm: avoid swapping out with swappiness==0)把 swappiness=0 从「尽量少换,必要时还是换」变成「除非空闲内存和文件缓存一起被压到高水位线以下,否则根本不扫匿名页」。所以 swappiness=0 不等于关掉 swap,等于把 swap 押到最后一刻,而那一刻通常已经来不及了。

第三步:换把尺子,别用 ps 的 RSS 找真凶

知道内核在动手,还得知道它动手的对象。ps aux --sort=-rss 排下来,本站 6 个 php-fpm 子进程各占 150M 上下,合计 900M 出头,看着 PHP 才该挨刀。用 PSS 重算一遍再看:

for p in $(ls /proc | grep -E "^[0-9]+$"); do
  if [ -r /proc/$p/smaps ]; then
    s=$(awk "/^Pss:/{n+=\$2} END{print n+0}" /proc/$p/smaps)
    echo "$s $(cat /proc/$p/comm)"
  fi
done > /tmp/pss.tmp
awk '{a[$2]+=$1; t+=$1} END {for (n in a) printf "%8.1f MB  %s\n", a[n]/1024, n;
  printf "%8.1f MB  全系统合计\n", t/1024}' /tmp/pss.tmp | sort -rn | head -7
   972.7 MB  全系统合计
   321.3 MB  mysqld
   236.3 MB  php-fpm
    84.6 MB  BT-Panel
    59.8 MB  monitor
    54.3 MB  nginx

RSS 把进程映射到的共享页整份算在自己头上,php-fpm 之间的 opcache 和动态库全是共享的,6 份 RSS 加起来 900M,按 PSS 分摊下来每个只占 39M,合计 236M。真正独吞内存的是 mysqld 那 321M。这也对上了内核的选择:它按 oom_score 挑人,本站 mysqld 的 score 是 721,php-fpm 子进程 690,5 次 OOM 杀的都是分最高的那个。

第四步:改掉 swappiness

cp -a /etc/sysctl.conf /root/sysctl.conf.bak-$(date +%Y%m%d-%H%M)
sed -i '1s/^vm.swappiness = 0$/vm.swappiness = 10/' /etc/sysctl.conf
sysctl -p
sysctl vm.swappiness      # vm.swappiness = 10

为什么填 10:swap 是云盘上的一个文件,随机 IO 比内存慢几个数量级,指望它扛性能不现实,但写上 10 之后,内存吃紧时内核会先把冷掉的匿名页推出去,而不是直接杀进程。为什么不填 60(内核默认值):2G 内存配 2G swap,60 会让内核倾向于来回搬页,换入换出带来的 IO 等待有时比杀掉一个进程更难受。值越小越保守,10 是这档配置的折中。

第五步:在 cgroup 沙盒里把机制验一遍

改生产参数得有证据。cgroup v1 的 memory 控制器能给单个进程设内存上限和 swappiness,出事只杀沙盒里的进程,宿主的服务不受影响:

d=/sys/fs/cgroup/memory/kt_test
mkdir -p $d
echo 200M > $d/memory.limit_in_bytes        # 内存上限
echo 600M > $d/memory.memsw.limit_in_bytes  # 内存+swap 合计上限
echo 10   > $d/memory.swappiness            # 对照组这里改成 0

python3 -c "
import os, time
open('$d/tasks','w').write(str(os.getpid()))
buf = bytearray(350*1024*1024)
for i in range(0, len(buf), 4096): buf[i] = 1
print('还活着'); time.sleep(8)
"
rmdir $d

同一段代码、同一个上限,只有 swappiness 不同,本站两组结果:

swappiness=0    1.0 秒后进程被 SIGKILL,oom_kill 1,宿主 pswpout 增量 0
swappiness=10   跑满 22 秒正常退出,峰值 rss 198.4M、峰值 swap 150.8M,
                宿主 pswpout +129,326 页(约 505M)、pswpin +89,165 页,oom_kill 0

0 的那组一个字节都没换出去,直接杀;10 的那组把冷页推给 swap,进程活到跑完。这就是那 5 次 OOM 的缩小版。

怎么确认真的生效

先看两处值是否一致,只改文件不算数:

sysctl vm.swappiness
cat /proc/sys/vm/swappiness

再盯三个计数器,长期观察靠它们:

grep -E "pswpin|pswpout" /proc/vmstat       # swap 换入/换出累计页数
free -m | grep Swap                         # 当前占用
dmesg -T | grep "Out of memory" | tail -1   # 最近一次 OOM 的时间

本站 74 天累计换出 169,005 页(约 660M)、换入 35,679 页,swap 停在 125M,节奏平缓。之后看到换出上涨不用慌,那正是调整想要的结果;要警惕的是换入持续飙高,说明页被搬出去又频繁搬回来,那时该减内存占用,而不是继续拧参数。回滚两条命令:

sysctl -w vm.swappiness=0
cp -a /root/sysctl.conf.bak-* /etc/sysctl.conf && sysctl -p

几个真实的坑

sysctl -w 只改内核当前值,重启就还原,必须落进 /etc/sysctl.conf。CentOS 7 由 systemd-sysctl 在开机时应用这些参数,本站 systemctl is-enabled systemd-sysctl 返回 static,它是被 target 拉起来的,别把配置写到不会被读的目录里。

swap 文件要确认 /etc/fstab 里有对应那行,本站是 /www/swap swap swap defaults 0 0,少了它重启后 swapon --show 会是空的,这时 swappiness 填多少都没用。

有 swap 不等于可以给 MySQL 加 buffer pool,swap 是保命用的,把 innodb_buffer_pool_size 顶到物理内存之上只是把磁盘 IO 换了个地方发生。该减的是静态上限:本站 28 张表的库全是 InnoDB,一张 MyISAM 都没有,而 my.cnf 里 key_buffer_size = 128M 还挂着,那是 MyISAM 键缓存的上限,改前先用 SHOW STATUS LIKE 'Key_blocks_used' 看实际用掉多少,以你的环境为准。

dmesg 是环形缓冲,本站只剩 1,842 行,更早的 OOM 记录已经被冲掉,journalctl -k 也只有 160 行,想留证据得早点把 journald 配成持久化。这次能查清一半靠运气,另一半靠 mysqld 的 err 日志:每次被杀之后都有 Starting crash recovery,5 次对得整整齐齐,站点数据库分别停摆 29 秒、33 秒、14 分 6 秒、97 秒、110 秒,那份日志里累计 108 次崩溃恢复。OOM 的代价不是重启一个进程,是数据库收拾残局的那几分钟。

那次 14 分钟的停摆尤其值得看一眼,内存账的另一面写在WordPress 502 和白屏排障那篇里;不想事后翻日志的话,用 Docker 搭一套监控告警里的 Uptime Kuma 能在站点不通的几十秒内叫你,比 dmesg 里的时间戳便宜得多。机器上的磁盘账在服务器磁盘被谁吃掉了里量过,swap 文件本身就占着 2G,盘紧的话先别急着加。

这台机器买了两年,我一直以为 2G 内存在这种负载下不够用,今天才知道内存够,是内核被一个参数绑住了手。一个参数不占内存,代价却按分钟算,这台机器上最贵的一行配置就是它。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容