背景
在线上 HPC 服务器上,我曾遇到这样一类问题:
- 操作系统:CentOS 7.9
- 物理内存:约 2 TB
- 未配置 swap
- 当系统
MemAvailable下降到较低水位(例如低于 100 GB)后,系统整体变慢,交互卡顿明显 - 现场观察到:
vmstat 1中运行队列变大、上下文切换显著增多,iostat -xz 1中根盘对应设备利用率升高,系统出现明显的“卡而不死”状态
从现场映射关系看:
1
2
3
4
5
ls -al /dev/mapper/
/dev/mapper/centos-root -> ../dm-0
/dev/mapper/centos-swap -> ../dm-1
/dev/mapper/tmpvg-tmplv -> ../dm-2
/dev/mapper/centos-var -> ../dm-3
也就是说:
dm-0对应根文件系统/dm-2对应/tmpdm-3对应/var
而当时 iostat -xz 1 里最忙的是 dm-0,这说明在内存压力下,主要 I/O 压力落在根文件系统对应的块设备上。
本文的目标,不是只解释“为什么慢”,而是给出一套可以在实验机上反复复现、观察、验证、缓解的完整方法,让这类问题真正被学透。
先说结论:这类“卡顿”通常不是单纯磁盘坏了,而是内存回收失衡
在“无 swap 或几乎等于无 swap”的场景中,一旦匿名页(anonymous memory,例如进程堆、栈、部分私有映射)占据了大量内存,而系统仍需为文件页缓存(page cache)、共享库、可执行文件映射页、目录项/inode slab 等保留工作集时,内核的回收空间会迅速变窄。
这时如果业务还在持续访问文件、共享库、动态链接器、二进制、配置文件、日志、临时文件,或者有扫描式读取行为,内核就可能进入一种很典型的状态:
- 可回收的 page cache 被回收掉;
- 很快又因为程序继续访问这些文件页而重新读入;
- 刚读入不久又因内存紧张再次被驱逐;
- 周而复始,形成 page-cache thrash / workingset refault。
用户侧感受到的现象,往往不是立即 OOM,而是:
- shell 变慢
top、ps、ls甚至登录都开始发飘- 应用偶发停顿
- I/O wait 上升
- 磁盘忙,但吞吐并不一定夸张
- CPU 也不一定跑满,系统却就是“很卡”
这类现象,本质上是 内存压力传导成了回收抖动、缺页抖动和文件页反复换入换出。
如何理解现场看到的 vmstat 与 iostat
1)dm-0 是什么
iostat -xz 1 看到的 dm-0,通常是 device-mapper 设备。对于使用 LVM 的系统,它经常对应某个逻辑卷。
现场已经确认:
1
/dev/mapper/centos-root -> ../dm-0
所以这里的 dm-0 就是根目录 / 对应的逻辑卷。
2)为什么根盘会忙
因为很多“看起来不像 I/O”的行为,底层其实都可能要回到根文件系统取页:
- 动态链接库
.so - 程序本体 ELF 文件
- Python 模块、Perl 模块、shell 脚本
/etc下配置文件/usr/bin、/usr/lib64等系统路径内容- 用户 home 目录下脚本、工具和缓存
- 根分区上的应用日志、临时文件、状态文件
当这些文件页因为内存压力被回收,后续再访问就会触发再次读盘,因此 dm-0 忙并不奇怪。
3)为什么 await 不一定特别大,但系统仍然卡
这是排障中最容易误判的地方。
在这类问题里,系统发卡并不要求单次 I/O 延迟特别夸张。即使 await 只是几毫秒,只要:
- 缺页频率高
- refault 频率高
- 任务线程大量阻塞/唤醒
- 运行队列积压
- reclaim 路径频繁扫描
整体交互体验一样会很差。
也就是说,问题可能不是“每次 I/O 都慢”,而是“系统不断被迫做本不该这么频繁发生的 I/O”。
4)为什么无 swap 会放大问题
没有 swap 时,匿名页几乎没有后备存储可退,内核主要只能从文件页缓存一侧回收。
结果就是:
- 文件页被更激进地回收
- 热 page cache 更容易被打掉
- 应用再次访问文件页时产生更多 major fault/refault
- 根文件系统上的读 I/O 增多
- 系统整体进入抖动
这也是为什么不少生产系统即使“几乎不希望用到 swap”,也仍然会保留一小部分 swap:不是为了让系统长期跑在 swap 上,而是为了在突发内存压力下给匿名页回收一点弹性,避免 page cache 被打穿。
一个清晰的判断框架:先判断“是回收抖动”,还是“单纯内存不够”
当再遇到类似问题时,可以按下面顺序判断。
现象层
先看三个一线视角:
1
2
3
vmstat 1
iostat -xz 1
sar -B 1
重点看:
vmstatr:运行队列是否明显升高free:是否接近底水位si/so:若有 swap,是否开始明显换入换出bi/bo:块设备读写是否持续活跃wa:I/O wait 是否上升cs:上下文切换是否显著放大
iostat- 哪个设备最忙
r/s、rkB/s是否持续上升await、svctm、%util的组合关系
sar -Bpgscank/s、pgsteal/s、pgmajfault/s是否显著增长
内存层
1
cat /proc/meminfo | egrep 'MemAvailable|MemFree|Cached|SReclaimable|Slab|AnonPages|Mapped|Shmem|Dirty|Writeback'
重点看:
MemAvailable是否逼近低水位AnonPages是否很大Cached是否在快速波动Slab/SReclaimable是否异常偏大Dirty/Writeback是否很高(若高,说明写回压力也在叠加)
缺页/回收层
1
grep -E 'pgscan|pgsteal|pgfault|pgmajfault|workingset' /proc/vmstat
连续采样,例如:
1
watch -n 1 "grep -E 'pgscan|pgsteal|pgfault|pgmajfault|workingset' /proc/vmstat"
如果以下计数增长明显:
pgscan_*pgsteal_*pgmajfaultworkingset_refault*workingset_activate*
就非常接近“内存回收抖动 + 工作集被反复驱逐”的判断了。
进程层
1
ps -eo pid,ppid,comm,%mem,%cpu,rss,vsz --sort=-rss | head -30
必要时继续:
1
2
3
4
for p in $(ps -eo pid= --sort=-rss | head -20); do
echo "===== PID $p ====="
cat /proc/$p/status | egrep 'Name|VmRSS|VmSize|RssAnon|RssFile|VmSwap'
done
目的不是只找“大进程”,而是判断:
- 是不是某一两个进程把匿名内存吃满了
- 还是很多中等进程共同挤压系统
- 进程 RSS 里匿名页和文件页大概是什么结构
实验目标:在实验机上复现“内存逼近耗尽后系统整体发卡”
实验机是一台 DELL T430 双路服务器,内存为 32GB * 2 = 64GB,配置过 16GB swap,但目前已 swapoff。
这台机器非常适合做缩小版复现实验。
实验核心思路
我们不追求把生产环境的 2TB 完全等比复制,而是复现其机制:
- 先用匿名内存把系统可用内存压到很低;
- 再并发施加文件读取压力,让 page cache 持续建立、被驱逐、再被读取;
- 观察系统在
vmstat、iostat、/proc/vmstat、用户交互上的变化; - 再分别通过“释放压力”“增加 swap/zram”“做资源隔离”“降低文件读放大”等方式验证缓解效果。
实验前准备
1)实验原则
- 仅在测试机进行,不要在生产环境操作
- 建议通过带外管理、iDRAC、KVM 或至少第二个 SSH 会话保底
- 确保有 root 权限
- 实验前确认没有重要业务在跑
2)关闭 swap(已完成)
1
2
swapoff -a
swapon --show
期望 swapon --show 无输出。
3)准备观测窗口
建议至少开 4 个终端:
终端 A:看总体态势
1
vmstat 1
终端 B:看块设备
1
iostat -xz 1
终端 C:看回收与缺页计数
1
watch -n 1 "grep -E 'pgscan|pgsteal|pgfault|pgmajfault|workingset' /proc/vmstat"
终端 D:看内存结构
1
watch -n 1 "cat /proc/meminfo | egrep 'MemAvailable|MemFree|Cached|SReclaimable|Slab|AnonPages|Mapped|Shmem|Dirty|Writeback'"
如系统安装了 sar,再补一个:
1
sar -B 1
实验一:先制造匿名内存压力
这个实验的目的是把系统推到“可回收余地变小”的边缘。
方法 A:使用 stress-ng(若已安装)
RockyLinux 上若可安装:
1
2
dnf install -y epel-release
dnf install -y stress-ng sysstat
然后例如分配约 48GB 匿名内存:
1
stress-ng --vm 6 --vm-bytes 8G --vm-keep --timeout 10m
这表示 6 个 worker,每个大约保持 8GB 内存,总计约 48GB。
也可以根据机器实时状态调整,例如 40G、48G、52G,逐步加压,不要一步顶满。
方法 B:使用 Python 匿名内存占用脚本
如果不想依赖 stress-ng,可用以下脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# mem_eat.py
import time
buf = []
chunk = 256 * 1024 * 1024 # 256MB
count = 0
try:
while True:
b = bytearray(chunk)
for i in range(0, len(b), 4096):
b[i] = 1 # 触页,确保真正分配
buf.append(b)
count += 1
print(f"allocated = {count * 256} MB")
time.sleep(1)
except KeyboardInterrupt:
print("stopped")
time.sleep(600)
运行:
1
python3 mem_eat.py
这个脚本会以 256MB 为步长逐渐吃掉匿名内存,并保持不释放。这样更适合边观察边加压。
预期现象
在这一阶段:
MemAvailable持续下降AnonPages明显增大- 系统暂时未必立刻很卡
- 如果只是单纯匿名内存占满但文件访问不频繁,I/O 可能还不明显
也就是说,光有内存紧张,不一定立刻出现最糟糕的交互卡顿。真正让系统“卡出感觉”的,往往是下一步:叠加文件工作集访问。
实验二:在低可用内存状态下,施加文件读取压力
这一步用于复现“page cache 被反复驱逐再读回”的症状。
准备一个足够大的测试文件
建议放在根文件系统 / 上,模拟 dm-0 压力:
1
2
3
mkdir -p /root/mem-lab
cd /root/mem-lab
fallocate -l 20G bigfile.bin
如果文件系统不支持 fallocate,可改用:
1
dd if=/dev/zero of=bigfile.bin bs=1M count=20480 status=progress
方式 A:顺序反复读
1
2
3
4
while true; do
dd if=bigfile.bin of=/dev/null bs=4M status=none
sleep 1
done
方式 B:使用 fio 做更贴近真实的读取模式
若已安装 fio:
1
dnf install -y fio
执行:
1
2
3
4
5
6
7
8
9
10
11
fio --name=readtest \
--filename=/root/mem-lab/bigfile.bin \
--rw=randread \
--bs=128k \
--iodepth=16 \
--ioengine=libaio \
--direct=0 \
--numjobs=4 \
--size=20G \
--time_based \
--runtime=600
这里 direct=0 的目的是让读路径经过 page cache,更容易观察“缓存建立—驱逐—refault”的现象。
预期现象
当匿名内存已经很高、可用内存很低时,再启动文件读取压力,通常会看到:
iostat -xz 1中根盘所在设备读 I/O 上升%util增高vmstat 1中bi、wa、cs抬升/proc/vmstat中pgscan、pgsteal、pgmajfault、workingset_refault_file增长更快- shell 响应开始明显变慢
- 新启动一个命令(例如
ls、bash、python3)的体感延迟变大
这就很接近生产环境里的“整体卡顿”了。
实验三:加入“共享库/程序反复冷启动”观察系统为什么更卡
前面提到一个很重要的问题:
有没有可能 cache 被反复地驱逐、加热,比如 shared libraries?
答案是:完全可能,而且这正是很多系统在内存高压下“看起来哪都没跑重活,但就是操作越来越卡”的关键原因之一。
可以用下面的办法做一个简单验证。
冷启动风格测试
在匿名内存压力 + 文件读取压力都存在时,另开一个终端反复执行:
1
2
3
4
while true; do
/usr/bin/time -f 'elapsed=%E' bash -lc 'python3 -c "import json,ssl,hashlib,subprocess" >/dev/null'
sleep 1
done
或者:
1
2
3
4
while true; do
/usr/bin/time -f 'elapsed=%E' bash -lc 'ls /usr/bin >/dev/null'
sleep 1
done
会看到什么
在内存充足时,这些命令通常很快。
但在内存逼近耗尽且 page cache 工作集被打散后:
- 这些本应“秒开”的命令启动时间会抖动
- 偶尔会突然慢很多
- 越到后面,抖动越明显
原因就在于:
- 动态链接器、共享库、解释器模块本身也是文件页
- 一旦它们不在内存里,就要重新从磁盘读入
- 如果刚读入又因回收被打掉,就会不断重复这个过程
这就是“shared libraries 被反复驱逐、再加热”的一个直观表现。
实验四:人为制造 slab / dentry / inode 压力(可选进阶)
实际生产环境里,除了匿名页与普通 page cache,目录项、inode、文件系统元数据 slab 也可能参与竞争。
可用一个大目录树做简单实验:
1
2
3
4
5
6
7
mkdir -p /root/mem-lab/tree
for d in $(seq 1 200); do
mkdir -p /root/mem-lab/tree/dir_$d
for f in $(seq 1 2000); do
touch /root/mem-lab/tree/dir_$d/file_$f
done
done
然后反复遍历:
1
2
3
4
while true; do
find /root/mem-lab/tree -type f | wc -l
sleep 1
done
观察:
1
2
slabtop
cat /proc/meminfo | egrep 'Slab|SReclaimable|SUnreclaim'
这个实验有助于理解:系统在高压下不只是“数据页”会争内存,很多元数据缓存也会参与回收与重建。
如何记录实验结果:建议建立一份“同屏观测表”
每做一次实验,都记录下面这些项目:
| 维度 | 关注点 | 典型异常 |
|---|---|---|
| 内存 | MemAvailable、AnonPages、Cached、Slab |
available 降到低水位,anon 高,cached 波动 |
| 回收 | pgscan、pgsteal、pgmajfault、workingset_refault_file |
连续快速增长 |
| I/O | r/s、rkB/s、await、%util |
根盘读活跃、util 升高 |
| CPU | wa、sy、上下文切换 |
wa、cs 上升,sy 也可能偏高 |
| 交互 | 登录、执行 ls、启动 Python |
响应明显变慢 |
| 业务 | 应用延迟、批处理耗时 | 抖动、停顿、吞吐下降 |
把“系统指标”和“人能感受到的卡顿”放在同一张表里,学习效果会比只盯着一两个命令强很多。
如何证明“问题不是单纯磁盘慢,而是内存压力诱发的 I/O 抖动”
可以做一个对照实验。
对照组 A:不吃匿名内存,只跑文件读取
只跑:
1
dd if=bigfile.bin of=/dev/null bs=4M status=none
或者 fio randread。
此时会看到有 I/O,但系统未必明显卡顿。
对照组 B:先把匿名内存压低,再跑同样的读取
在匿名内存占到 70%~85% 之后,再跑同样 I/O。
这时通常会看到:
- 交互明显变差
pgmajfault、workingset_refault_file更明显- 同样的磁盘吞吐下,用户体验显著更差
这就说明:根因不是“磁盘天然就慢”,而是内存工作集失衡让磁盘被迫承接了本不该承担的频繁冷读。
缓解与优化建议
下面按“短期止血、中期优化、长期治理”三层来讲。
一、短期止血
1)保留少量 swap,不要完全裸奔
很多人看到 swap 就本能排斥,但对大内存 Linux 服务器来说,少量 swap 往往是稳定性缓冲器,而不是性能灾难本身。
对于测试机或生产机,可以考虑:
- 至少保留一个小规模 swap 分区或 swapfile
- 例如物理内存的 1%~5%,或者按业务特征给一个更保守但非零的值
- 配合较低
vm.swappiness,避免系统过早积极换出
例如:
1
2
sysctl -w vm.swappiness=10
sysctl -w vm.watermark_scale_factor=150
说明:
vm.swappiness=10代表更保守地使用 swap,但不是禁用 swapwatermark_scale_factor可影响回收启动水位,适合实验验证,但正式上线前应结合业务压测审慎评估
2)给关键服务预留内存余量
如果一台机器承担了大量 EDA/HPC 任务,不要让任务把系统吃到几乎无余量。
建议在调度器或作业侧做控制:
- 单节点总申请内存设置上限
- 为 OS 和基础服务预留固定内存 buffer
- 对“容易膨胀”的进程设置更严格的资源申请与审计
对于 HPC/EDA 场景,这一点比单纯调内核参数更重要。
3)优先识别匿名内存大户
如果系统一旦逼近低水位就卡,第一件事常常不是“调内核”,而是先查:
1
ps -eo pid,comm,%mem,rss,vsz --sort=-rss | head -20
看是不是:
- 某类 job 的内存申请失控
- 某个进程泄漏
- 大量并发任务叠加占用
如果是业务负载把机器吃干,再漂亮的回收策略也只是延缓症状。
二、中期优化
1)考虑 zswap 或 zram
如果担心传统 swap 直接落盘影响 SSD/HDD,也可以评估:
zswap:压缩后仍以 swap 为后备zram:在内存中做压缩块设备,常用于提高内存利用效率
它们不能替代容量规划,但在“突发内存高压、又不想让磁盘成为第一落点”的场景下,往往比“完全关闭 swap”更稳。
2)把高 churn 的临时 I/O 与根分区解耦
如果某些应用会在 / 下制造大量临时文件、缓存或中间结果,建议梳理:
- 临时目录是否落到了独立文件系统
- 应用 cache 是否能迁到独立盘或独立 LV
- 日志与热点数据是否与根分区混布
虽然这不能解决匿名内存高压本身,但能减轻 dm-0 既承载系统文件又承载业务热文件的冲突。
3)减少“冷启动频繁”的链路
如果系统处于高压状态,而业务又频繁:
- 启动短命令
- 大量 fork/exec
- 动态加载大量模块
- 重复调用脚本型工具
那么 shared libraries 与解释器模块会不断被重新取页。
在可行时,可以考虑:
- 长驻 worker 替代高频短进程
- 合并脚本调用
- 避免无意义的反复冷启动
- 评估是否存在批处理框架层面的过度调度
4)用 cgroup / systemd 做资源隔离
对 HPC 节点、登录节点或混合负载机器,建议把不同业务放进不同控制组,做最基本的内存约束和保护。
例如:
- 给关键基础服务设置
MemoryLow=/MemoryMin=保护 - 给高风险批处理任务设置
MemoryMax= - 避免单类业务把系统直接拖进全局 reclaim 风暴
如果已经是 systemd 体系,很多事情并不需要很复杂的容器化才能做到。
三、长期治理
1)把“MemAvailable 低于某阈值”纳入预警,但不要只看 free
很多团队只盯 MemFree,这不够。
更合理的是结合:
MemAvailablepgmajfault/spgscan/sworkingset_refault_file- 根盘
%util/r/s - 关键应用延迟
做联动判断。
2)在监控中显式加入“workingset refault”指标
如果内核与采集链路支持,把下列指标纳入监控非常有价值:
/proc/vmstat中的workingset_refault*pgmajfaultpgscan_*pgsteal_*
这些指标比单纯看 CPU 或磁盘,更能说明“系统是不是在做无效内存回收”。
3)为调度平台建立“节点保底余量”策略
对 LSF、Slurm、K8s 这类平台,建议不要把节点可调度内存设到 100%。
应该显式保留:
- OS 基础开销
- page cache 工作集余量
- 文件系统元数据缓存余量
- 监控、代理、登录会话等系统服务余量
从平台治理角度看,这比事后救火更有效。
4)容量规划时把“热工作集”算进去,而不是只算 RSS
很多作业申报内存时只看进程 RSS,但系统稳定运行还需要:
- 文件页缓存
- 共享库页
- 文件系统元数据缓存
- 短时峰值 buffer
对于 EDA、仿真、编译、数据处理等场景,这些往往不能忽略。
一个推荐的完整实验流程
如果希望做一遍比较系统的学习,建议按下面节奏走。
Phase 1:建立基线
- 系统空闲时记录:
vmstat 1iostat -xz 1sar -B 1/proc/meminfo/proc/vmstat
- 执行几次常见命令,体会“正常交互速度”
Phase 2:只施加匿名内存压力
- 运行
stress-ng --vm ...或mem_eat.py - 把
MemAvailable压到 20GB、10GB、5GB 分阶段观察 - 记录系统是否已经出现明显卡顿
Phase 3:在低水位下叠加文件读取压力
- 在
/上创建大文件 - 循环读或用
fio - 观察
dm-0、pgmajfault、workingset_refault_file - 记录 shell / 新进程启动的体感变化
Phase 4:加入冷启动命令测试
- 反复启动 Python / bash / ls 等常用命令
- 记录耗时抖动
- 理解 shared libraries、解释器模块为何会形成额外负担
Phase 5:做对照实验
分别测试:
- 无 swap
- 有小 swap +
swappiness=10 - 启用 zram/zswap
- 降低匿名内存占用阈值
- 对实验进程加 cgroup 内存上限
最后比较:
- 系统是否仍然会“整体发卡”
workingset_refault_file是否下降- 根盘读压力是否下降
- 交互响应是否改善
通过这一步,我们不只是“知道原因”,而是能定量地知道哪种治理手段更有效。
实战排障命令清单
下面这组命令值得收藏。
1)块设备与挂载关系
1
2
3
4
lsblk
ls -al /dev/mapper/
df -hT
findmnt
2)总体压力观察
1
2
3
4
vmstat 1
iostat -xz 1
sar -B 1
sar -W 1
3)内存结构
1
2
3
4
cat /proc/meminfo
cat /proc/zoneinfo
numastat -m
slabtop
4)回收与缺页
1
2
grep -E 'pgscan|pgsteal|pgfault|pgmajfault|workingset' /proc/vmstat
watch -n 1 "grep -E 'pgscan|pgsteal|pgfault|pgmajfault|workingset' /proc/vmstat"
5)找大户进程
1
ps -eo pid,ppid,comm,%mem,%cpu,rss,vsz --sort=-rss | head -30
6)看哪些文件被频繁读(进阶)
如果内核/权限允许,可用:
1
2
3
4
perf trace
bcc-tools / bpftrace
pidstat -d 1
iotop -oPa
不过要注意:这些工具更适合在实验环境里用来“定位谁在读”,而不是在最卡的时候再临时重装、临时启用。
对生产环境的建议:不要把“未配置 swap”当成默认最佳实践
对一些延迟敏感业务,担心 swap 影响性能是可以理解的。但把 swap 完全去掉,等于把匿名页回收这条路几乎堵死,一旦内存逼近上限,page cache 更容易被打穿,最终表现出来的可能不是更快,而是更差、更抖、更难预测。
更稳健的做法通常是:
- 保留小规模 swap 或评估 zram/zswap
- 控制节点总内存占用上限
- 为 OS 与关键服务保留余量
- 监控
MemAvailable + pgmajfault + workingset_refault + 根盘读压力 - 对异常内存增长的作业做治理
对于 HPC / EDA 平台,这通常比简单地“禁用 swap 追求纯净”更实用。
结语
这类问题最容易被误判成“磁盘慢了”或者“CPU 不够了”。
但真正的根因,往往是:
- 匿名内存把可用空间压得太低;
- 无 swap 让回收策略失去弹性;
- 文件页缓存、共享库、解释器模块、元数据缓存被反复驱逐;
- 系统持续产生 refault、major fault 与 reclaim 扫描;
- 最终,所有用户都感受到一种说不清楚但非常明显的“系统发卡”。
当在实验机上亲手把这个过程一步步复现出来,再结合 vmstat、iostat、/proc/meminfo、/proc/vmstat 去看,就会真正理解:
很多“卡顿”并不是单个指标异常,而是整个内存—缓存—I/O 链路开始失衡。
理解这一点,后续不管是做 HPC 节点治理、EDA 服务器稳定性优化,还是设计容量与监控策略,都会更稳。
参考资料
- Linux kernel documentation: cgroup v2 memory statistics and workingset counters
https://docs.kernel.org/admin-guide/cgroup-v2.html - Oracle Linux Blog: Linux Swapping FAQ
https://blogs.oracle.com/linux/linux-swapping-faq - Red Hat Performance Tuning Guide:
vmstat
https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/performance_tuning_guide/sect-red_hat_enterprise_linux-performance_tuning_guide-tool_reference-vmstat - Red Hat Performance Tuning Guide:
iostat
https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/performance_tuning_guide/sect-red_hat_enterprise_linux-performance_tuning_guide-performance_monitoring_tools-iostat proc(5)/vmstat(8)/iostat(1)/sar(1)/slabtop(1)/numastat(8)man pages