内存明明宽裕服务却偶发卡顿:一次 Linux swap 与 swappiness 排查复盘

一个 Java 服务跑在 32G 内存的机器上,偶发卡顿几百毫秒到一秒多,可 CPU 闲着、free 还显示 8G 空闲内存,而 swap 却用掉了 2.3G。排查梳理:swap 不是内存的备用油箱不是内存满了才用,内核内存够时也会主动把很久没访问的冷页换出去,这是为多留文件缓存做的主动内存管理;判断 swap 要不要紧看 vmstat 的 si/so 换入换出流量而非 free 里 used 的存量;真正造成卡顿的是被换出的页又被访问触发 major page fault 要等磁盘 IO,慢了百万倍,这种停顿在 GC 日志里查不到;控制换出积极程度的是 vm.swappiness,默认 60 对延迟敏感服务偏高;正确解法是把 swappiness 调到 10、用 swapoff -a 再 swapon -a 把旧页清回内存、必要时给单个服务设 cgroup MemorySwapMax=0,彻底关 swap 要权衡 OOM 风险,以及一套 swap 排查纪律。

2024 年,我负责的一个 Java 服务,跑在一台 32G 内存的服务器上。这服务平时一切正常,可有一阵子,它开始出现一种很难复现的卡顿:大多数请求几十毫秒就回了,但每隔一段时间,某个请求会突然慢到几百毫秒甚至一秒多。我先查 GC,GC 日志里确实偶尔有长一点的停顿,可对不上这个频率。我又查 CPU,CPU 一直闲着;查内存,free 一看,32G 里还有 8G 多是空闲的——内存明明够用。我当时就想:内存这么宽裕,肯定不是内存的事。可我盯着 free 的输出又看了一会儿,目光落在了一个我平时从来不看的字段上:Swap。那一行写着,swap 已经用掉了 2.3G。我心里咯噔一下,可随即又说服了自己:内存还剩 8G 呢,系统怎么会去用 swap?用了 swap,不就是内存不够的意思吗?这两件事——"内存还剩 8G"和"swap 用了 2.3G"——在我当时的认知里,是直接矛盾的。我盯着这个矛盾想了很久,最后才反应过来:我对 swap 的理解,从根上就错了。我一直以为 swap 是"内存的备用油箱",油箱空了才会去用它。可事实根本不是这样。这件事逼着我把 Linux 的 swap、swappiness、内存回收、缺页中断这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,32G 内存,一个 Java 服务
事故现象:
- 服务大多数请求几十毫秒,偶尔某个请求卡几百毫秒~1 秒多
- CPU 一直闲着;free 显示还有 8G 多空闲内存
- ★ 内存看着很宽裕,但 swap 已经用掉了 2.3G

现场排查:
# 1. 看内存和 swap
$ free -h
              total   used   free   shared  buff/cache  available
Mem:           31Gi   18Gi  2.1Gi    0.5Gi        11Gi      12Gi
Swap:         8.0Gi  2.3Gi  5.7Gi                              # ★ swap 用了 2.3G

# 2. ★ 看 swappiness 设的多少
$ cat /proc/sys/vm/swappiness
60                                       # ★ 默认值 60,偏积极

# 3. ★ 看是不是真有"换入换出"在持续发生
$ vmstat 2 5
 ... si   so ...
      0    0
    120    0                              # ★ si 不为 0 = 正在从 swap 换【入】
     88    0                              #   换入就意味着:有进程在等磁盘

# 4. ★ 看是谁的内存被换进了 swap
$ for f in /proc/*/status; do
    awk '/VmSwap|Name/{printf $2" "}END{print ""}' $f
  done | sort -k2 -rn | head
java 1980000 kB                           # ★ Java 进程被换出了近 2G

根因(后来想清楚的):
1. ★ swap 不是"内存的备用油箱"。它不是"内存满了
   才用"。内核【内存还有空闲时,就会主动】把很久
   没被访问的【冷内存页】换到 swap 上去。
2. 决定它"多积极地换"的,是 vm.swappiness。默认 60,
   是一个【相当积极】的值 —— 它倾向于"宁可换出一些
   冷页,也要多留点内存给文件缓存"。
3. 我的 Java 服务,堆里有些区域确实长时间没访问
   (冷页),于是被内核【挑出来换到 swap】了 ——
   尽管当时内存根本没紧张。
4. ★ 麻烦在"换回来"那一下:被换出的页,下次一旦
   被访问,必须先从【磁盘】把它读回内存(major
   page fault)。磁盘比内存慢几个数量级。
5. 对延迟敏感的服务,一次"换回"就是几十到几百毫秒
   的停顿 —— 这就是那些偶发的、对不上 GC 的卡顿。
swap 被用,不代表内存不够;但被换出的关键页,会带来延迟。

修复 1:内存还有空闲,为什么用了 swap

# === ★ 先纠正最核心的误解:swap 不是"备用油箱" ===

# === 我以为的 swap vs 真实的 swap ===
# 我以为:swap 是内存的"备用油箱",物理内存【用光了】,
#   系统走投无路,才会去借用 swap。所以"用了 swap"
#   就等于"内存不够"。
# ★ 真相:swap 是内核【主动的内存管理手段】,不是
#   走投无路的应急。内核在内存【还很宽裕】时,就会
#   主动把一些页换到 swap 上去。

# === ★ 内核为什么"内存够也要换出" ===
# 内核的逻辑不是"省内存",而是"让内存用得更值"。
# 内存里的页,大致分两类:
#  - 匿名页:进程的堆、栈 —— 没有对应的磁盘文件,
#            要腾走它,只能写到 swap。
#  - 文件页:从文件读进来的缓存(buff/cache)——
#            它本来就有磁盘上的源文件。
# ★ 内核会盘算:有些进程的匿名页【很久没碰过】(冷),
#   与其让这些冷页白占着宝贵内存,不如把它们换到
#   swap,腾出来的内存拿去做【文件缓存】,让文件读写
#   更快。这是一笔"内核认为划算"的买卖。

# === ★ 所以那个"矛盾"根本不矛盾 ===
# "内存还剩 8G" 和 "swap 用了 2.3G" 一点都不冲突:
#   内核不是因为不够才换,是因为它【判断换了更划算】。
# ★ "用了 swap" ≠ "内存不够"。这是两件事。

# === 怎么判断"用了 swap"到底要不要紧 ===
# 关键不是看 swap used 这个【存量】数字,
#   而是看有没有持续的【换入换出流量】:
$ vmstat 2
 ... si   so ...
      0    0      # ★ si/so 都是 0 = 换过一次后就静止了,问题不大
    120   60      # ★ si/so 持续不为 0 = 正在频繁换,这才要命
# si = swap in(换入)  so = swap out(换出)
# ★ swap 有占用、但 si/so 长期是 0 —— 通常无害。
#   si/so 持续跳动 —— 才是真有进程在被磁盘拖慢。

修复 2:swappiness——内核"换出冷页"的积极程度

# === ★ 控制"换得多积极"的旋钮:vm.swappiness ===

# === swappiness 是什么 ===
$ cat /proc/sys/vm/swappiness
60
# swappiness 是一个 0~100 的值。它调的是内核回收内存
#   时的【倾向】:
#  - 回收内存有两条路:换出匿名页(动 swap),
#    或者丢掉文件缓存页(buff/cache)。
#  - ★ swappiness 【越高】,内核越倾向于【换出匿名页】;
#    越低,越倾向于【先丢文件缓存】、尽量不动 swap。

# === ★ 几个关键取值 ===
# - swappiness = 0   :最大限度【避免】换出匿名页。
#                      (注意:不是"绝对不 swap",
#                       内存真不够时还是会换)
# - swappiness = 1   :极力避免 swap,但留一条活路。
# - swappiness = 10  :★ 服务器常用值 —— 很不情愿换,
#                      只在比较有必要时才动 swap。
# - swappiness = 60  :★ 默认值。相当积极,桌面合理,
#                      但对延迟敏感的服务【偏高了】。
# - swappiness = 100 :最积极地换出匿名页。

# === ★ 为什么默认 60 对我的服务不合适 ===
# 60 意味着内核相当乐意"为了多留文件缓存而换出冷页"。
# 对一个【文件 IO 不多、但对延迟极敏感】的 Java 服务,
#   这笔买卖是亏的:省下来的文件缓存我用不太上,
#   可被换出的堆页一旦被访问,就是实打实的卡顿。
# ★ 这种服务,该把 swappiness 调低(10,甚至 1)。

# === 临时改(立即生效,重启失效)===
$ sysctl vm.swappiness=10
$ cat /proc/sys/vm/swappiness
10

# === ★ 永久改(写进配置)===
$ vi /etc/sysctl.conf
vm.swappiness = 10
$ sysctl -p                       # 让配置立即生效
# ★ 改完是"以后内核换得不那么积极了",
#   已经在 swap 里的页,不会自己跑回来(见修复 5)。

# === 一句话 ===
# ★ swappiness 不决定"用不用 swap",它决定内核回收
#   内存时"多愿意动 swap"。延迟敏感服务,调低它。

修复 3:被换出的页慢在哪——major page fault

# === ★ 真正造成卡顿的,是"换回来"那一下 ===

# === 换出 vs 换入,慢的是换入 ===
# - 换出(swap out):内核挑个冷页写到 swap。这事在
#   后台干,进程基本无感。
# - ★ 换入(swap in):进程要访问一个【已经被换到
#   swap 上】的页 —— 这个页此刻【不在内存里】,
#   必须先从磁盘 swap 区把它读回内存,进程才能继续。
#   这一读,进程就【卡在那等磁盘】。

# === ★ 这个"等磁盘"有个名字:major page fault ===
# 进程访问一个不在内存的页,触发"缺页中断"。
#  - minor fault:页其实在内存,只是没建好映射 —— 极快。
#  - ★ major fault:页在磁盘上(比如 swap),必须发起
#    一次【磁盘 IO】把它读回来 —— 这就慢了。
# 内存访问:纳秒级。磁盘 IO:机械盘毫秒级,
#   ★ 慢了【百万倍】量级。一次 major fault,
#   就够让一个请求多花几十上百毫秒。

# === 看进程的 major fault 多不多 ===
$ ps -o pid,maj_flt,cmd -p 9400
  PID MAJFLT CMD
 9400  82000 java ...               # ★ major fault 累计很高
# 隔一段时间再看一次,看 MAJFLT 涨得快不快 ——
#   ★ 持续猛涨 = 进程一直在等 swap 换入。

# === ★ 为什么"对不上 GC" ===
# 我一开始查 GC,因为卡顿像 STW。但 swap 换入造成的
#   停顿,和 GC 是【两个独立的源头】:
#  - GC 停顿:在 GC 日志里有记录。
#  - ★ swap 换入停顿:GC 日志里【啥也没有】 ——
#    它是进程在"执行普通代码"时,碰到一个被换出的页,
#    被磁盘 IO 卡住的。日志当然不会记。
# 这就是为什么卡顿频率和 GC 日志对不上 —— 找错地方了。

# === 实时看换入换出流量 ===
$ vmstat 1
 ... si   so ...
    140    0          # ★ si 持续不为 0 = 一直在从 swap 换入
# si(swap in)只要持续跳动,就有进程在被磁盘拖慢。

# === ★ 认知 ===
# swap 本身不慢,慢的是"被换出去的页又被用到"。
#   对一个其实一直在被访问的"热"数据,把它换出去,
#   就是给它的每次访问都埋了一颗"磁盘 IO"的雷。

修复 4:怎么看是谁被换进了 swap

# === ★ 定位:到底是哪个进程的内存进了 swap ===

# === 第一步:确认 swap 总量和趋势 ===
$ free -h
Swap:         8.0Gi  2.3Gi  5.7Gi
$ vmstat 2 5            # 看 si/so 是不是持续不为 0

# === ★ 第二步:逐进程看 VmSwap —— 谁被换得最多 ===
# /proc//status 里的 VmSwap,就是这个进程
#   有多少内存被换进了 swap。
$ for f in /proc/[0-9]*/status; do
    pid=$(awk '/^Pid:/{print $2}' $f)
    name=$(awk '/^Name:/{print $2}' $f)
    sw=$(awk '/^VmSwap:/{print $2}' $f)
    [ -n "$sw" ] && [ "$sw" -gt 0 ] && echo "$sw kB  pid=$pid  $name"
  done | sort -rn | head
1980000 kB  pid=9400  java          # ★ 罪魁:Java 被换出近 2G
  45000 kB  pid=1203  mysqld
# ★ 这一段:把所有进程按"被换进 swap 的量"排序,
#   排第一的,就是受 swap 影响最大的进程。

# === 第三步:用 smem 看得更直观(若装了)===
$ smem -s swap -r | head
# smem 能直接按 swap 占用排序列出进程,比上面的脚本省事。

# === ★ 第四步:看这个进程是不是"延迟敏感" ===
# 被换出最多的是 java —— 一个在线服务。
#   在线服务被换出 = 它的某些堆页被访问时会卡。
# ★ 如果被换出最多的是一个【离线批处理/很少跑的
#   后台进程】,那其实无所谓 —— 它的冷页被换出去,
#   省下内存给别人用,反而是好事。
# 关键看:【被换出的,是不是一个延迟敏感的进程】。

# === 第五步:核对它的 major fault ===
$ ps -o pid,maj_flt,cmd -p 9400
# ★ VmSwap 大 + maj_flt 持续涨 = 这进程正实实在在
#   地被 swap 拖慢,必须处理。
# 只是 VmSwap 大、但 maj_flt 不涨 = 换出的是真冷页,
#   暂时没被访问,影响没那么急。

修复 5:正确解法——调 swappiness、清 swap、按需关 swap

# === ★ 解法:按"服务是否延迟敏感"分情况处理 ===

# === ★ 第一步(根治):把 swappiness 调低 ===
# 对延迟敏感的服务器,默认 60 太积极。调到 10:
$ vi /etc/sysctl.conf
vm.swappiness = 10
$ sysctl -p
# ★ 这一步是治本:让内核【以后】不再那么爱换出冷页。
#   但它【不会】把已经在 swap 里的页弄回来。

# === ★ 第二步:把已经换出去的页"清"回内存 ===
# 改完 swappiness,之前换出的 2.3G 还躺在 swap 里。
#   要让它们回到内存,最干脆的办法是 swap 关了再开:
$ swapoff -a          # ★ 关闭所有 swap:会把 swap 里的
                      #   页【全部强制换回内存】
$ swapon -a           # 再重新启用 swap
# ★ 重要前提:执行 swapoff 前,必须确认【物理内存
#   装得下】swap 里那些页(free 看 available 够不够)。
#   内存不够时 swapoff 会很慢甚至触发 OOM —— 别硬来。

# === 第三步:确认清干净了 ===
$ free -h
Swap:         8.0Gi    0Gi  8.0Gi      # ★ swap used 归零
$ vmstat 2                              # si/so 应该回到 0

# === ★ 要不要"彻底关掉 swap" ===
# 对【极度延迟敏感】的服务(Redis 官方就建议关,
#   或某些数据库场景),可以彻底禁用 swap:
$ swapoff -a
$ vi /etc/fstab                         # ★ 把 swap 那行注释掉
# /dev/mapper/swap  swap  swap  defaults  0 0
# ★ 注意权衡:彻底关 swap 后,内存一旦真的不够,
#   就没有 swap 兜底,会【直接触发 OOM Killer】杀进程。
#   关不关,取决于你更怕"偶发卡顿"还是"OOM"。
# 折中做法:不关 swap,但 swappiness 设 1,
#   让 swap 只在内存真的告急时才作为"最后兜底"。

# === ★ 第四步:针对单个服务用 cgroup 精细控制 ===
# 不想全局动 swappiness,可以只给某个服务设:
# (systemd service 里)
#   MemorySwapMax=0          # ★ 这个服务不许用 swap
# ★ 这样只保护关键服务,其他进程该换还换 —— 更精细。

# === ★ 解法选择小结 ===
# 1. 延迟敏感服务器:swappiness 调 10,几乎都适用
# 2. 改完用 swapoff -a; swapon -a 把旧页清回来
# 3. 极致低延迟服务:cgroup 给它单独 MemorySwapMax=0
# 4. 彻底关 swap 要想清楚:换来的是 OOM 风险

修复 6:swap 排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ "用了 swap" 不等于 "内存不够" ===
# 内核内存够时也会主动换出冷页,这是正常管理手段。

# === 2. ★ 看 swap 要不要紧,看 si/so 流量,不看 used 存量 ===
$ vmstat 2          # si/so 持续不为 0 才是真问题

# === 3. 真正的卡顿源是 major page fault(从 swap 换回)===
$ ps -o pid,maj_flt,cmd -p PID    # maj_flt 持续涨 = 被 swap 拖

# === 4. ★ swap 造成的停顿,GC 日志里查不到 ===
# 卡顿频率对不上 GC,就该往 swap / IO 这边查。

# === 5. 延迟敏感的服务器,swappiness 默认 60 偏高,调 10 ===
$ vi /etc/sysctl.conf 设 vm.swappiness=10; sysctl -p

# === 6. ★ 改完 swappiness,要 swapoff -a;swapon -a 清旧页 ===
# 前提:确认物理内存装得下,否则别执行。

# === 7. 彻底关 swap 要权衡:没了兜底,内存不够会直接 OOM ===

# === 8. 排查"偶发卡顿、疑似 swap"的步骤链 ===
$ free -h                          # ① swap 有没有被占用
$ vmstat 2                          # ② si/so 是否持续不为 0
$ 逐进程看 VmSwap                   # ③ 谁被换进了 swap
$ ps -o maj_flt 看那个进程          # ④ 它在不在被换入拖慢
$ 调低 swappiness + swapoff/swapon  # ⑤ 根治 + 清旧页
# 按这个顺序,swap 引起的卡顿基本能定位、能根治。

命令速查

需求                        命令
=============================================================
看内存和 swap 用量          free -h
看换入换出流量 si/so        vmstat 2
看当前 swappiness           cat /proc/sys/vm/swappiness
临时改 swappiness            sysctl vm.swappiness=10
永久改 swappiness            改 /etc/sysctl.conf 后 sysctl -p
看某进程被换出多少          grep VmSwap /proc/PID/status
看某进程的 major fault       ps -o pid,maj_flt,cmd -p PID
把 swap 里的页清回内存      swapoff -a 然后 swapon -a
彻底关闭 swap                swapoff -a + 注释 /etc/fstab 里 swap 行
给单个服务禁 swap            systemd service 设 MemorySwapMax=0

口诀:用了 swap 不等于内存不够,看 si/so 流量不看 used 存量
      卡顿源是换回页的 major fault,延迟敏感服务把 swappiness 调到 10

避坑清单

  1. swap 不是内存的备用油箱,不是内存满了才用,内核内存够时也会主动换出冷页
  2. 用了 swap 不等于内存不够,这是内核为多留文件缓存而做的主动内存管理
  3. 判断 swap 要不要紧看 vmstat 的 si/so 流量,而不是看 free 里 used 的存量
  4. swappiness 0-100 控制内核换出匿名页的积极程度,默认 60 对延迟敏感服务偏高
  5. 真正造成卡顿的是被换出的页又被访问,触发 major page fault 要等磁盘 IO
  6. swap 换入造成的停顿在 GC 日志里查不到,卡顿对不上 GC 就该往 swap/IO 查
  7. 延迟敏感的服务器把 swappiness 调到 10,临时用 sysctl 永久写 sysctl.conf
  8. 改完 swappiness 旧页还在 swap 里,swapoff -a 再 swapon -a 可把它们清回内存
  9. 执行 swapoff 前必须确认物理内存装得下 swap 里的页,否则会很慢甚至触发 OOM
  10. 彻底关 swap 要权衡,没了兜底内存一旦真不够会直接触发 OOM Killer 杀进程

总结

这次"内存明明宽裕、服务却偶发卡顿"的事故,纠正了我一个对 swap 的、几乎是望文生义的误解。在我的脑子里,swap 这个东西的形象一直特别简单,简单到像一个常识:它是内存的"备用油箱"。物理内存是主油箱,平时就烧它;只有当主油箱见了底、车子快开不动了,系统才会迫不得已,去启用那个备用的、慢吞吞的 swap。这个"备用油箱"的比喻,推导出一个在我看来天经地义的结论:系统一旦动用了 swap,就等于在向我宣告——"内存不够了"。正因为我把这两件事画上了等号,所以当 free 同时告诉我"还剩 8G 内存"和"swap 用了 2.3G"时,我的大脑里产生了一个短路般的矛盾:还剩着 8G 呢,主油箱明明还小半箱油,你启用备用油箱干什么?面对这个我自己制造出来的矛盾,我选择了一个最省事的处理方式——我说服自己"swap 用了点没关系,内存还够",然后把目光移开了。我没有去深究它,因为在我那个"备用油箱"的模型里,这个现象是无法被解释的,而一个无法被解释的现象,人是很容易选择视而不见的。复盘到根上,我才明白,swap 根本不是什么走投无路时才用的"备用油箱"。它是内核手里一个主动的、日常的内存管理工具。内核管理内存,想的从来不是"省着点用",而是"让每一页内存都用在最值的地方"。它会冷眼打量内存里所有的页:你这个进程的这块堆内存,已经好久好久没人碰过你了,你是一块"冷"的内存;而把你腾走以后空出来的这块地方,我拿去给文件读写做缓存,能实实在在地提速。于是它做了一笔在它看来稳赚的买卖:把那块冷内存写到 swap 上去,把腾出来的空间改作他用。它做这件事的时候,内存根本没有紧张——它换出冷页,不是因为"不得不",而是因为它"算过觉得划算"。而它有多热衷于做这种买卖,由一个我从来不知道的旋钮 swappiness 决定,这个旋钮的出厂默认值是 60,是一个相当热衷于换出的姿态。我的服务的卡顿,就是这笔"内核觉得划算"的买卖,转嫁到我头上的成本:它判断为"冷"的那些堆页,其实并不是真的永远不用,它们只是暂时安静;一旦某个请求又需要用到它们,这些页此刻已经不在内存、而是躺在慢吞吞的磁盘 swap 区里了,进程就只能停下来,死死地等着磁盘把它们一页一页读回来——一次内存访问,本该是纳秒级的,现在变成了毫秒级的磁盘 IO,慢了百万倍。这就是那些幽灵般的、和 GC 日志怎么都对不上的卡顿的真相。这次最大的收获,是我意识到,我用来理解一个系统组件的那个"比喻",本身就可能是整个误解的源头。"备用油箱"这个比喻,生动、好记、符合直觉,可它从根上就是错的——它把一个"内核主动的、日常的优化手段",描绘成了一个"被动的、应急的最后手段"。我不是被某个具体的技术细节绊倒的,我是被自己脑子里那个错误的比喻绊倒的。这个错误的比喻像一副有色眼镜,它不仅没帮我看清 swap,反而让我对着 free 输出里那个清清楚楚的事实,硬生生地视而不见。所以下一次,当我面对一个系统组件,发现它的真实行为和我的预期对不上、甚至像我这次一样"自相矛盾"时,我会先停下来,做一件事:我会去检查,我脑子里用来理解它的那个比喻、那个心智模型,究竟是从哪里来的?它是我真正搞懂这个东西之后提炼出来的,还是仅仅因为它的名字、它的样子,让我想当然地套上去的?当一个事实和一个模型冲突时,需要被怀疑、被推翻的,永远应该是那个模型,而不是那个事实。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

调用接口偶尔卡 5 秒整:一次 Linux DNS 解析慢排查复盘

2026-5-20 21:11:33

Linux教程

服务跑三天就报 Too many open files:一次 Linux 文件描述符耗尽排查复盘

2026-5-20 21:20:06

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索