服务器内存够用却总 OOM:读懂 free 的 available、buff/cache 与 OOM Killer 调优实战

一台"内存不够"的服务器,其实内存够用

几乎所有用 Linux 建站的站长都经历过这个场景:登录服务器敲下 free -h,看到 used 那一栏把内存吃掉了一大半,available 却还剩不少;或者看到 buff/cache 数字很大,直觉上以为是"缓存把内存占满了"。更糟的是,某天 OOM Killer 突然杀掉了 MySQL 或 PHP-FPM,网站 502,日志里一行 Out of memory: Killed process,然后你就开始怀疑是不是该加钱升配。

很多时候,结论恰恰相反:内存其实是够的,问题出在参数配置和认知偏差上。Linux 的内存管理跟 Windows 很不一样,"空闲内存"少不代表紧张,"缓存占用"多是好事。真正决定会不会 OOM 的,是 overcommit 策略、swap 的使用倾向、以及单个进程能吃多少内存这些参数。

这篇把服务器内存这条链路讲透:怎么正确读 free 的输出、buff/cache 到底在干什么、OOM Killer 是怎么挑人的、以及针对个人站最常用的一套调优参数(swappiness、overcommit、PHP-FPM/MySQL 内存上限)。全程给可执行的命令,方便你对着自己的机器复现。

先学会正确地读 free:available 才是关键

先看一个典型的输出:

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       2.9Gi       208Mi        45Mi       700Mi       620Mi
Swap:          2.0Gi       410Mi       1.6Gi

很多人的第一反应是"used 2.9G,快满了"。但正确的读法是看最后一列 available——它才是"在不触发 swap 的前提下,新进程还能拿到多少内存"。这个值由内核计算,已经扣掉了不可回收部分,并把大部分 buff/cache 算作可回收。

所以上面这台机器真实情况是:还有 620Mi 可以给新进程用,不算紧张但也不宽裕。列的含义分别如下:

  • total:物理内存总量。
  • used:已被进程实际占用的内存,注意它不含 buff/cache。
  • free:完全没被使用的内存。这个数字小完全正常,Linux 会主动把空闲内存拿去做缓存,不用白不用。
  • buff/cache:内核缓冲区 + 文件页缓存,存放最近读写过的文件内容。它是可回收的——内存紧张时内核会自动丢掉干净的缓存页。
  • available:估算的可分配内存,唯一需要盯的数字

如果你用的是比较新的内核(3.14+),free 会自动输出 available。老内核没有这列,可以用 cat /proc/meminfoMemAvailable。至于 sharedSwap 行,前者是 tmpfs、共享内存等的占用,后者是交换分区的使用情况——swap 被用了几百 MB 完全不必恐慌,只有持续增长且伴随高 si/so 时才说明真的不够。

判断 swap 是否在被"高频读写"(这才是坏事),用 vmstat 看换入换出速率:

$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0 420732  21256   8432 706524    0    0     3     9  120  210  4  1 95  0  0

si(从 swap 换入)和 so(换出到 swap)长期接近 0,说明 swap 只是静静躺着当"安全垫",没有问题。如果这两个值持续是几百上千,那才说明物理内存真的被压榨了,进程在被反复换进换出,性能会烂到无法忍受(这就是所谓的 thrashing)。

buff/cache 到底在为你做什么

理解了缓存是可回收的,还要理解它为什么重要:cached 越大,磁盘读越少,网站越快。Nginx 读静态文件、MySQL 读 InnoDB 表空间、PHP 读代码文件,走的都是这层页缓存。这就是为什么"服务器跑了一段时间后 free 变小了"是健康的——内核在用闲置内存加速你的磁盘 IO。

顺带说一个个人站特别容易踩的坑:MySQL 的 innodb_buffer_pool_size 最好设成物理内存的 50%~70%。这个参数是 MySQL 自己管理的缓存池,不算在 buff/cache 里,而是算在 used 里。你可能会发现一台 4G 的机器,MySQL 一启动 used 就涨了 2G——那不是内存泄漏,是 buffer pool 正常占用。设太大会让系统可用内存不足触发 OOM,设太小则 MySQL 疯狂读盘。用下面这条看当前占用:

mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
mysql -e "SELECT * FROM sys.memory_by_thread_by_current_bytes LIMIT 5;" 2>/dev/null

如果发现 used 莫名很大但找不到元凶,用 psRSS(常驻内存)排序,而不是看 VSZ(虚拟内存,几乎没意义):

ps aux --sort=-rss | head -15
# 更直观的百分比排序
ps -eo pid,ppid,rss,pmem,comm --sort=-rss | head -15

OOM Killer 是怎么挑人的,怎么防止它杀错

available 真的耗尽、内核无法满足内存分配时,就会触发 OOM Killer。它在 dmesg / journal 里留下一行:

dmesg -T | grep -i "killed process"
# 典型输出
# Out of memory: Killed process 12345 (mysqld) total-vm:...

OOM Killer 挑谁下手,靠的是每个进程的 oom_score。分数越高越先被杀,而分数大致和进程的内存占用、以及 oom_score_adj 这个可调参数有关:

# 查看某进程的 oom 分数
cat /proc/$(pgrep -f mysqld | head -1)/oom_score
# 查看可调优先级(-1000 到 1000,越大越容易被杀)
cat /proc/$(pgrep -f mysqld | head -1)/oom_score_adj

默认情况下,吃内存最多的进程分最高——所以 MySQL 和 PHP-FPM 这两个"大胃王"经常首当其冲。关键服务被误杀会让网站整个挂掉。解决办法是给你不想被杀的进程降低优先级

# 让 sshd 极不容易被 OOM 杀掉(调试时的保命设置)
echo -1000 > /proc/$(pgrep -x sshd | head -1)/oom_score_adj

不过要注意,/proc 下的设置在进程重启后失效。持久化的正确姿势是给 systemd 服务加配置:

# systemctl edit mysql(生成 override.conf)
[Service]
OOMScoreAdjust=-500

这样 MySQL 每次启动都会自动带上这个优先级,被 OOM Killer 选中的概率大大降低。相对的,如果你有个可牺牲的批处理任务,可以给它设正值,让它优先被牺牲。

另一种更主动的方案是给关键服务设 MemoryMax,让 cgroup 在它超限时只杀它自己的子进程,而不是把系统搞崩:

systemctl edit php8.2-fpm
[Service]
MemoryMax=1G
MemoryHigh=800M

MemoryHigh 是软限制,超了会被"节流"并尽量回收;MemoryMax 是硬限制,超了直接 OOM 掉这个服务。这套配置在单机跑多个服务的个人站上非常有用,能把故障范围限制在一个服务内。

三个最值得调的参数

1. vm.swappiness:控制内核有多爱用 swap

cat /proc/sys/vm/swappiness     # 默认 60
sysctl -w vm.swappiness=10      # 临时生效

swappiness 表示"内存回收时,倾向于换出匿名页(进程内存)还是丢弃文件缓存",取值 0~100。60 是面向桌面/通用的默认值,对服务器偏高。调低到 10,内核会优先丢弃文件缓存、尽量不把进程换到 swap,从而避免站点因为换页而卡顿。但注意:不要设成 0。设为 0 时,内存压力大时内核几乎不用 swap,容易直接触发 OOM;设 10 是"能用但很不情愿",兼顾了性能和安全。持久化写进 /etc/sysctl.d/99-tuning.conf

vm.swappiness = 10

2. vm.overcommit_memory:让内核怎么判断"还能不能再给内存"

cat /proc/sys/vm/overcommit_memory
# 0 = 启发式(默认),1 = 永远允许,2 = 严格限制

默认的 0 是启发式判断,大多数场景没问题。但有一类经典故障必须调整:Redis 在启动时会检测到 overcommit 是 0 并给出警告,在极端 fork 场景下可能导致 fork 失败(比如做 RDB 持久化时)。Redis 官方推荐设成 1:

vm.overcommit_memory = 1

如果你跑 Redis 给网站做缓存,这条一定要加,否则某次持久化时 fork 失败,缓存就挂了。用 cat /proc/sys/vm/overcommit_ratio 可以看到严格模式(2)下的比例参数,一般不用动。

3. swap 空间本身:小机器一定要有

很多 VPS 默认不给 swap,或者只有 512M。对 2G 内存的小机器,建议至少配一块 等于内存大小的 swap 文件(大内存机器配 2~4G 就够)。创建 swap 文件:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
swapon --show    # 确认生效

swap 的定位不是"提升性能",而是救命垫:在内存峰值时刻(比如 MySQL 做备份、PHP-FPM 突然流量涌入),它能让内核有机会缓冲而不是立刻 OOM。宁可让它躺在那里几乎不用,也不能没有。

一次真实的 OOM 排查流程

把上面串起来,假设网站突然 502,你要查是不是内存问题,按这个顺序走:

  1. 看有没有 OOM 记录dmesg -T | grep -i "out of memory",或者 journalctl -k | grep -i oom。有记录就确认是内存问题,能直接看到被杀的是哪个进程。
  2. 看当前内存状况free -havailablevmstat 1 5si/so 是否持续非零。
  3. 找内存大户ps -eo pid,rss,pmem,comm --sort=-rss | head,重点看 MySQL、PHP-FPM 的 children 总数。
  4. 算 PHP-FPM 的理论上限pm.max_children × 单进程平均内存(ps 看 PHP-FPM 子进程 RSS)。如果 max_children=50、每个进程 60MB,那就是 3G,4G 的机器肯定扛不住——这种要减 max_children 或升级,OOM 是必然的。
  5. 确认是峰值还是常态:如果 only 在半夜备份时 OOM,考虑给备份任务加 MemoryMax 或错开时间;如果是常态,就是配置问题。

这套流程比"一看到内存高就加钱升配"靠谱得多。我见过太多站点其实是 PHP-FPM 的 max_children 设得离谱(比如照抄了 8 核机器的 100),在 2 核小机上永远 OOM,改一个数字就彻底解决了。

小结:内存调优的本质是"分清三件事"

把这篇文章压缩成三句话:

  • 缓存不是占用buff/cache 大是好事,free 小是正常,真正该看的是 available
  • swap 不是敌人si/so 低就是安全的;vm.swappiness=10 兼顾性能与安全,别设 0。
  • OOM 是可以预防的。控制关键服务的 OOMScoreAdjustMemoryMax,把 pm.max_childreninnodb_buffer_pool_size 按物理内存算好,OOM 基本不会再发生在核心服务上。

个人站长没必要背下一堆内核参数,但把这几个关键数字搞清楚,能避免大量"莫名其妙就挂了"的事故。毕竟对一台 2C4G 的小机器来说,配置得当和配置错误,能扛的并发可以差好几倍。

Last modification:September 23rd, 2026 at 09:27 pm

Leave a Comment