某天凌晨,线上服务突然变慢:接口 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 | 容器吃掉节点全部内存,触发节点 swap | docker stats / kubectl top pods |
| 应用内存泄漏 | 缓存、集合只增不减 | 观察 RSS 是否单调上涨 |
| 缓存无上限 | 本地 Cache 不淘汰 | 看进程 RSS 与命中率 |
| 连接池/线程池过大 | 每连接/线程占几 MB | 调小后观察 swap 回落 |
容器场景下,宿主机开了 swap 而容器没设 limit 是最隐蔽的坑:容器以为内存无限,拼命分配,最终把节点内存吃光、触发全节点 swap。用下面命令一眼看清:
docker stats --no-stream
kubectl top pods -n <namespace>
# 若节点 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 内存 > 已用 swap,否则会 OOM
# 仅清文件缓存:
echo 1 > /proc/sys/vm/drop_caches
⚠️ swapoff 有风险:它会把交换区数据全部搬回物理内存,若空闲内存不足,会直接触发 OOM Killer 杀进程。务必先确认 free 的空闲量大于 swapon --show 的已用大小再执行。
| 手段 | 适用场景 | 风险 |
|---|---|---|
| 调小 swappiness | 想减少换出、应急 | 几乎无风险,推荐首选 |
| swapoff -a | swap 已用不大、想彻底释放 | 空闲不足会 OOM,慎用 |
| 重启进程 | 定位到泄漏进程 | 会中断该服务,需灰度 |
六、长期根治:从配置到架构
止血只是权宜,根治要从内存分配与隔离入手:
1. 给 JVM / 运行时设合理上限
# 容器里务必让堆 < 容器 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 | 防止单进程吃光节点 |
| 禁用 swap | kubelet 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]) > 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+5min | vmstat 发现 so>0 | 锁定 swap 方向 |
| T+12min | smem 定位 Java 进程 VmSwap 1.2G | 找到元凶 |
| T+20min | sysctl 调小 swappiness + 重启 | 延迟回落一半 |
| T+2d | 加 -Xmx 上限 + 缓存淘汰 | swap 归零,根治 |
内存 swap 排查的核心心法就一句:先看 si/so 确认换页,再用 smem/status 锁定元凶进程,最后从内存上限与隔离入手根治。它和 Nginx 502/504 排查、慢 SQL 拖垮 CPU 一样,都是生产排错的基本功。配合 Docker 容器编排 与 GitHub Actions 的部署约束,把内存 limit 写进流水线,才能从源头避免这类“隐形变慢”。




