这台 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 内存在这种负载下不够用,今天才知道内存够,是内核被一个参数绑住了手。一个参数不占内存,代价却按分钟算,这台机器上最贵的一行配置就是它。



![[教程][服务器]在阿里云服务器上卸载AliYunDunMonito(阿里盾)-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2025/12/114612929920251229005735.png)

![[免费][教程]子比主题美化,优化教程-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2026/09/3b06fd6a0520260920160148.webp)
![[免费][插件]将WordPress图片上传到LskyPro蓝空图床的插件-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2026/09/b9e1998d8420260920160152.webp)

![[教程][笔记]Centos7宝塔下安装ImageMagick,无法安装webp扩展解决办法-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2026/09/6ebaa1156520260920155617.webp)
![[资源][教程]AI SEO - WordPress插件-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2026/01/9f396016b220260112151830.png)



暂无评论内容