Linux swap 告警排查:从 si/so 到 VmSwap 的处置指南
半夜两点被 swap 告警叫醒,大概是运维值班里最让人心里发毛的一类情况。CPU 打满你能一眼看出是哪个线程在空转,磁盘写满你能顺手删日志,唯独 swap 这个指标,它高了不代表一定出事,它不高也不代表一定安全,属于典型的“看起来很简单、追下去全是坑”。我在几套线上环境里都追过这类告警,从最开始看到 Swap Used 超过 60% 就手忙脚乱重启机器,到后来能相对沉稳地先看 si/so 再看进程内存结构,中间踩的坑足够写一篇长文。
这篇内容围绕“线上机器 swap 过高导致告警”这个场景展开,把 swap 的底层机制、告警的真实含义、排查顺序、常见根因、处置手段和参数调优串成一条完整的线。写给谁看:手上管着 Linux 服务器、被监控告警追着跑的后端和运维同学;也写给刚接触线上环境、看到 free 命令输出一脸懵的新人。文中给的命令和参数都可以直接复制到测试机上验证,涉及取舍的地方我会把“为什么这么选”讲清楚,而不是只丢一堆命令让你自己猜。
1. 先弄明白 swap 告警到底在报什么
1.1 swap 的本质是“内存的溢出区”,不是“内存的扩展”
很多人对 swap 的第一印象是“内存不够时拿来凑数的”,这个理解不算错,但太粗。Linux 的虚拟内存管理把进程地址空间里的页分成几类:匿名页(堆、栈、malloc 出来的内存)、文件页(可执行文件、mmap 的文件、page cache)、共享内存页。当物理内存紧张时,内核的内存回收器会先尝试回收干净的文件页,因为这些页丢了还能从磁盘重新读回来,代价可控。真正回收不动的时候,才会把匿名页写到 swap 分区或 swap 文件里,把物理页腾出来。
这一步的关键在于:匿名页被换出之后,进程下次访问它,会触发一次缺页异常,内核得再从 swap 读回来。读回来这个动作是磁盘 IO。机械盘上单次随机读大概几毫秒到十几毫秒,SSD 上是几十到几百微秒,云盘还要叠加上网络存储的抖动。一个进程如果工作集被大量换出,它的每次内存访问都可能变成一次同步 IO,表现就是接口 RT 飙升、iowait 上去、CPU 的 sys 占比变高。所以 swap 高本身只是“症状”,真正要看的是“换入换出是否在持续发生”。
这里有个特别容易被忽略的点:swap 使用率高和 swap 正在被频繁读写,完全是两回事。一台机器 swap 用了 80%,但 si/so 长期是 0,说明那些页是很久以前换出去的,之后一直没被访问,对性能毫无影响;反过来,swap 只用了 15%,但 si/so 每秒几千个页,机器已经卡得不行了。监控如果只配了“Swap Used > 阈值”这一条规则,误报率会高得离谱。
1.2 告警规则通常踩了哪些坑
市面上常见的监控采集方式,对 swap 的定义并不统一。有的采集器取的是SwapTotal - SwapFree,有的取的是/proc/meminfo里的SwapCached,还有的把 swap 和 buff/cache 混在一起算“可用内存”。这几个值含义差别很大:SwapFree是还没被使用的 swap 空间;SwapCached指的是已经被换出、但同时又还留在物理内存里的页(内核做了优化,避免重复写盘),它既占 swap 又占内存,统计口径完全不同。
我见过最典型的一次误报,是告警规则写成了“swap 使用率 > 50%”,而采集脚本读的是SwapCached / SwapTotal。那台机器上跑着一个会周期性扫描大量小文件的批处理任务,page cache 压力大,内核频繁在换出和缓存之间倒腾,SwapCached一直很高,但实际SwapFree几乎没动,机器负载也很平稳。值班同学按告警去重启服务,重启完指标还是那样,白折腾一轮。
所以看到 swap 告警,第一件事不是动手,是先确认三件事:告警用的是哪个指标、阈值是多少、持续了多久。如果监控平台支持看历史曲线,把过去 24 小时的 swap 曲线和 si/so 曲线叠在一起看,能省掉一大半的无效操作。曲线平缓上升然后横住,多半是历史遗留;曲线在某个时间点陡增且伴随 si/so 尖峰,那才是真有事。
1.3 什么情况下 swap 高是真危险
我自己的判断习惯是做一个三层过滤,从粗到细:
| 层级 | 观察项 | 危险信号 | 通常含义 |
|---|---|---|---|
| 第一层 | Swap Used 绝对值 | 超过 SwapTotal 的 70% 且持续上升 | 内存确实紧张 |
| 第二层 | vmstat 的 si/so | 连续多个采样周期 si 或 so 大于 0 | 正在发生换入换出 |
| 第三层 | 应用侧指标 | RT 上升、超时率抬头、iowait 占比高 | 已经影响业务 |
三层都命中,基本可以判定是需要立即处置的故障。只命中第一层,属于观察项,可以先记录再排期处理。只命中第二层而 swap 使用率不高,说明内存回收压力集中在文件页上,方向要往 page cache 和 IO 那边找,不一定是内存容量问题。
还有一类场景要单独拎出来:容器环境。宿主机上的 swap 使用率,跟容器里看到的完全是两个世界。容器里的free命令默认读的是宿主机的/proc/meminfo,你以为容器 swap 用了 3G,实际上是整台宿主机所有容器的总和。cgroup v1 里 swap 的限制是memory.memsw.limit_in_bytes(内存加 swap 的总和),v2 里拆成了memory.swap.max。这两个值配错,轻则 swap 用量失控,重则触发 OOM Kill 而不是 swap,排查方向完全不同。
2. 排查之前,先把现场信息固定下来
2.1 第一时间该抓的几组数据
线上问题的黄金排查窗口往往只有几分钟,等你想起来要留证据的时候,负载可能已经恢复,进程可能已经被重启,什么都看不到了。我的习惯是登录机器后先跑一组“快照命令”,把输出重定向到文件,然后再慢慢分析。
mkdir -p /tmp/swap_diag_$(date +%s) && cd /tmp/swap_diag_* free -m > free.txt cat /proc/meminfo > meminfo.txt vmstat 1 10 > vmstat.txt ps aux --sort=-rss | head -30 > ps_rss.txt ps aux --sort=-vsz | head -30 > ps_vsz.txt cat /proc/vmstat | grep -E 'pswp|pgmajfault|pgsteal' > vmstat_detail.txt dmesg -T | tail -200 > dmesg.txt这几条命令覆盖了几个关键维度:free -m给的是内存和 swap 的总览;/proc/meminfo里有SwapCached、Dirty、Writeback这些细分项;vmstat 1 10采样十秒,能看出 si/so 是不是在持续;两个ps排序分别按物理内存和虚拟内存排,因为有些进程 VSZ 巨大但 RSS 很小(比如大量 mmap 但没实际访问),有些反过来;/proc/vmstat里的pswpin/pswpout是累计换入换出页数,配合pgmajfault能判断缺页的严重程度;dmesg里如果有 OOM Killer 的记录,那问题的性质就变了。
注意:
ps aux的输出里 RSS 单位是 KB,但有些发行版的列宽会截断,建议用ps -eo pid,ppid,user,rss,vsz,comm --sort=-rss | head -30更清晰。另外容器里跑ps看到的是容器内的进程,宿主机的进程要进宿主机命名空间才能看到,别搞混。
2.2 主机侧和监控侧数据交叉验证
监控平台的曲线和主机上现场采的数据,经常对不上,原因五花八门:采集周期太长漏掉了尖峰、采集脚本本身占了内存、指标聚合方式不同、时区或时间戳偏移。我的做法是把监控曲线的时间点和主机上的vmstat采样对齐看,比如监控显示 14:32 出现 swap 陡增,那就去翻那个时间点前后的dmesg和系统日志,看有没有进程启动、有没有定时任务触发。
还有一个细节:很多监控采集器本身是用 Python 或 Go 写的 agent,它们自己也会占几十上百兆内存。如果机器内存本来就紧,agent 采集时触发的内存分配可能恰好成为压垮骆驼的最后一根稻草,形成一个“采集导致告警、告警触发更多采集”的循环。这种情况在内存余量不到 5% 的机器上出现过不止一次,处置办法是把采集频率降下来,或者给 agent 加上内存上限。
2.3 告警分级决定你该多快动手
不是所有 swap 告警都值得半夜爬起来。我一般按影响面分三档:
- P0:swap 使用率持续上升且 si/so 明显大于 0,同时核心接口错误率或 RT 已经抬头。这种情况直接进应急流程,先止血再定位。
- P1:swap 使用率高但 si/so 平稳,业务指标正常。记录后在工作时间处理,优先排查内存泄漏和容量规划。
- P2:单次采样超过阈值、很快回落,历史曲线没有趋势性变化。归到误报或抖动,调整阈值或加持续时长条件。
分级的意义在于把有限的精力放在真正影响业务的事情上。我见过团队把 swap 告警一律设成电话呼叫,结果一个月被叫醒十几次,最后大家形成了“看到 swap 告警先忽略”的肌肉记忆,等真出事的时候反应反而慢了。告警的价值在于可信度,不在数量。
3. swap 过高的六类典型根因
3.1 内存真的不够:容量规划和实际用量脱节
这是最直接也最常见的一类。业务上线时按峰值 QPS 估了内存,但估算模型里漏了几块:JVM 的堆外内存(DirectByteBuffer、Netty 的池化缓冲)、glibc 的 malloc arena 碎片、线程栈(默认 8M 一个,几百个线程就是几个 G 的虚拟内存)、page cache 的合理占用。实际跑起来 RSS 比预期高一截,机器常年处在内存水位 90% 以上,内核只能靠 swap 来腾挪。
判断这类问题有个简单办法:看free -m里 available 的长期水位。如果 available 长期低于总内存的 10%,且没有明显的单个大进程,那就是整体容量不足,换机器或者拆服务是唯一出路,调参数只能延缓。反过来,如果 available 还有 30% 以上但 swap 很高,说明内存分配不均或者回收策略有问题,得往swappiness和内存碎片方向查。
3.2 swappiness 参数与回收策略的错配
vm.swappiness这个参数被误解得最厉害。它的取值范围是 0 到 100,控制的是内核在回收内存时,回收匿名页和回收文件页的相对倾向,不是“是否使用 swap”的开关。值越高,内核越倾向于把匿名页换出去;值越低,越倾向于丢弃 page cache。
默认值通常是 60,这个值对通用服务器还算合理,但对数据库、缓存类服务就不合适了。这类服务的 page cache 本身就是工作集的一部分,丢掉了要重新读盘,而它们的堆内存又往往是热数据,换出去再换回来纯属浪费 IO。我一般会把这类机器的swappiness调到 10 甚至 1,让内核优先保住匿名页。
但要注意一个反直觉的现象:swappiness设成 1 之后,swap 使用率反而可能慢慢涨上去。原因是内核在极端内存压力下,仍然会换出匿名页,只是频率低。一旦某个页被换出,如果它之后再也不被访问,就会一直躺在 swap 里,SwapUsed只增不减。这时候看到的高 swap 使用率其实是“历史沉积”,不代表当前有压力。所以调完参数后要看 si/so 的趋势,不要只盯着使用率绝对值。
3.3 内存泄漏和堆外内存的隐形增长
Java 服务的内存泄漏有个特点:堆内存被-Xmx卡住,泄漏体现在老年代 GC 后回收不干净,最终 OOM 或者频繁 Full GC。但堆外内存泄漏就没这么友好了,它不受-Xmx约束,增长曲线平滑,监控上看起来就是 RSS 缓慢爬升,swap 跟着涨,等你发现的时候已经过去好几天了。
排查方向:先用pmap -x <pid>看进程的内存段分布,找出异常大的匿名段;再用jcmd <pid> VM.native_memory summary(需要启动时开-XX:NativeMemoryTracking=summary)看 JVM 自己统计的各区域用量;如果 NMT 显示的总量远小于 RSS,那泄漏大概率在 JVM 之外的 native 库,比如某些编解码库、加密库的缓冲区没有正确释放。
非 Java 的服务也类似。Go 程序看GODEBUG=gctrace=1的输出和 pprof 的 heap profile;Python 看tracemalloc;C/C++ 服务直接上 valgrind 或者 ASan 在预发环境复现。关键是要区分“堆内泄漏”和“堆外泄漏”,两者的工具链完全不同。
3.4 透明大页和内存碎片
透明大页(THP)这个特性,开启和关闭都有人踩坑。开启的好处是减少 TLB miss,提升大内存应用的性能;坏处是内核做内存规整(compaction)时会产生不可控的延迟尖峰,表现为偶发的 RT 抖动和 swap 换出。有些数据库官方文档明确要求关闭 THP,就是因为这个。
检查方式很简单:
cat /sys/kernel/mm/transparent_hugepage/enabled cat /sys/kernel/mm/transparent_hugepage/defrag输出里方括号包住的是当前生效值。always表示对所有内存区域都尝试用大页,madvise表示只对显式声明MADV_HUGEPAGE的区域使用,never是关闭。defrag那一项如果是always,内存规整会更激进,延迟风险更高。
内存碎片是另一个隐性因素。机器长时间运行后,物理内存被切成很多不连续的小块,即使总空闲量够,也分配不出连续的大块内存,内核只能把一些页换出去凑连续空间。/proc/buddyinfo能看出各阶空闲块的分布,如果高阶(order 9、10)的计数长期是 0,说明碎片严重,重启或者做内存规整是常见手段。
3.5 容器和 cgroup 的内存限制配置
容器场景下,swap 的行为由 cgroup 控制,和裸机不一样。cgroup v1 里,memory.limit_in_bytes限制的是内存,memory.memsw.limit_in_bytes限制的是内存加 swap 的总和。如果只设了前者没设后者,容器里的进程可以把宿主机 swap 用得很凶;如果两个都设了但 memsw 设得比 limit 小,写入会直接失败。
cgroup v2 把这两个拆成了memory.max和memory.swap.max,语义更清晰。但很多老版本的容器运行时对 v2 的 swap 支持不完整,可能出现配置了也不生效的情况。排查时要先确认 cgroup 版本:
stat -fc %T /sys/fs/cgroup输出cgroup2fs是 v2,tmpfs是 v1。然后进到对应容器的 cgroup 目录看实际生效值,别只信编排文件里写的。
还有一种情况:容器内看到 swap 使用率很高,但容器根本没配 swap 限制,用的是宿主机的 swap。这时候问题不在容器本身,而是宿主机上其他进程导致的整体内存压力。定位时要跳出容器视角,上宿主机看全局。
3.6 IO 抖动引发的连锁反应
这一类最隐蔽。根因不在内存,而在磁盘。某个时刻存储层出现抖动(云盘限流、网络存储延迟、RAID 卡缓存策略问题),导致 page cache 回写变慢,Dirty页堆积,内核被迫启动内存回收,把匿名页换出到 swap。swap 换出本身又是一次写 IO,进一步加剧磁盘压力,形成正反馈。
识别特征是:swap 上涨的时间点和 iowait 飙升、Dirty值异常高、磁盘 await 变大高度重合。这时候去查内存泄漏或者调 swappiness 都是南辕北辙,正确方向是排查存储链路:iostat -x 1看各设备的 util 和 await,cat /proc/meminfo | grep -E 'Dirty|Writeback'看回写积压,云环境还要看云监控里的磁盘 IOPS 和吞吐是否触顶。
4. 一套可以直接照做的排查流程
4.1 第一步:分清 swap 是“历史沉积”还是“正在活动”
这是整个排查的分水岭,做错了后面全是无用功。核心看两个值:vmstat的 si/so,和/proc/vmstat里pswpin/pswpout的增量。
# 方法一:vmstat 采样,si/so 单位是 KB/s vmstat 1 10 # 方法二:看累计页数的增量,更精确 grep -E 'pswpin|pswpout' /proc/vmstat sleep 10 grep -E 'pswpin|pswpout' /proc/vmstat两次采样的差值除以 10,就是每秒换入换出的页数。乘以页大小(通常 4KB)就是字节数。如果这个值持续大于每秒几百页,说明内存压力是实时的;如果差值接近 0,那 swap 使用率再高也只是沉积。
我在一次排查里遇到过这样的机器:swap 用了 7.8G(总 8G),看着触目惊心,但 si/so 十秒采样全是 0,pgmajfault的增量也很小。继续看进程,发现是某个已经下线的服务残留的守护进程,它启动时申请了一大块内存,之后再也不访问,被内核整体换出去了。把这个进程干掉,swap 立刻降下来,业务毫无感知。如果当时直接按告警去重启整台机器,影响面就大了。
4.2 第二步:定位到底是谁的内存被换出去了
确认 swap 在活动之后,下一个问题就是“哪个进程的页被换出去了”。这里有个坑:ps和top显示的 RSS 是物理内存驻留集,不包含被换出的部分;要看被换出的量,得读/proc/<pid>/status里的VmSwap字段。
for pid in $(ls /proc | grep -E '^[0-9]+$'); do if [ -r /proc/$pid/status ]; then swap=$(awk '/^VmSwap:/{print $2}' /proc/$pid/status 2>/dev/null) if [ -n "$swap" ] && [ "$swap" -gt 0 ]; then name=$(cat /proc/$pid/comm 2>/dev/null) echo "$swap KB $pid $name" fi fi done | sort -rn | head -20这段脚本遍历所有进程,把VmSwap大于 0 的挑出来排序,单位是 KB。跑一遍就知道 swap 主要被谁占着。有些发行版自带smem工具更省事:
smem -t -k -p -s swap结果里Swap那一列就是每个进程被换出的量。注意smem需要单独装,且在某些内核上读不到准确值,交叉验证一下更稳。
有了进程列表,下一步是区分“这个进程本来就该占这么多内存”还是“它异常膨胀了”。对比历史数据最有效:如果接了 APM 或者有进程级的内存监控,拉一周曲线看一眼;没有的话,看进程启动时间和 swap 增长的时间点是否吻合,能排除掉一批。
4.3 第三步:拆开进程内存结构,看是哪一类页被换出
同一个进程的 swap 占用,可能来自完全不同的区域:堆、栈、mmap 的文件、共享内存。处置方式完全不同,所以要拆开看。
# 看进程内存映射的详细分布,Swap 列是每个映射区被换出的量 cat /proc/<pid>/smaps | awk ' /^[0-9a-f]/ {addr=$0} /^Swap:/ {if ($2 > 0) print $2, addr} ' | sort -rn | head -20smaps里每个虚拟内存区域都有一段统计,Swap字段表示这个区域被换出的大小。输出结果里如果大额 swap 集中在[heap],说明是堆内存被换出,配合语言层面的分析工具看;如果集中在某个.so的映射区,可能是那个库的静态数据或 mmap 的缓冲区;如果集中在[stack],线程栈被换出,通常意味着线程数太多或者栈太大。
还可以用pmap快速看整体:
pmap -x <pid> | tail -5最后一行汇总了 total 的 RSS 和 Dirty,但没有 swap 汇总,所以还是得靠smaps或者/proc/<pid>/status的VmSwap。
提示:读
smaps对大进程来说开销不小,一个几 G 内存的 JVM 进程,smaps可能有几十万行,遍历一次会短暂占用 CPU。线上操作尽量避开业务高峰,或者先cp一份再离线分析。
4.4 第四步:结合系统水位判断是“局部问题”还是“全局问题”
定位到具体进程之后,别急着处置,先看一眼全局水位。如果机器整体内存余量充足,只是某个进程的 swap 高,那可能是这个进程自己的内存管理策略问题(比如它主动 madvise 换出,或者它的工作集就是冷的)。如果整机 available 很低,多个进程都在被换出,那就是全局内存不足,单点优化意义不大。
几个关键水位值:
# 各内存 zone 的水位线 cat /proc/zoneinfo | grep -E 'Node|zone|min|low|high|free' # 内存规整和回收的统计 grep -E 'compact_|pgscan|pgsteal' /proc/vmstatzoneinfo里的min/low/high是内核给每个 zone 设的水位线,free低于low就会启动 kswapd 后台回收,低于min就会触发直接回收(direct reclaim),后者会阻塞分配内存的进程,是 RT 抖动的常见来源。如果pgscan_direct的计数增长很快,说明已经进入直接回收,情况比较紧急。
4.5 第五步:验证假设,小步处置
排查到这一步,通常会形成一个假设,比如“某进程堆内存泄漏”“某参数配置不当”“存储抖动导致回收”。处置原则是小步验证,别一上来就重启机器。
如果是单个进程异常膨胀,优先考虑重启该进程或者摘流量后滚动重启,观察 swap 是否回落。如果是参数问题,先在一台非核心机器上改,观察 24 小时。如果是容量问题,短期靠限流、扩容、拆服务缓解,长期做容量规划。
有一个动作我要特别提醒:swapoff -a不要随手执行。它的作用是把 swap 里的页全部读回物理内存,如果物理内存不够,内核会触发 OOM Killer 杀进程,甚至整机失联。真要执行,先确认free里的 available 大于SwapUsed加上安全余量(我一般留 20%),并且挑业务低峰期。
5. 处置手段和参数调优怎么做才不出事
5.1 应急处置:三种止血手段的适用边界
止血手段就那么几种,关键是选对场景。
第一种是重启异常进程。适用于单进程内存膨胀、且有副本可以顶住流量的情况。操作前确认进程的重启耗时和预热时间,避免重启后一批请求打进来引发雪崩。如果是无状态服务,滚动重启是最稳的。
第二种是摘流量降级。适用于暂时判断不了根因、但业务不能停的情况。把流量切到其他机器,问题机器保留现场慢慢分析。这招在故障定位阶段特别有用,比在现场一边排查一边扛流量从容得多。
第三种是扩容。适用于全局容量不足。云上扩容通常几分钟能完成,但要评估新机器的规格是否和旧机器一致,避免出现“扩容后新机器没问题、老机器继续告警”的尴尬。
三种手段都不涉及改内核参数。内核参数的调整我一般放到故障恢复之后做,因为改参数的效果需要时间验证,在故障现场改,出了问题分不清是新故障还是参数引起的。
5.2 参数调优:几个值得动的开关
长期治理阶段,值得关注的内核参数有这几个:
| 参数 | 默认值 | 建议值 | 适用场景 | 副作用 |
|---|---|---|---|---|
| vm.swappiness | 60 | 1~10 | 数据库、缓存、延迟敏感服务 | 极端内存压力下仍会换出,不能完全禁用 |
| vm.min_free_kbytes | 自动计算 | 物理内存的 1%~2% | 大内存机器,防止直接回收 | 设太大浪费内存,设太小易触发 direct reclaim |
| vm.overcommit_memory | 0 | 0 或 1 | 有大量 fork 或 mmap 的服务 | 设为 1 后可能掩盖内存超卖问题 |
| vm.zone_reclaim_mode | 0 | 0 | NUMA 机器 | 设为 1 会让内核优先本地回收,可能加剧 swap |
| THP enabled | always | madvise | 数据库、JVM 服务 | 关闭后大页应用性能可能下降 |
min_free_kbytes这个值很多人忽略。它决定了每个内存 zone 保留的空闲页下限,设得太小,内核容易进入直接回收,表现为进程分配内存时偶发卡顿;设得太大,等于白白浪费一块内存。经验值是物理内存的 1% 到 2%,32G 的机器设 512M 左右比较合适。调整前先看当前值:
cat /proc/sys/vm/min_free_kbytes改的时候别直接写死,按内存比例算,写进/etc/sysctl.d/下的配置文件,重启后也能生效。改完观察一周的pgscan_direct计数,如果增长明显放缓,说明方向对了。
5.3 长期治理:从“救火”转向“不再起火”
参数调优只能治标,真正让 swap 告警不再频繁出现的,是容量和架构层面的调整。
容量层面,把内存水位纳入常态监控,设定两条线:warning 线(available 低于 20%)和 critical 线(低于 10%)。在 warning 线触发时就做扩缩容评估,别等到 critical 再动手。同时把 swap 使用率的历史趋势也纳入观察,如果一条曲线在数月内持续爬升,即使没到阈值,也说明有慢性泄漏或者业务在增长,值得提前介入。
架构层面,把内存密集型和 IO 密集型服务分开部署。内存密集型服务(缓存、搜索、大数据计算)对 swap 极其敏感,应该部署在swappiness调低、甚至不开 swap 的机器上;IO 密集型服务对 swap 的容忍度稍高,但也别和内存大户混部。混部带来的资源争抢,是很多 swap 告警的隐性根因。
还有一个实践是给关键服务加内存使用的软限制。比如通过 systemd 的MemoryHigh和MemoryMax(cgroup v2 下),或者容器编排里的资源限制,让单个服务最多用到某个水位,超了先触发自身的内存回收而不是拖垮整机。这招的效果是把故障域从“整机”缩小到“单个服务”,代价是要花时间调限制值,调太紧会误杀,调太松没效果。
6. 常见问题速查和几个反直觉的坑
6.1 高频问题速查表
| 现象 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| swap 使用率高但 si/so 为 0 | 历史沉积,进程不再访问 | vmstat 1 10看 si/so | 记录观察,不必处置 |
| swap 持续上涨伴随 RT 抖动 | 内存泄漏或容量不足 | 看 VmSwap 排序和 available 水位 | 定位进程,重启或扩容 |
| 重启进程后 swap 不降 | 页仍在 swap 中未被访问 | 对比重启前后SwapFree | 需要 swapoff/on 才能回收 |
| 容器内 swap 指标异常 | 读到的是宿主机数据 | 确认 cgroup 版本和挂载 | 进宿主机看全局 |
| swap 上涨同时 iowait 飙升 | 存储抖动导致回收 | iostat -x 1看 await | 排查存储链路 |
| 空闲内存很多但 swap 也高 | swappiness 策略导致 | 看pgscan和pgsteal | 调低 swappiness,观察趋势 |
| 偶发 OOM 而非 swap 增长 | cgroup memsw 限制 | 看dmesg的 OOM 记录 | 调整限制值或拆服务 |
6.2 几个反直觉但真实存在的坑
第一个坑:swap 使用率高不等于要扩容。我在一台 64G 内存的机器上见过 swap 用了 20G,第一反应是内存不够,结果查下来是某个批处理任务晚上跑的时候把大量冷数据换出去了,白天业务跑起来 si/so 是 0,性能毫无影响。这种情况下扩容纯属浪费。判断标准永远是 si/so 和业务指标,不是使用率绝对值。
第二个坑:swappiness=0不等于禁用 swap。很多人以为设成 0 就完全不换了,实际上内核在内存极度紧张时仍然会换出,只是优先级最低。真要禁用,得swapoff。但swapoff有 OOM 风险,所以更常见的做法是干脆不分配 swap 分区,从根上避免。不过完全不配 swap 也有代价:内存紧张时没有缓冲,直接 OOM Kill,进程被杀得更突然。两种方案各有取舍,取决于业务对“缓慢降级”和“快速失败”的偏好。
第三个坑:重启进程不会自动把 swap 页读回来。进程重启后会重新申请内存,原来的 swap 页变成孤儿,要等内核的 swap 回收机制慢慢清理,或者手动swapoff -a && swapon -a强制换回。所以有时候重启完发现 swap 使用率没降,不是没生效,是那些页还没被回收。验证方法是看SwapFree在swapoff/on前后的变化。
第四个坑:监控采集的 swap 指标可能包含缓存。前面提过SwapCached的干扰,这里补充一个更隐蔽的:某些采集脚本用free命令的输出来算,而free在不同版本里的列名和含义有差异(比如buff/cache的合并方式),跨发行版对比数据时会失真。跨机器做横向对比前,先确认采集口径一致。
第五个坑:NUMA 架构下的 swap 分布不均。多路机器上,内存分属不同 NUMA 节点,进程如果跨节点访问内存,延迟会高很多,内核在回收时也可能出现某个节点 swap 特别高的情况。numastat能看到各节点的分配和命中情况,如果发现明显的跨节点访问,用numactl绑定或者调整进程的 CPU 亲和性,比调 swap 参数更对症。
6.3 我个人踩过最疼的一次
早年间处理过一次 swap 告警,机器 swap 用了 90%,业务 RT 翻了三倍。当时判断是内存泄漏,直接重启了应用,RT 恢复,以为完事了。结果第二天同样时间又告警,重启又好了。连续三天之后才反应过来,问题不在应用,在定时任务:每天凌晨有个数据导出任务,会一次性加载几百万条记录到内存做聚合,跑完之后内存不释放(用的是连接池里的常驻对象),swap 慢慢涨上去,白天业务高峰时这些被换出的页又被频繁访问,形成抖动。
真正的解法是把导出任务改成流式处理,分批读、分批写,内存占用从几个 G 降到几百兆。改完之后 swap 再没告警过。这件事给我的教训是:swap 告警的根因往往不在“内存管理”本身,而在“谁在不合理地使用内存”。盯着内核参数调半天,不如去看一眼业务代码里那些一次性加载全量数据的写法。
后来我形成了一个习惯:每次处理完 swap 告警,都会问一句“这个进程的内存使用曲线,三个月前是什么样”。如果是一条缓慢上升的线,那大概率是代码层面的问题,参数调优只能拖延时间;如果是一条阶梯上升的线,每次上升都对应一次发布,那就是那次发布的改动引入的,回滚或者修复改动是最快的路。曲线比任何工具都诚实。
另外分享一个查 swap 的小技巧:如果机器上装了atop,用atop -m能看到内存的详细分解,包括每个进程的 swap 占用和历史变化,比手写脚本方便。atop的日志功能还能回看历史时刻的内存状态,对于已经恢复的故障特别有用,前提是提前开了日志采集。
最后说一句关于告警配置的体会:swap 告警的阈值不要设成单一静态值,最好用“使用率 + 持续时间 + si/so 联动”的组合条件。比如“swap 使用率 > 60% 且持续 10 分钟 且 si+so > 100 页/秒”才触发,这样误报会少很多,告警来了也更有底气直接进应急流程。告警的质量决定了值班同学对这个系统的信任度,宁可少报几次,也别天天狼来了。