Linux运维13 min read次阅读

加了内存还是卡、数据库要不要开 swap:这套 swap 与 swappiness 逻辑我调了三次生产才顺

一台 64G 内存的数据库备机,白天业务低峰,内存用到 30G 出头,按说该很闲。但应用那边反馈,每隔十几分钟就有一次几百毫秒的卡顿尖刺。我上去看,内存没满,load 也不高,唯一不对劲的是 iotop 里有个 kswapd0 进程偶尔把磁盘写带宽吃到 200MB/s,与此同时 free -hSi 列在跳数字。

那一刻我才意识到,之前对 swap 的理解是错的——它根本不是「内存满了才动」的备胎,而是一套一直在后台运行的内存淘汰调度。备机内存明明没满,它还在默默把匿名页换出去,等业务真要访问那些页时,又得换回来,卡顿就是这一进一出产生的。

swap 真正在做什么

先纠正一个最常见的误解:swap 不是「内存用光之后的垃圾桶」。Linux 的内存管理里有两个池子一直在争抢物理页——匿名页(进程堆、栈、mmap 的匿名映射,没有对应文件)和 page cache(文件缓存,读过的文件缓存在内存里加速下次读)。

系统要回收内存时,有两件事可以做:把 page cache 里的干净页丢掉(下次读文件重新从磁盘取,但文件本身还在磁盘,零风险),或者把匿名页写进 swap 分区/文件再释放(下次访问时从 swap 读回)。swappiness 控制的就是内核在「丢文件缓存」和「换出匿名页」之间偏向谁。

所以即便你内存大把空闲,只要内核觉得「换出一点匿名页、腾出内存多缓存点文件能让整体更快」,它就会动手——前提是 swappiness 给了它这个倾向。默认 60 这个值是给桌面系统调的,对很多服务器负载并不合适,这就是我那台备机卡顿的根因:60 的倾向让它过度热衷于把匿名页换出,而这些页很快又被访问。

swappiness 那个数字到底什么意思

vm.swappiness 取值 0 到 100,但它不是「换出概率」那么直白。老内核(3.5 之前)里,0 是「除非要 OOM 了否则尽量不换出」;3.5 之后内核改了语义,0 仍然会在内存压力下换出匿名页以避免 OOM,真正「几乎不换出」的档位是 1。所以网上流传的「设成 0 就关 swap」是错的,设成 0 该换还是换,想尽量抑制就设 1。

100 表示内核极度偏好换出匿名页、尽量保留 page cache。60 是中间档。关键点在于:这个值衡量的是「匿名页 vs 文件页的回收偏好权重」,不是「内存占用到百分之多少才开始换出」。很多人以为 swappiness=10 就是「用到 90% 内存才换」,不对,它影响的是回收时先挑哪种页。

看当前值:

cat /proc/sys/vm/swappiness
# 或
sysctl vm.swappiness

临时改(立刻生效,重启失效):

sysctl -w vm.swappiness=10

持久化写到 /etc/sysctl.d/99-swap.conf

cat > /etc/sysctl.d/99-swap.conf <<'EOF'
vm.swappiness=10
EOF
sysctl --system

不同的活儿,合适的档位差很多。跑 MySQL、PostgreSQL 这类对延迟敏感的数据库,匿名页一旦被换出、下次访问触发磁盘 IO,就是一次不可预测的慢查询,所以大多建议 110,宁可多丢点文件缓存也别动匿名页。纯计算、几乎不读文件的应用服务,可以 10~30。桌面环境保留默认 60 甚至更高反而更顺,因为桌面更在乎文件打开速度。

加一个 swap 文件(不重装系统、不碰分区表)

云上很多机器买来根本没 swap 分区,磁盘又用的是整块云盘没法随便切。这时候建个 swap 文件比建分区现实得多。我习惯用 fallocate 直接打洞,比 dd 快:

# 比如加 8G swap
fallocate -l 8G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
swapon --show
free -h

fallocate 的问题是某些文件系统(老 ext4 稀疏、btrfs 某些形态)它可能分配出空洞,swap 不支持空洞,稳妥起见偶尔用 dd

dd if=/dev/zero of=/swapfile bs=1M count=8192 status=progress

开机自动挂载写进 /etc/fstab,注意用 swap 类型和 sw 选项:

/swapfile  none  swap  sw  0  0

一台机器可以挂多个 swap,用优先级让它们分摊写入而不是先后顺序填坑。swapon-p 参数范围是 0~32767,数字大的优先用:

# 两个 swap 设备做负载分担
swapon -p 10 /dev/nvme0n1p2
swapon -p 10 /dev/sdb1
# fstab 里对应写 pri=10

zram:把 swap 放到压缩内存里

机械盘、甚至普通云盘做 swap,换出一次的延迟都够喝一壶。现在更常见的做法是 zram——划一块内存做压缩 swap,换出的页在内存里被压缩存储,读回几乎零磁盘延迟。现在的发行版基本自带 zram 模块:

modprobe zram
# 给 zram0 分配 8G 压缩池(注意是未压缩前的逻辑大小)
echo 8G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0

很多系统用 systemd-zram-setup@.servicezram-generator 自动管这事,配 /etc/systemd/zram-generator.conf 就行,不用手敲。对我那类内存富余、怕磁盘 swap 抖动的机器,zram 比真磁盘 swap 体验好太多——它本质是「用 CPU 换 IO」,匿名页压缩后通常能压到 30%~50%,等于白赚一倍可用内存还不拖慢。

怎么判断 swap 正在害你

卡顿怀疑 swap 时,第一眼看 vmstat 1

vmstat 1
# procs --memory-- ---swap-- -----io---- -system-- ----cpu----
#  si  so 这两列是重点
#  si  = swap in(从 swap 读回内存),单位 KB/s
#  so  = swap out(换出到 swap),单位 KB/s

只要 si/so 持续非零,说明匿名页在反复进出 swap,业务延迟必然抖。再配合 iotop 看是不是 kswapd0 在刷盘——kswapd 是内核换页守护线程,它长期占高 CPU 还带高磁盘写,就是典型的 swap 风暴前兆。

还有个隐蔽的:/proc/<pid>/status 里的 VmSwap 能看单个进程换出去多少:

for p in $(pgrep mysqld); do grep VmSwap /proc/$p/status; done

如果数据库进程的 VmSwap 一直在涨,说明 swappiness 偏高把它换出去了,这时候降 swappiness 往往立竿见影。

sar -W 1 也能持续记录换页速率,配合 sar -B(页面回收统计)一起看,能区分是 file page 回收还是 anonymous 换出占主导。

vfs_cache_pressure:另一个会坑你的旋钮

调 swap 时经常会顺手看到 vm.vfs_cache_pressure,它控制内核回收目录项(dentry)和 inode 缓存的积极程度,默认 100。很多「调优帖」会让你把它调到 50 甚至更低来「保留文件缓存」。

我吃过这个亏。有次为了多留点文件缓存把 vfs_cache_pressure 设成 50,结果短期内文件访问快了,但几天后 ls 大目录、频繁 stat 的路径开始变慢——因为目录项缓存被过度保留了,反而挤压了别的有用内存,内核回收目录项时又引发一波抖动。除非你真用 slabtop 观察过 dentry/inode 占了多少、且确认业务吃这套,否则别动它,默认值就是大多数场景的最优解。

关 swap 不是「设 0」,是要真的 off

有人想彻底禁 swap,第一步是改 swappiness=0,但前面说了那不等于关。真要关,得 swapoff

swapoff -a
# 或指定设备
swapoff /swapfile

swapoff 会把 swap 里的页搬回内存,如果内存不够搬,它会卡住等——所以关之前先 free -h 确认空闲内存够容纳当前 swap 已用量(swapon --show 里的 Used)。曾经有次我在一台内存快满的机器上直接 swapoff -a,命令挂了半小时,因为内核在拼命找内存安置那些页,期间业务跟着抖。稳妥做法是先降 swappiness、释放压力,确认 swap used 掉到 0 再 off。

数据库服务器要不要开 swap,圈子里一直有争论。我的经验:开,但 swappiness 设 1~5。开 swap 是给 OOM 一道缓冲——内存真要打满时,换成 swap 慢死总比被 OOM killer 直接挑个进程杀掉强,尤其当那个进程是数据库本身。但 swappiness 压低,保证平时几乎不换出,延迟不受损。两头好处都吃到,代价只是分几 G 磁盘。真正该关 swap 的,是那些内存极度紧张、swap 设备又是慢盘的容器节点——那种环境下 swap 只会把 OOM 拖成漫长的死亡挣扎。

一个常被忽略的联动:cgroup v2 的 swap 上限

如果机器上跑了容器或 systemd scope,光调全局 swappiness 不够。cgroup v2 里 memory.swap.max 控制每组能换出多少,设成 0 等于「禁止这组用 swap」,设成 max 是不限。我之前写过一篇 cgroup v2 资源隔离的,那里 MemoryMax 限制的是内存本身,swap 上限得单独管,否则一个组狂换出照样拖慢整机 IO。排查容器节点 swap 风暴时,记得两个维度一起看。

回过头说那台卡顿的备机。把 swappiness 从 60 降到 5,再观察 vmstatsi/so 一列很快归零,应用反馈的卡顿尖刺消失了。没加一分钱内存,没重启任何服务,就改了一个数字。内核对 swap 的默认态度是「尽量物尽其用」,但对服务器负载,这个「物尽其用」常常是以延迟抖动为代价的。理解它在回收什么、偏向谁,你才调得动它,而不是盲信「设 0 就关了」那种半截子说法。

分享:

相关文章

一台机器被一个进程拖垮:用 cgroup v2 和 systemd 把它关进笼子
Linux运维14 min read

一台机器被一个进程拖垮:用 cgroup v2 和 systemd 把它关进笼子

凌晨两点,一台共用机器 load 飙到 80,SSH 都卡。查下来是个批处理脚本 fork 爆炸,把整台宿主的 CPU 和内存吃光。这种「一个进程搞死全机」的事,ulimit 管不住、nice 降优先级也只是缓解。真正的答案是 cgroup v2——配合 systemd 的 resource control,给每个服务画好 CPU、内存、IO 的硬边界,谁越界谁先死,绝不连累邻居。

评论区