1. Swap 到底在解决什么问题,先把概念和取舍掰开讲
做运维这几年,被问得最多的一类问题就是“我这台机器的 Swap 到底该不该留”。问的人里,有刚接手一台云服务器、free -h一敲发现 Swap 是 0 的新人,也有被监控告警追着跑、发现凌晨三点 Swap 使用率飙到 90% 的老手。这个问题没有标准答案,但如果连 Swap 是怎么工作的都没搞明白,直接抄别人的swapoff -a,那大概率是在给自己埋雷。
Swap(交换空间,也叫交换分区)本质上是内核在物理内存吃紧时,把一部分“暂时用不上”的内存页挪到磁盘上的一块预留区域。挪出去的过程叫换出(swap out),需要时再读回来叫换入(swap in)。它和 Windows 里的“虚拟内存”是一回事,只是 Windows 把分页文件和内存管理包装得更隐蔽,Linux 则把控制权完整交到你手里——你可以决定它有多大、在哪块盘上、什么时候用、优先级多高,甚至完全关掉。
1.1 虚拟内存、Swap、物理内存三者的关系
很多人把“虚拟内存”和“Swap”画等号,这其实不准确。Linux 上每个进程看到的地址空间都是虚拟内存,它由 MMU 通过页表映射到物理内存页帧上,这套机制跟有没有 Swap 没关系。Swap 只是虚拟内存体系里的一个“后备仓库”:当归档页(匿名页,比如 malloc 出来的堆、进程栈)在物理内存里待不下时,内核可以把它写到 Swap 里,腾出物理页给别人用。
所以判断一台机器“内存够不够”,不能只看物理内存大小,还要看工作集的常驻部分有多大。一个 8G 内存的机器跑一个 6G 常驻的 Java 应用,Swap 设 2G 可能就够它扛过流量尖峰;但如果是 8G 内存跑 16G 常驻的数据库,Swap 设多大都救不了你,只会把每次内存回收变成磁盘 IO 风暴。
再补一个常被忽略的点:文件页(page cache)不需要 Swap 也能回收,脏页直接写回磁盘就行。真正依赖 Swap 的是匿名页。这也是为什么一台纯文件服务器把 Swap 关掉,基本没啥感觉;而一台跑着大量堆内存应用的机器关掉 Swap,OOM Killer 会来得更快。
1.2 为什么有人要禁用 Swap,有人却离不开它
禁用 Swap 的主流理由有三个,都很实在。
第一个是延迟可预测性。Swap 在 SSD 上的随机读写延迟大约是内存的几百到上千倍,在一次请求链路里触发换入,尾延迟(P99、P999)会直接炸掉。对延迟敏感的中间件、实时计算节点,宁可让请求失败也不愿意让它慢慢换页。
第二个是避免“假活”。有些服务在内存不足时会疯狂换页,进程还在、端口还通、健康检查还过,但每秒只能处理几个请求,这种状态比直接挂掉更难排查。关掉 Swap 之后,内存不够就是 OOM,挂了就是挂了,问题暴露得更干脆。
第三个是特定软件的硬性要求。Kubernetes 从早期版本起就要求节点关闭 Swap,kubelet 会检查failSwapOn,不关就直接拒绝启动。Hadoop、部分分布式数据库的官方部署文档里也明确写了要关。
反过来,不能随便关 Swap 的场景同样明确:内存本身就小的开发机、跑着 Elasticsearch 这类需要大量 mmap 的中间件、还有要支持休眠(hibernate)的笔记本。休眠要求 Swap 容量至少能装下整个内存镜像,你把 Swap 删了,systemctl hibernate直接报错。
我的习惯是:服务器按用途分类决策,桌面机和个人开发机一律保留 Swap。桌面机上的浏览器、IDE、容器会瞬间把内存打满,有 Swap 兜着至少不会正在写的代码被 OOM 干掉。
1.3 先看现状:查看 Swap 使用情况的几条命令
动手之前先搞清楚现在是什么状态。别小看这一步,我见过有人在没确认swapon --show输出的情况下直接mkswap,把一块正在用的 Swap 分区重新格式化了,结果就是内核直接懵掉。
最常用的三条命令,各有侧重:
# 1. 人类可读的内存概览,看总量和已用量 free -h # 2. 专门列 Swap 设备,能看到类型、大小、已用、优先级 swapon --show swapon -s # 老系统上更通用的写法 # 3. 内核视角的原始数据,配合脚本解析很好用 cat /proc/swapsfree -h的输出里有两个字段容易被误读。buff/cache是文件缓存加上内核 slab,这部分内存“用着但可回收”,不需要紧张。available才是内核估算出来的“新起一个进程大概能拿到多少内存”,它已经扣掉了不可回收部分,判断内存是否真的吃紧,看这一列比看free那一列靠谱得多。
还有一个细节:swapon --show如果没有任何输出,说明当前系统一块 Swap 都没启用。这时候要区分两种情况——是压根没配,还是配了但没启用。前者查/etc/fstab有没有 swap 条目,后者看blkid里有没有TYPE="swap"的分区。
想把使用强度也摸清楚,可以加两条:
# 从 /proc/vmstat 看换入换出页数,单位是页(通常 4K) grep -E 'pswpin|pswpout' /proc/vmstat # 每 2 秒刷一次,观察 si/so 列(换入/换出,单位 KB/s) vmstat 2vmstat的si和so长时间非零,才是真正需要关注的信号。偶尔跳一下几十 KB 属于正常抖动,持续几百 KB/s 以上就说明内存压力已经很明显了。
注意:
pswpin/pswpout是自开机以来的累计值,看单次快照没意义,要间隔采样对比。别拿一个累计十几天的数字去判断“现在是不是在换页”。
2. 动手前的准备:容量规划、形态选型和风险自查
决定要动手之后,大部分人的第一反应是“设多大”。这个问题在网上的答案五花八门,有说内存两倍的,有说固定 8G 的,有说越大越好的。这些说法放在十年前可能在某种程度上成立,但现在的硬件和内核已经有了更细的调优手段,照抄容易踩坑。
2.1 Swap 大小怎么定,给一套能直接套用的规则
传统经验值确实来自 RAM 两倍那个年代,当时内存普遍 1G 到 2G,内存本身昂贵,用磁盘顶一顶性价比高。现在一台机器动辄 64G 内存,再按两倍算就是 128G Swap,纯属浪费磁盘。
我更倾向于按“是否需要休眠”和“内存压力来源”两个维度来定:
| 场景 | 建议 Swap 大小 | 说明 |
|---|---|---|
| 桌面/笔记本,需要休眠 | 物理内存的 1.0~1.5 倍 | 内核休眠镜像会压缩,但仍建议留足,SSD 上还要考虑恢复速度 |
| 桌面/笔记本,不休眠 | 4G~8G 固定值 | 主要用来兜底浏览器和 IDE 的尖峰 |
| 通用服务器,内存 ≤ 4G | 2G~4G | 允许一定程度换页,避免频繁 OOM |
| 通用服务器,内存 8G~32G | 4G~8G 固定值 | 超过 8G 收益递减,重点是限制单次换页规模 |
| 数据库、缓存、消息队列 | 2G~4G,swappiness 调低 | 只为应急,不参与日常换页 |
| Kubernetes 节点 | 0(关闭) | kubelet 强制要求,可用 zram 替代部分场景 |
| 容器宿主(内存敏感型) | 4G~8G,配合 cgroup 限制 | 防止某个容器把整机内存拖垮 |
固定值的思路是:Swap 的定位是“缓冲带”,不是“扩容盘”。它的作用是让内核有喘息空间去回收可回收页、让 OOM Killer 有更准确的判断依据,而不是让你把 8G 内存的机器当 16G 用。给 4G 到 8G,足够扛过一次短时的内存尖峰;给 128G,只会让系统在慢速换页中慢慢僵死。
还有个经常被忽略的容量维度:单次换出规模。内核回收内存时按 LRU 链表扫描,Swap 空间足够大时,它可能一次性换出几百 MB 的匿名页,造成明显的 IO 尖峰。有些场景下反而是“Swap 小一点”能起到断路器的作用——空间用满之后,内核只能转向直接回收(direct reclaim)甚至触发 OOM,问题暴露得更早。
2.2 分区 Swap、文件 Swap、zram 三种形态怎么选
确定要多大之后,下一个问题是“用什么形式”。主流有三种,各自的适用边界很清楚。
Swap 分区是传统做法,从磁盘划一块独立分区,mkswap格式化后启用。优点是 IO 路径最短,没有文件系统层开销,性能最稳;缺点是调整大小要动分区表,扩容得删了重建,在已经上线的机器上操作风险高。适合装机时一次性规划好的物理机。
Swap 文件是现在更推荐的方案。在现有文件系统上创建一个普通文件,mkswap之后当 Swap 用。优点是灵活,扩容就是swapoff、加文件、swapon三步,不用碰分区表;缺点是有文件系统开销,而且在不同文件系统上有不同的坑(下面细说)。绝大多数云主机、容器宿主用的都是这种。
zram是另一条路,它不是往磁盘写,而是在内存里划一块区域做压缩块设备。写入的数据先压缩再存,压缩比通常在 2:1 到 4:1 之间。好处是速度快(因为不发磁盘 IO),坏处是它吃的是物理内存,属于“用 CPU 换内存”。树莓派、ChromeOS、Android、Fedora 默认都用 zram。内存 2G 以下的机器、或者 IO 慢但 CPU 空闲的设备,zram 收益非常明显。
# 看看内核是否支持 zram modinfo zram 2>/dev/null | head -n 3 # 看系统里有没有已经建好的 zram 设备 lsblk | grep -i zram| 形态 | 性能 | 扩容难度 | 是否需要磁盘空间 | 典型场景 |
|---|---|---|---|---|
| Swap 分区 | 最优 | 高 | 是 | 物理机、装机规划 |
| Swap 文件 | 良好 | 低 | 是 | 云主机、虚拟机、临时扩容 |
| zram | 最好(无磁盘 IO) | 中 | 否(占内存) | 小内存设备、只读根文件系统 |
我自己的选择逻辑是:树莓派、路由器、小内存 VPS 优先上 zram;普通云主机用 Swap 文件;装机时就分好的物理机沿用分区。三种可以共存,靠优先级区分先后使用顺序。
2.3 开工前的备份和风险自查清单
改 Swap 属于“影响面看起来小、实际可能很大”的操作,尤其是涉及swapoff的时候。我踩过一次够呛的坑:在一台内存使用率 85% 的机器上直接swapoff -a,因为要换更大的 Swap 文件,结果内核得把所有已换出的页读回物理内存,本来剩 15% 内存根本装不下,当场触发 OOM,把线上服务干掉一个。
所以每次动手前,我一定跑这套检查:
- 确认可用内存是否大于已用 Swap。
free -h里available的值必须大于 Swap 的used值,留出至少 20% 余量。不满足就先扩容内存,或者分批停服务。 - 确认目标磁盘空间。
df -h看清楚要放 Swap 文件的挂载点的剩余空间,还要留意 inode 情况(df -i)。 - 确认文件系统类型。
df -T或者mount | grep 挂载点。ext4 基本无坑,XFS 和 btrfs 有特殊要求(见第 3 章)。 - 备份
/etc/fstab。这是最容易被改坏的文件,改错一行可能导致系统起不来。cp /etc/fstab /etc/fstab.bak.$(date +%F)只要一秒钟,能救你半小时。 - 确认是否在容器里。容器内的 Swap 行为受宿主控制,
/proc/swaps在容器内可能显示宿主的数据,别被误导。 - 确认有没有监控告警。换 Swap 期间如果触发内存告警,提前跟值班同学打个招呼,省得半夜被电话叫醒。
提示:涉及
swapoff的操作,尽量放在业务低峰期做。如果实在无法停机,改用“先加新的、再停旧的”这个顺序:swapon新文件 → 降低旧 Swap 优先级 → 观察压力 →swapoff旧的。这样内存里始终有可用的换出空间。
3. 创建并启用 Swap 的完整实操流程
准备工作做完,进正题。这一章把“从零加一块 Swap”的完整链路走一遍,两条路线:Swap 文件(推荐)和 Swap 分区。每一步我都会说明为什么要这么写,以及不这么写会出什么问题。
3.1 路线一:用 Swap 文件创建(推荐,最灵活)
假设要给/分区加一块 8G 的 Swap 文件,路径定在/swapfile。命令顺序很重要,别跳步。
# 1. 创建文件,分配 8G 的块,不写零(快) sudo fallocate -l 8G /swapfile # 如果 fallocate 不可用或不生效,退回 dd # sudo dd if=/dev/zero of=/swapfile bs=1M count=8192 status=progress # 2. 立刻锁权限,只有 root 能读写 sudo chmod 600 /swapfile sudo chown root:root /swapfile # 3. 格式化成 Swap 格式 sudo mkswap /swapfile # 4. 启用 sudo swapon /swapfile # 5. 验证 swapon --show free -h这里有几个点值得展开说。
关于fallocate和dd。fallocate只是把块标记为已分配,不实际写数据,所以秒完成。但它在某些文件系统上会导致 Swap 文件“看起来有洞”,swapon时会报swapon failed: Invalid argument。判断方法是:fallocate之后检查文件是否连续分配——用filefrag -v /swapfile看输出,如果 extent 的 physical offset 出现大量空洞或者有unwritten标记,就得改用dd。我现在的习惯是fallocate之后直接swapon,失败就换dd,两分钟内能搞定。
关于权限600。这不是可选项。Swap 文件里可能残留进程的敏感数据,权限放开就是信息泄露。内核也会校验,权限不对swapon会直接拒绝并提示insecure permissions 0644。
关于mkswap的输出。它会打印一段 UUID 和最后一行的Setting up swapspace version 1, size = 8 GiB。如果 size 明显小于预期,说明文件分配有问题,回到上一步检查。
关于 btrfs 和 XFS 的特殊处理。btrfs 上创建 Swap 文件必须先关掉 COW:
# btrfs 专用:先建空文件、关 COW、再分配 sudo truncate -s 0 /swapfile sudo chattr +C /swapfile sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfileXFS 相对简单,但要注意内核版本。老内核(3.x 早期)在 XFS 上对 Swap 文件的支持不完善,fallocate出来的文件无法启用,直接用dd最稳。另外 XFS 上的 Swap 文件大小上限受文件系统块大小和内核版本限制,超大 Swap(比如 64G 以上)建议改用分区。
关于 zram 的创建方式,简单贴一下供参考:
sudo modprobe zram # 创建 4G 的 zram 设备,压缩算法用 lz4 echo lz4 | sudo tee /sys/block/zram0/comp_algorithm echo 4G | sudo tee /sys/block/zram0/disksize sudo mkswap /dev/zram0 sudo swapon -p 100 /dev/zram0 # 优先级给高,优先用它zram 的优先级的思路是“先用快的那块”,所以给-p 100,物理 Swap 文件给-p 10。这样内核优先在 zram 里压缩存储,满了才往磁盘写。
3.2 路线二:用独立分区创建 Swap
云主机很少用这个路线,但物理机装机或者磁盘重新规划时会遇到。核心是分区表的操作,风险比建文件高一个量级,因为分区一改,相邻分区的起始位置可能被挪动,数据就没了。
安全顺序是:先备份数据,再用fdisk或gdisk划出分区,改分区表之后立刻partprobe让内核重读,最后才mkswap。
# 以 /dev/sdb 为例,假设要划一块 8G 的 Swap 分区 sudo fdisk /dev/sdb # 交互过程: # n -> 新建分区 # p -> 主分区 # <回车> -> 默认起始扇区 # +8G -> 大小 # t -> 改类型 # 82 -> Linux swap(GPT 下用 19) # w -> 写入并退出 # 让内核重新读取分区表(不需要重启) sudo partprobe /dev/sdb # 或者用这个更保险 sudo partx -a /dev/sdb # 格式化并启用 sudo mkswap /dev/sdb1 sudo swapon /dev/sdb1如果分区表是 GPT,且系统用 systemd 管理,建议顺手改一下分区类型 UUID,让 systemd 能识别:
# GPT 下把分区标记为 Linux swap sudo sgdisk --typecode=1:8200 /dev/sdb这里有个经验:能不动分区表就不动。真正需要独立分区 Swap 的场景只有“装机时预先规划”和“根文件系统是只读的嵌入式设备”。已经上线的机器扩容 Swap,用文件方案能省掉 90% 的风险。
另外一个细节,mkswap有个-L参数可以打标签,对多块 Swap 的场景很有用:
sudo mkswap -L SWAP_DATA /dev/sdb1 # 后续 fstab 里可以用 LABEL=SWAP_DATA 引用3.3 写 fstab 实现开机自动挂载,顺手调 swappiness
上面swapon启用的 Swap 是临时的,重启就没了。持久化要写/etc/fstab。
改之前先备份:
sudo cp /etc/fstab /etc/fstab.bak.$(date +%F-%H%M)然后追加一行。推荐用 UUID 而不是设备路径,因为设备名(/dev/sdb1)可能因为插拔顺序、内核枚举变化而漂移,UUID 是分区本身固有的。
# 先拿到 UUID sudo blkid /swapfile # 输出类似:/swapfile: TYPE="swap" UUID="a1b2c3d4-..." # 也可以用 mkswap 重新生成并打印 UUID sudo mkswap -U random /swapfile写入/etc/fstab,格式是六列:
# <file system> <mount point> <type> <options> <dump> <pass> UUID=a1b2c3d4-... none swap sw,pri=10 0 0六列的含义分别是:设备标识、挂载点(Swap 固定写none)、文件系统类型、挂载选项、dump 备份标志(Swap 填 0)、fsck 检查顺序(Swap 填 0)。
pri=10是优先级。如果有多块 Swap,数字大的先用。这一点在多盘机器上很有价值——把快的 NVMe 盘上那块 Swap 优先级设高,慢的机械盘设低。
写完别急着重启,用mount -a或者swapon -a验证语法:
# 先全部关掉,再按 fstab 重新启用,能过就是语法没问题 sudo swapoff -a sudo swapon -a swapon --show注意:
swapoff -a有内存风险,执行前务必确认free -h里的 available 大于 Swap used。如果你在远程 SSH 会话里操作,一旦触发 OOM 连不上机器,只能走控制台。
接下来调swappiness。这个参数控制内核换出匿名页的积极程度,取值 0-100。默认值各发行版不一样,RHEL 系一般是 30,Ubuntu 桌面是 60。数值越高,越倾向于把进程内存换出去给文件缓存腾地方。
# 临时改,立即生效,重启失效 sudo sysctl -w vm.swappiness=10 # 永久改 echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf sudo sysctl -p /etc/sysctl.d/99-swappiness.conf # 验证 cat /proc/sys/vm/swappiness关于怎么取值,我的经验是:
| 场景 | swappiness | 理由 |
|---|---|---|
| 桌面/笔记本 | 60(默认) | 允许积极换出,保住前台应用的响应 |
| 通用服务器 | 10 | 尽量不换,但保留应急能力 |
| 数据库/缓存 | 1 | 只在极端压力下换出 |
| K8s 节点 | 0(配合关闭 Swap) | 已关 Swap 时该参数无意义 |
| 小内存设备(zram) | 100 | 鼓励换出到 zram,压缩后释放物理内存 |
这里有个常见的误解:swappiness=0不等于完全不换页。内核在内存极度紧张、无法回收足够页框时,仍然会换出匿名页,这是硬性行为。所以别指望着设成 0 就能让延迟彻底稳定,真要彻底禁用,还是得swapoff。
再加一个配套参数vm.vfs_cache_pressure,默认 100。它控制内核对 dentry 和 inode 缓存的回收倾向,调高(比如 200)会更快回收目录项缓存,降低内存长期占用,代价是文件访问变慢。内存小的机器上我会设成 200 试试,效果因负载而异。
4. 修改已有 Swap 的大小和优先级
实际运维里更常见的是“改”而不是“建”。扩容、缩容、调整优先级,这一章把三种操作拆开讲清楚。
4.1 扩容:把 2G 的 Swap 扩到 8G 的完整流程
最安全的做法不是原地改文件大小,而是新建一个大文件、启用、再删掉旧文件。原因很简单:swapoff旧文件时要把它承载的页全部读回内存,这一步本身就有 OOM 风险,而原地调整大小并不能绕过这一步。
完整流程如下,假设旧 Swap 是/swapfile2G,要扩到 8G:
# 1. 看当前压力,确认能安全做 swapoff free -h swapon --show # 2. 新申请 6G 的空间(不需要 8G,因为可以复用一部分) sudo fallocate -l 6G /swapfile.new sudo chmod 600 /swapfile.new sudo chown root:root /swapfile.new sudo mkswap /swapfile.new # 3. 先启用新的,给个更低的优先级,让内核先用旧的 sudo swapon -p 5 /swapfile.new swapon --show # 4. 观察几分钟,确认内存没被压垮 vmstat 5 12 # 5. 停掉旧的 sudo swapoff /swapfile # 6. 删除旧文件,把新文件改名占位 sudo rm /swapfile sudo mv /swapfile.new /swapfile最后一步要特别注意:改名之后 UUID 没变(UUID 存在 Swap 头里,跟路径无关),所以/etc/fstab里的 UUID 条目不用改。但如果 fstab 里写的是路径/swapfile,那就得确认路径一致,否则开机swapon -a会失败。
验证:
sudo swapon -a 2>&1 swapon --show free -h还有一种偷懒做法,直接用dd追加到现有文件后面:
sudo swapoff /swapfile sudo dd if=/dev/zero of=/swapfile bs=1M count=6144 oflag=append conv=notrunc sudo mkswap /swapfile sudo swapon /swapfile这个方式看起来简洁,但有个隐患:mkswap会重写 Swap 头,如果中途断电,文件就废了。而且它依然没绕过swapoff时的内存风险。所以只在“内存充裕的小机器”上用,别在生产环境图省事。
4.2 缩容和删除:顺序错了会出大问题
删除 Swap 的正确顺序永远只有一条:
swapoff- 从
/etc/fstab里删掉对应条目 - 删除文件或分区
顺序错在前两步的话,重启后系统会尝试挂载一个不存在的 Swap,swapon -a报错,系统进入紧急模式(emergency mode),那场面就很尴尬了。
# 1. 停用 sudo swapoff /swapfile # 如果有多块,想全停 # sudo swapoff -a # 2. 编辑 fstab 删掉那一行 sudo vi /etc/fstab # 确认没有残留 grep -i swap /etc/fstab # 3. 删文件 sudo rm -f /swapfile # 4. 验证 swapon --show # 应该没输出 free -h如果确实搞坏了 fstab 导致进不去系统,补救路径是:在 GRUB 菜单按e编辑启动参数,在linux那行末尾加systemd.unit=emergency.target或init=/bin/bash,进去之后把 fstab 改回来。这个操作我做过两次,第二次是因为复制粘贴多打了一个空格。
缩容的操作跟扩容反着来:先swapoff,删掉文件重建一个更小的,再swapon。缩容前一定要确认内存余量够,因为swapoff会把已换出的页全部拉回内存。如果 available 不够,宁可先加内存再缩 Swap,别硬来。
4.3 多块 Swap 的优先级策略和分布
多块 Swap 共存时,内核按优先级从高到低使用。相同优先级的会做条带化(round-robin),相当于变相提速。
查看优先级:
swapon --show=NAME,TYPE,SIZE,USED,PRIO # 或者 cat /proc/swaps设置优先级有两个途径:
# 临时:命令行指定 sudo swapon -p 100 /dev/zram0 # zram 给最高 sudo swapon -p 20 /swapfile.ssd # SSD 上的文件 sudo swapon -p 5 /swapfile.hdd # 机械盘垫底 # 永久:写进 fstab 的 options 列 # UUID=xxx none swap sw,pri=100 0 0我在一台 32G 内存的构建机上做过这样的分层:zram 8G(pri=100)+ NVMe Swap 文件 8G(pri=20)+ SAS 盘 Swap 分区 16G(pri=5)。构建任务突发内存峰值时先用 zram,压缩存储几乎不损失速度;zram 满了才溢出到 NVMe;SAS 盘基本用不上,属于兜底。整体构建时间比单层 Swap 缩短了大约 15%,磁盘写入量也明显下降。
提示:相同优先级的多块 Swap 会做条带化,理论上能提升吞吐,但也会放大单块盘故障的影响面。生产环境如果想用这个特性,确保两块盘在不同物理设备上,别在同一个 RAID 组的两个逻辑卷上做。
另外,Swap 不要放在网络文件系统上。NFS、CIFS 上的 Swap 文件内核根本不支持,swapon会直接报错。也别放在软 RAID 上的文件系统里做 Swap 文件,IO 路径太长,延迟不可控。要用 RAID,就用 RAID 上的分区做 Swap。
5. 关闭 Swap:临时禁用、永久禁用和关闭后的补坑
前面说过,关 Swap 是个决策,不是个操作。但既然要做,就得做干净,不能只敲一句swapoff -a就完事。这一章把三种关法讲清楚,再把关闭之后必须补的坑一次性列全。
5.1 swapoff 的几种姿势分别用在什么场景
swapoff的行为差异很大,取决于你给什么参数、以及当时的内存状况。
# 关掉指定的一个 sudo swapoff /swapfile # 关掉全部 sudo swapoff -a # 关掉 /etc/fstab 里定义的所有 Swap sudo swapoff --allswapoff -a在内存紧张的机器上是高危操作。它会把所有已换出的页读回内存,而这个读回过程是同步的,期间进程会卡住。如果读回量超过了可用内存,内核只能启动 OOM Killer 挑进程杀。所以在内存使用率超过 70% 的机器上,我宁愿一个接一个地关,中间加个观察窗口:
for dev in $(swapon --show=NAME --noheadings); do echo "disabling $dev ..." sudo swapoff "$dev" free -h sleep 10 done如果swapoff卡住不动了,大概率就是在疯狂换入。这时候别 Ctrl+C 强断,因为中断可能导致部分页状态不一致。要么等它跑完,要么从另一个终端看vmstat 1确认si列在掉、说明在推进。实在不行,准备好重启。
一个小技巧:swapoff期间临时把vm.swappiness改成 0,能让内核尽量不产生新的换出,加快整个过程。
5.2 永久禁用的三种落地方式
只敲swapoff -a是临时的,重启之后 Swap 又回来了。要让禁用真正生效,得同时处理几个地方。
方式一:注释 fstab 条目。最传统、最稳妥。
sudo vi /etc/fstab # 找到 swap 那行,行首加 # #UUID=xxx none swap sw 0 0 # 验证语法,确保不会因为格式问题进紧急模式 sudo findmnt --verify --verbose方式二:用 systemd 的 swap 单元屏蔽。现代发行版(systemd 239+)会把 fstab 里的 Swap 自动转换成.swap单元。直接屏蔽比改 fstab 更干净。
# 查看有哪些 swap 单元 systemctl list-units --type=swap --all # 停掉并屏蔽 sudo systemctl stop swapfile.swap sudo systemctl mask swapfile.swap # 确认 systemctl status swapfile.swapmask会创建一个指向/dev/null的符号链接,任何试图启动它的操作都会失败。适合“我确定这台机器永远不要 Swap”的场景。
方式三:内核启动参数。在 GRUB 里加swapaccount=0并不能禁用 Swap,别被误导。正确的做法是确保没有resume=参数指向 Swap 分区,同时保证 fstab 和 systemd 里都没有 Swap 定义。单纯靠启动参数关 Swap 并不可靠,还是得回到前两种方式。
改完 GRUB 相关配置之后记得:
sudo update-grub # Debian/Ubuntu 系 sudo grub2-mkconfig -o /boot/grub2/grub.cfg # RHEL 系重启后验证:
swapon --show # 空输出 free -h # Swap 行全 0 cat /proc/swaps # 只有表头5.3 关掉 Swap 之后必须补的几个坑
这一步是很多人漏掉的。关 Swap 不是终点,它会牵动一串周边配置。
坑一:OOM Killer 变得更激进。没有 Swap 作为缓冲,内存耗尽时内核只能杀进程。要调整 OOM 保护策略:
# 看某个进程的 OOM 评分,数值越高越容易被杀 cat /proc/$(pgrep -f mysqld | head -1)/oom_score # 保护关键进程,范围 -1000 到 1000,-1000 表示几乎不会被杀 echo -500 | sudo tee /proc/$(pgrep -f mysqld | head -1)/oom_score_adjsystemd 服务可以在 unit 文件里写OOMScoreAdjust=-500,比手动改更持久。
坑二:休眠功能失效。关掉 Swap 之后再执行systemctl hibernate会报Failed to hibernate system via logind: Not enough swap space for hibernation。如果你确实要用休眠,就不能关 Swap,而且 Swap 容量要够大。折中方案是保留一块专用的小 Swap 只用于休眠,通过resume=参数指向它,业务层面用swappiness=0让它平时不参与换页。
坑三:Kubernetes 节点还要改 kubelet 配置。光是关掉 Swap 不够,kubelet 的failSwapOn默认是 true,某些版本需要显式确认。检查/var/lib/kubelet/config.yaml:
failSwapOn: true memorySwap: swapBehavior: NoSwap改完重启 kubelet:
sudo systemctl restart kubelet sudo systemctl status kubelet journalctl -u kubelet -n 50 --no-pager | grep -i swap坑四:容器内存限制的语义变了。Docker 和 containerd 早期版本不支持给容器设 Swap 限制,容器能用的 Swap 等于宿主的 Swap。宿主关掉 Swap 之后,容器的内存上限就是硬上限,超了直接 OOM kill,不会再往下滑。这其实是好事,但要让业务方知道,做好内存水位监控。
坑五:监控告警要跟着改。原来基于“Swap 使用率”的告警会全部失效(因为恒为 0)。要换成基于available内存、pgmajfault(主缺页次数)、以及容器内存使用率的告警。
# 主缺页次数,从 /proc/vmstat 看,反映真实的物理页加载 grep pgmajfault /proc/vmstat # 每个 cgroup 的内存事件统计(cgroup v2) cat /sys/fs/cgroup/<path>/memory.eventsmemory.events里的oom_kill计数是判断容器是否被杀的黄金指标,比看日志靠谱。
6. 常见问题与排查技巧实录
写了这么多原理和步骤,最后落到实战。这一章是我这些年攒下来的问题清单,按“看到什么现象 → 怎么定位 → 怎么解决”组织,可以直接当速查表用。
6.1 典型报错速查表
出错的时候人会慌,慌的时候容易乱敲命令,越搞越糟。下面这些报错我都遇到过多遍,对着表格先定位再动手。
| 报错信息 | 根本原因 | 解决办法 |
|---|---|---|
swapon: /swapfile: read swap header failed | 文件没mkswap,或者被fallocate打出空洞 | 重新mkswap;fallocate出问题就换成dd |
swapon: /swapfile: insecure permissions 0644, 0600 suggested | 权限太松 | chmod 600 /swapfile |
swapon: /swapfile: swapon failed: Invalid argument | btrfs/XFS 上的 COW 或 extent 问题 | btrfs 先chattr +C;XFS 改用dd创建 |
swapon: /dev/sdb1: Device or resource busy | 分区已经被挂载或占用 | lsblk确认用途,别误删数据分区 |
swapoff: /swapfile: swapoff failed: Cannot allocate memory | 可用内存装不下已换出的页 | 先加内存或停服务,分批swapoff,或直接重启 |
systemd[1]: Failed to activate swap /swapfile | fstab 里的 UUID 或路径不对 | blkid对照,findmnt --verify检查语法 |
Failed to hibernate: Not enough swap space | Swap 容量小于内存 | 扩容 Swap 到内存的 1.0~1.5 倍 |
zram: Cannot allocate memory | zram 设备创建时物理内存已不足 | 降低 zram 大小,或先释放内存再操作 |
补一个不在表里但很常见的现象:free -h里 Swap 的 used 一直是 0,但系统明显变慢。这时候别怀疑 Swap,去查文件 IO。可能是 page cache 抖动、或者某个进程在做频繁的小文件读写。用pidstat -d 1或者iostat -xz 1看磁盘利用率。
6.2 定位“到底是谁在用 Swap”
Swap 使用率高了,第一反应肯定是“哪个进程干的”。但 Linux 没有一个开箱即用的命令能直接按进程列出 Swap 占用量,因为 Swap 是页级别的,进程和页的对应关系在内核里,暴露得不直接。
最直接的方法是遍历/proc/*/status:
for pid in /proc/[0-9]*; do p=${pid#/proc/} name=$(grep -m1 '^Name:' "$pid/status" 2>/dev/null | awk '{print $2}') swap=$(grep -m1 '^VmSwap:' "$pid/status" 2>/dev/null | awk '{print $2}') [ -n "$swap" ] && [ "$swap" -gt 0 ] && echo "$swap kB PID=$p $name" done | sort -rn | head -20这行脚本会按 Swap 占用从大到小列出前 20 个进程。VmSwap字段单位是 kB,注意换算。跑的时候如果有权限问题,加sudo。
更方便的是smem,如果系统里有装:
# 按 PSS/USS 和 Swap 排序显示进程 smem -t -k -c "pid user command swap uss pss"smem的 Swap 列就是每个进程的 Swap 占用,比手写脚本省事,还能显示 PSS(比例集大小)这种更准确的内存指标。Ubuntu 上apt install smem,RHEL 上dnf install smem。
还有一个场景是容器里的进程。容器的/proc是否能看到宿主的进程取决于 PID namespace 配置。如果容器里看不到,就得在宿主上用crictl ps或docker top定位容器,再去读宿主的/proc。
定位到进程之后的处理思路:如果是一个内存泄漏的服务,重启能缓解但要修代码;如果是配置问题(比如 JVM 堆设太大),改配置;如果是流量突增导致的,考虑扩容或加限流。千万别人为地在高 Swap 占用时重启服务,重启瞬间会触发大量换入,可能把机器拖垮。真要重启,先swapoff一块一块地停(前提是内存够),或者用滚动重启降低压力。
6.3 几处实战中攒下的经验和坑
最后这部分是我自己的私房笔记,都是踩过的坑换来的,网上大部分教程不会提。
第一,别迷信“Swap 一定要设成内存两倍”。内存 64G 的机器设 128G Swap,除了浪费磁盘,还会让内核在内存回收时更“大胆”地换出,因为它觉得有空间。结果是长期运行在换页状态,性能持续下降而不报警。固定 4G~8G 加低swappiness是我现在的主流配置。
第二,云主机的 Swap 效果和本地盘差异巨大。同样标称 SSD 的云盘,随机小 IO 的延迟可能是本地 NVMe 的十倍以上。在云主机上,Swap 更应该定位成“防 OOM 的最后一道锁”,而不是性能手段。给 2G~4G,swappiness设 1,让它基本不参与日常换页,这个配置在大多数云环境里表现最稳。
第三,改 fstab 之后一定要findmnt --verify。这个命令会解析 fstab 并检查语法、UUID 是否存在、挂载点是否有效,比人工眼里检查靠谱得多。我现在的流程固定是:改 fstab →findmnt --verify --verbose→swapon -a或mount -a→ 重启验证。四步少一步都可能翻车。
第四,zram 在小内存 VPS 上的收益超出预期。一台 1G 内存跑静态站点和小数据库的 VPS,加 512M zram(压缩比约 3:1,相当于 1.5G 逻辑容量),在流量峰值时的响应时间明显改善,而且磁盘 IO 几乎为零。配置也很简单,几条echo命令就搞定。唯一要注意的是 zram 吃 CPU,CPU 已经跑满的机器上收益会打折扣。
第五,swapoff -a之前先看available,这条规则怎么强调都不为过。我在前文提了三次,是因为它真的很重要。一个检查动作只要两秒钟,能避免一次线上事故。养成肌肉记忆之后,free -h是每次碰 Swap 的第一个动作,比ls还顺手。
第六,Swap 文件的路径别放在/tmp或者会被 tmpfs 覆盖的挂载点下。tmpfs 是内存文件系统,你在上面建 Swap 文件,等于用内存模拟磁盘再拿来当内存用,逻辑上来回绕了一圈,实际可用容量反而变小。老老实实放/或者专门的swap挂载点,/swapfile这个路径是大多数发行版文档的惯例,就随大流。
第七,多台机器做批量化的时候,把 Swap 配置写进配置管理。Ansible 里用一个 task 搞定创建、格式化、fstab、sysctl 四件事,比手工 SSH 上去敲靠谱得多。下面这段是我常用的 Ansible 片段思路,供参考:
- name: 创建 swap 文件 command: fallocate -l 4G /swapfile args: creates: /swapfile - name: 设置权限 file: path: /swapfile mode: '0600' owner: root group: root - name: 格式化为 swap command: mkswap /swapfile register: mkswap_result changed_when: "'Setting up swapspace' in mkswap_result.stdout" - name: 启用 swap command: swapon /swapfile when: mkswap_result.changed - name: 写入 fstab lineinfile: path: /etc/fstab line: '/swapfile none swap sw,pri=10 0 0' state: present - name: 设置 swappiness sysctl: name: vm.swappiness value: '10' sysctl_file: /etc/sysctl.d/99-swappiness.conf reload: yes用 Ansible 的好处不只是省事,更重要的是幂等。creates和changed_when保证重复执行不会把已经正常的 Swap 又格式化一遍,这在批量运维里是刚需。
关于 Swap 的话题,能挖的细节其实还有很多,比如 cgroup v2 下怎么给单个容器限制 Swap、memory.swap.max和memory.max的配合逻辑、cgroup v1 和 v2 在统计口径上的差异。这些内容跟具体的内核版本和容器运行时强相关,写在这里反而容易误导人。如果你在这几个方向上遇到了具体问题,建议先用uname -r和cat /sys/fs/cgroup/cgroup.controllers确认自己处在哪套体系里,再去找对应的文档,比照着通用教程瞎调要靠谱得多。