内存 swap 导致性能骤降排查实战

某天凌晨,线上服务突然变慢:接口 P99 从 50ms 飙到 2s,服务器负载(load average)一路上涨,可你登上机器一看,CPU 使用率(us+sy)明明没打满,磁盘 IO 也不高。这种“明明没满却卡得要死”的现象,十有八九是 内存 swap(交换分区)在作怪。当物理内存不足,Linux 会把一部分匿名内存页换出到磁盘,等进程再访问时就要从磁盘读回来——而磁盘比内存慢几个数量级,于是整体性能断崖式下降。本文带你用一套可复用的命令,从“确认在用 swap”到“定位元凶进程”再到“止血与根治”,彻底搞懂内存 swap 性能骤降排查实战。

一、先确认:系统真的在用 swap 吗

别急着甩锅给 swap,第一步要先用数据确认它确实在被使用。下面两条命令组合起来,能快速判断。

free -h
swapon --show
cat /proc/swaps

free -h 的 Swap 行若 used 不为 0,说明已有数据驻留在交换区;swapon --show 能看到交换设备与已用大小。但更关键的是实时换入换出速率,用 vmstat 1 观察最直观:

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 812344  90212  41028 521900  120   88     0    24  310  420  9  4 85  0  2

重点看末两列 si(swap in,换入)和 so(swap out),单位是 KB/s。只要 si/so 持续大于 0,就说明系统正在频繁换页,这正是性能骤降的直接信号。

指标含义健康阈值
si每秒从交换区读回内存的页数(KB/s)持续 > 0 即异常
so每秒换出到交换区的页数(KB/s)持续 > 0 即异常
swpd已使用的交换空间总量越小越好,理想为 0
free完全未被使用的物理内存过低(<5%)需警惕

二、swap 为什么会拖垮性能

swap 本身不是坏东西,它是内存压力的“安全气囊”。问题出在启用 swap 之后的访问延迟:当进程访问被换出的匿名页,会触发缺页中断,内核必须从磁盘把页读回内存,这个延迟是毫秒级甚至更高,而内存访问是纳秒级。一旦 si/so 持续,原本该纳秒完成的访问变成毫秒级,应用线程大量陷入等待,负载飙升、吞吐骤降。

此外,内核的 kswapd 守护进程会在内存紧张时持续扫描并回收页面,占用 CPU(表现为 sy 升高),进一步挤压业务线程。所以swap 往往是“内存不够”的替罪羊,真正的根因通常是某个进程把内存吃光了。

三、定位是谁在吃内存

找到“内存杀手”是排查的核心。先用 top 按常驻内存(RES)排序,或直接用 ps:

top -b -n 1 | head -20
ps aux --sort=-%mem | head -10

更专业的工具是 smem,它能区分“独占物理内存”与“共享”,避免被共享库误导:

smem -t -k -s swap -r | head -15

要确认某个进程到底占了多少 swap,可以直接读它的状态文件:

pid=1234
grep -E 'VmRSS|VmSwap|Name' /proc/$pid/status
# VmRSS:  进程实际占用的物理内存
# VmSwap: 该进程被换出到交换区的内存大小
字段含义排查用途
VmRSS常驻物理内存(不含 swap)看进程“真吃”了多少内存
VmSwap被换出的匿名内存谁 VmSwap 高,谁就是 swap 元凶
VmSize虚拟地址空间总大小虚高通常无意义,看 RSS/Swap

四、为什么内存会不够:常见根因

定位到进程后,还要追问“为什么它会吃这么多内存”。常见根因与对应的快速确认命令如下:

根因典型场景快速确认
JVM 堆设置过大-Xmx 设得比容器/物理内存还大jstat -gc 看 OU/OC
容器无内存 limit容器吃掉节点全部内存,触发节点 swapdocker stats / kubectl top pods
应用内存泄漏缓存、集合只增不减观察 RSS 是否单调上涨
缓存无上限本地 Cache 不淘汰看进程 RSS 与命中率
连接池/线程池过大每连接/线程占几 MB调小后观察 swap 回落

容器场景下,宿主机开了 swap 而容器没设 limit 是最隐蔽的坑:容器以为内存无限,拼命分配,最终把节点内存吃光、触发全节点 swap。用下面命令一眼看清:

docker stats --no-stream
kubectl top pods -n &lt;namespace&gt;
# 若节点 kubelet 未禁用 swap,容器超用会拖垮整台机器

五、临时止血:先让服务喘口气

生产环境第一原则是“先恢复,再排查”。在不重启业务的前提下,有两招能快速缓解:

1. 降低 swappiness,减少换出倾向

sysctl -w vm.swappiness=10   # 默认 60,调小让内核更不愿换出
# 持久化:写入 /etc/sysctl.d/99-swap.conf -> vm.swappiness=10

2. 释放已用 swap(需足够空闲内存)

swapoff -a && swapon -a     # 把 swap 内容倒回内存,再重新挂载
# 前提:free 内存 &gt; 已用 swap,否则会 OOM
# 仅清文件缓存:
echo 1 &gt; /proc/sys/vm/drop_caches

⚠️ swapoff 有风险:它会把交换区数据全部搬回物理内存,若空闲内存不足,会直接触发 OOM Killer 杀进程。务必先确认 free 的空闲量大于 swapon --show 的已用大小再执行。

手段适用场景风险
调小 swappiness想减少换出、应急几乎无风险,推荐首选
swapoff -aswap 已用不大、想彻底释放空闲不足会 OOM,慎用
重启进程定位到泄漏进程会中断该服务,需灰度

六、长期根治:从配置到架构

止血只是权宜,根治要从内存分配与隔离入手:

1. 给 JVM / 运行时设合理上限

# 容器里务必让堆 &lt; 容器 limit
JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseG1GC"

2. 用 cgroup 限制容器内存,并禁用 swap

# docker-compose
version: '3'
services:
  app:
    image: myapp:1.0
    deploy:
      resources:
        limits:
          memory: 1g
# k8s
resources:
  limits:
    memory: 1Gi
  requests:
    memory: 512Mi

Kubernetes 集群建议在 kubelet 配置 --fail-swap-on=true,让节点直接禁用 swap,从根本杜绝容器换页。

根治项做法收益
设内存上限JVM -Xmx / 容器 limits防止单进程吃光节点
禁用 swapkubelet fail-swap-on / swapoff消除换页延迟根因
容量评估压测得到真实内存峰值预留 20%~30% 余量
代码治理缓存淘汰、连接池上限遏制内存泄漏与膨胀

七、监控与告警:别等用户投诉

swap 问题最怕“悄悄发生”。用 node_exporter 采集交换区指标,配合 Prometheus 在 si/so 刚冒头时就告警:

# Prometheus 告警规则
groups:
- name: swap.rules
  rules:
  - alert: SwapActivity
    expr: rate(node_vmstat_pgpgout[5m]) &gt; 0
    for: 5m
    labels: { severity: warning }
    annotations:
      summary: "节点 {{ $labels.instance }} 正在频繁换页"

除了 swap 速率,还应盯住 node_memory_MemAvailable_bytes 与 node_memory_SwapTotal_bytes 的比值,早于 swap 发生前就发现内存压力。

八、一次真实复盘

某次线上故障:一个报表服务上线后,P99 从 50ms 涨到 2s,负载从 2 涨到 40,但 CPU 才 13%。运维按前文流程排查:vmstat 看到 so 稳定在 200+ KB/s,smem -s swap 锁定一个 Java 进程 VmSwap 高达 1.2G;jstat -gc 显示老年代接近上限,确认是本地缓存无上限导致的内存泄漏。临时把 vm.swappiness 调小、重启该进程止血,随后给堆加 -Xmx 上限并引入缓存淘汰,swap 归零,P99 回到 60ms。

时间动作效果
T0收到延迟告警,登机排查确认 P99 2s、load 40
T+5minvmstat 发现 so>0锁定 swap 方向
T+12minsmem 定位 Java 进程 VmSwap 1.2G找到元凶
T+20minsysctl 调小 swappiness + 重启延迟回落一半
T+2d加 -Xmx 上限 + 缓存淘汰swap 归零,根治

内存 swap 排查的核心心法就一句:先看 si/so 确认换页,再用 smem/status 锁定元凶进程,最后从内存上限与隔离入手根治。它和 Nginx 502/504 排查、慢 SQL 拖垮 CPU 一样,都是生产排错的基本功。配合 Docker 容器编排 与 GitHub Actions 的部署约束,把内存 limit 写进流水线,才能从源头避免这类“隐形变慢”。

上一篇 Linux perf 性能分析实战:用火焰图定位 CPU 瓶颈
下一篇 WebSocket 实时通信实战:用 Socket.IO 落地
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]