服务器卡到 SSH 都打不出字:swap 抖动与内存预算的排查次序

下午三点,SSH 连着连着突然卡住,输入一个字母要等十秒才回显;顺手敲个 ls,命令提示符半天不出来。与此同时,网站前台照常响应,Nginx 的 access.log 还在唰唰地写。这种「网站没事、SSH 卡死」的现象,十次里有八次是同一类原因:机器在用 swap,而且换得很难受。

很多站长看到 swap 被用掉几个 G 就慌了,直接 swapoff -a,结果把内存彻底压爆,OOM killer 开始随缘杀进程。这篇文章想讲清楚三件事:swap 到底什么时候是好事、什么时候是坏事;怎么用几个数字判断你现在的卡顿是不是 swap 造成的;以及在不重启、不冒险的前提下,怎么把 swap 的负面效应压下去。

一、先搞清楚 swap 是什么,以及它不是「虚拟内存」

swap 是一块磁盘或分区,内核可以把暂时用不到的内存页(匿名页,比如进程堆、栈)整页挪到这块盘上,把物理内存腾出来给更需要的进程。等到那个进程再次访问这一页,就会触发一次「缺页」,内核再把它读回来。整个过程对应用是透明的。

常见的误解是把 swap 当成「内存不够的补充」。它不是。磁盘的随机读取速度比内存慢三到五个数量级:普通云盘的随机 4K 读大概 100 微秒到 1 毫秒,NVMe 大约 80 微秒,内存是 100 纳秒级别。也就是说,一次 swap in 的代价,可能等于内存访问几千次。所以 swap 的作用不是「让你能跑更多进程」,而是「让暂时闲着的内存别浪费在占着不动的地方,同时在内存瞬时紧张时提供一个缓冲,避免立刻杀进程」。

把它比作一张书桌更贴切:内存是你桌上摊开的资料,swap 是身后书架上合起来的活页夹。书架可以放很多,但每次去拿都要站起来走过去。如果你正在高速查阅,桌上资料被换到书架去,速度就毁了。如果你只是在写总结,桌上一半资料暂时用不到,换到书架完全没问题。

二、三个数字判断卡顿是不是 swap 引起的

不要凭感觉。登录上去先跑两条命令:

free -m

输出大概长这样:

               total        used        free      shared  buff/cache   available
Mem:            3936        3120         210          12         605         480
Swap:           2047        1420         627

看三列:available 比 free 重要得多,它估算的是「不考虑回收缓存,实际还能直接分给新进程的内存」;Swap 那一行的 used 是已占用的 swap;但真正说明问题的不是「用了多少」,是「换进换出还在不在动」。

所以第二条命令更关键:

vmstat 1 5

看 si 和 so 两列,单位是 KB/秒。持续为 0 说明 swap 里放着的东西很安稳,只是「躺」在那里,不影响性能。如果 si(swap in)持续在几百甚至上千 KB/s 以上,同时 bi 也偏高,那就是典型的 swap thrashing(抖动):进程需要的数据被换出去又被换回来,系统在做无用功,磁盘 IO 被吃满,延迟飙升。SSH 卡住基本就是这个状态——sshd 或者 bash 的某些页被换出去了,你每敲一个键它都要去磁盘上把页换回来。

第三个数字在 /proc/vmstat 里,是累计缺页统计:

grep -E '^(pswpin|pswpout|pgmajfault)' /proc/vmstat

pswpin 是累计换入页数,pswpout 是累计换出页数,pgmajfault 是「主缺页」次数——所谓主缺页,就是缺的页必须从磁盘读回来,而不是从内存里别的缓存处直接拿。这个数字如果在几分钟内涨了几百上千,说明系统正处在密集的换页活动中。反过来,pgmajfault 一天只涨十几个,那 swap 用掉两 G 也没什么大不了。

把这三个数放在一起,可以很快分出三种情况:

  • swap 占用高、si/so 为 0、pgmajfault 几乎不涨 → 只是有冷页躺在 swap 里,无害,别动它。
  • swap 占用高、si/so 周期性波动、pgmajfault 缓慢增长 → 有压力但还没崩,该查是谁在吃内存了。
  • swap 占用高、si/so 持续剧烈、pgmajfault 疯涨 → 已经在抖动,先做应急处理(见下一节),再查根因。

三、swap 被反复使用,通常是这四类进程在占内存

知道系统在抖,下一步要找出「谁把内存吃掉了」。我的经验里,小站服务器上原因高度集中在这几类:

第一类,数据库的缓冲区开得比物理内存还大。 MySQL 的 innodb_buffer_pool_size 加上 key_buffer_size 加上各种 per-connection buffer,加起来超过物理内存的 70%,只要并发一上来,连接 buffer 一分配,机器就立刻开始换页。这类问题最坑的地方在于平时看不出来——低负载时 buffer pool 只是个预留,不会真占满,等到高峰才爆。

第二类,PHP-FPM 或类似的常驻 worker 数量乘以单个进程峰值内存,超出了预算。一个 WordPress 站,单个 PHP-FPM worker 处理一次复杂查询时峰值能到 80 到 150 MB,pm.max_children = 30 就是 3 到 4.5 G 的潜在占用。很多 pm.max_children 是直接抄教程设的,根本没算过。

第三类,日志或缓存文件被当成内存文件读了。比如有人用 cat 看一个 2 G 的日志、用 grep 扫全站源码,或者备份脚本把整个目录 tar czf 之前忘了排除 cache/。page cache 虽然是可回收的,但在回收前会挤压真正需要的内存,间接把匿名页推向 swap。

第四类,内存泄漏。Node、Java、Python 的长驻服务最常见。进程的 RSS 每天涨一两百兆,一周后就把机器吃干了。这种进程用 ps aux --sort=-rss | head 一眼能看出来,但要确认是泄漏还是正常的缓存增长,得看 RSS 随时间的变化曲线。

定位用这条:

ps -eo pid,ppid,rss,vsz,comm --sort=-rss | head -15

RSS 是实际驻留物理内存,VSZ 是虚拟地址空间大小(不用太在意,Node 和 Java 的 VSZ 常常几十 G,那不是真占用)。真正要看的是 RSS 的前几名有没有「看起来不该那么大」的进程。

四、应急处理:先恢复 SSH,再谈别的

如果已经卡到没法操作,最有效的动作往往不是调参,而是给关键进程一点优先权。有两个即时手段:

手段一,临时把 swappiness 调到 0(不是永久)。

sysctl -w vm.swappiness=0

这个参数控制内核主动换出匿名页的倾向,取值范围 0 到 200(新版内核),默认 60。设成 0 的意思是「除非真的没内存了,否则别主动换出匿名页」。它不能让已经换出去的页回来,但能立刻制止「还在继续往外换」这个趋势,通常几十秒内 SSH 就恢复响应了。

注意这只是止血,不是治本。而且长期设 0 有个副作用:内存确实耗尽时,内核对匿名页的回收弹性变小,更容易触发 OOM killer 直接杀进程。所以我一般只在应急时用,事后按场景调回一个合理的值。

手段二,给 sshd 和关键服务加一点 OOM 保护,降低被杀概率。

echo -1000 > /proc/$(pgrep -o sshd)/oom_score_adj

oom_score_adj 范围是 -1000 到 1000,越小的分数越不容易被杀。给 sshd 设成 -1000 相当于「绝不先杀我」,保证你在最坏情况下还留一条命进去收拾。这个值重启后失效,需要的话写进 systemd 的 service 覆盖配置里。

手段三,如果不是生产高峰,可以先手动释放一部分 page cache。

sync
echo 1 > /proc/sys/vm/drop_caches

第一个 sync 不能省,作用是先把脏页刷盘,否则直接 drop 会丢数据(虽然内核会阻止纯脏页被丢弃,但保险起见还是先 sync)。这个命令只释放「干净」的缓存页,不会释放 swap,但在缓存占满了物理内存的场景下能立刻腾出空间。生产环境慎用——缓存清空后,下一段时间的读请求全部落到磁盘,短时间性能反而更差。

应急完之后,一定要做一件事:把当时的 vmstat、free -m、ps aux --sort=-rss 输出都存一份。理由很实际——等你处理完,系统恢复正常了,你就再也看不到故障现场了,而复盘靠的是那几行数字,不是记忆。

五、swappiness 到底该设多少:不要迷信「服务器就设 1」

网上流传最广的说法是「服务器上把 swappiness 设成 1 或 0」。这个说法的来源是桌面场景——桌面系统希望界面响应快,不愿意把应用内存换出去。但服务器场景不完全一样,要分两种情况:

如果是纯数据库服务器,我倾向于设 1 到 10。原因是数据库自己的 buffer pool 已经把热数据管得很好,内核再去做匿名页换出,只会让数据库的访问模式变差,而且数据库的页缓存收益不大。

如果是跑 Web 应用、PHP-FPM、Node 的混合型机器,我倾向于设 10 到 30。适度的换出能让一部分「开了但闲置」的 worker 让出内存,这恰恰是 swap 的价值所在。设成 0 会让闲置 worker 一直霸占物理内存,反而挤掉更需要的 page cache,让 IO 变多。

如果是容器宿主机,还要考虑 cgroup v2 的 memory.swap.max,这是 per-cgroup 的 swap 上限,比全局 swappiness 更精确。给小容器配一个较小的 memory.swap.max,可以避免单个容器把整机的 swap 吃掉,OOM 范围也收敛在那个容器里。

一个折中的做法是设一个中等的 swappiness,同时给关键服务设 oom_score_adj,双保险。swappiness 决定「换不换」,oom_score_adj 决定「万一真不够,杀谁」。这两个旋钮各管一段,比只调一个更稳。

六、swap 的大小怎么定

老教程说「swap 是内存的两倍」,那是 32 位时代留给休眠(hibernate)用的经验,现在基本不适用。我的经验值是:

  • 内存 2 G 以下:swap 给 2 G,用作缓冲,防止突发把机器直接压死。
  • 内存 4 到 8 G:swap 给 2 到 4 G。这个区间最常见,也最容易出问题。
  • 内存 16 G 以上:swap 给 4 到 8 G 就够,主要作用是「万一」,不是「常用」。
  • 需要休眠的机器:swap 必须大于等于内存,这个约束和性能无关,是功能要求。

更重要的是 swap 放在哪。同一个云盘上开 swap,swap 抖动会和数据库 IO、日志写入抢同一块盘的带宽,雪上加霜。如果机器有第二块盘(尤其是本地 NVMe 或临时盘),swap 放在第二块盘上,效果会好很多。用临时盘要注意:很多云厂商的临时盘重启会清空,swap 被清空不是问题(重启后自然重建),但别往上面放别的东西。

想加 swap 文件,步骤是这样的(以 2 G 为例):

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# 写入 fstab,注意用 sw 而不是 swap,避免和旧分区冲突
echo '/swapfile none swap sw 0 0' >> /etc/fstab
swapon --show

有两个细节容易踩坑。fallocate 在某些文件系统(尤其是 btrfs、部分 xfs 配置、以及 ZFS)上创建的文件可能有空洞,导致 swap 无法启用或者性能异常,报错通常是「swapon failed: Invalid argument」。遇到这种情况改用 dd if=/dev/zero of=/swapfile bs=1M count=2048,慢但可靠。另外,fstab 里的选项写 sw 是老写法,新系统写 swap 也可以,但务必保证 swapon -a 测试通过,否则下次重启会卡在 emerg 模式进不来。

七、一次真实排查的完整次序

把上面的东西串一遍,我实际处理时是这个次序:

第一步,远程登录(如果 SSH 卡住,用云厂商的 VNC/控制台 或 ssh -o ConnectTimeout=5 重试,别傻等),先跑 uptime 看负载。load average 里第三个数字(15 分钟)如果远低于第一个(1 分钟),说明负载是刚刚起来的,和某个事件对得上(比如备份脚本、定时任务、爬虫高峰)。

第二步,vmstat 1 10 看 si/so,判断是不是 swap 抖动。同时看 wa 列(IO 等待),如果 wa 超过 30 同时 si/so 在动,基本可以定案。

第三步,ps aux --sort=-rss | head -20 找内存大户。ps -eo pid,etime,rss,comm --sort=-etime | head 辅助看哪些进程是「刚启动的」——重启过的进程往往是嫌疑对象。

第四步,如果是 MySQL,登录进去跑 SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%',看 Innodb_buffer_pool_pages_free 和命中率;如果是 PHP-FPM,看 pm.max_children 和 pm.process_idle_timeout;如果是 Node/Python 长驻服务,看 RSS 是否已经接近内存上限。

第五步,做参数调整,一次只改一个,改完观察十分钟。同时改三个参数,出了问题不知道是哪个起作用(或者起反作用)。

第六步,写进记录。哪台机器、哪个时间、什么现象、改了哪个参数、之后指标怎么变的。这种记录攒到十条,你会发现自己机器的脾气。运维最贵的东西不是技术,是你对自己系统的那点熟悉。

八、几个常见误区

误区一:看到 swap used 就紧张。swap 里躺着冷页是正常状态,就像书架上有不用的活页夹。判断标准永远是 si/so 有没有在动,不是 used 有多大。

误区二:直接 swapoff -a。这会把 swap 里的所有页立刻读回物理内存。如果物理内存本来就紧张,这一步会直接把系统打爆,表现是瞬间卡死,然后 OOM killer 一口气杀掉好几个进程,甚至包括 MySQL。要清 swap,正确姿势是先确保 available 大于 swap used,再 swapoff。

误区三:认为加内存就能解决一切。加内存确实能解决「内存不够」的问题,但解决不了「某个进程在泄漏」的问题。一个每天泄 300 MB 的 Node 服务,你给它加到 32 G 也就是多撑几个星期。先用 ps 看 RSS 曲线,确认是泄漏还是容量不足,再决定花钱还是改代码。

误区四:把 swap 放在网络存储上。NFS 上放 swap 是可以的,但网络抖动或者 NFS 服务端重连时,整机会挂住,连本地命令都不响应。除非是很特殊的场景,不要在 NFS 上开 swap。

说到底,swap 是个很朴素的机制:拿 IO 换内存弹性。它不好也不坏,关键看你的系统是不是把「偶尔换一换」误当成了「日常运营」。持续抖动说明内存预算出了问题,那个问题往往藏在一个没算过的参数里,或者一个正在慢慢泄漏的进程里。找到它,比任何调参都管用。

Last modification:November 10th, 2026 at 12:24 pm

Leave a Comment