1. 先说清楚:这篇 RH442 系列笔记想解决什么问题
很多做 Linux 运维的朋友一听到 RHCA,第一反应就是“那是架构师考的,离我太远”。直到我自己开始啃 RH442 这门课,才发现它其实非常落地。RH442 的全称是性能调优:Linux,它不教你背命令,也不要求你记住一堆内核参数的默认值,它真正训练的是“面对一台变慢的服务器,你怎么用系统自带的观测手段,一步步定位瓶颈,再做出有依据的调整”。这门课挂在 RHCA 体系里,但它解决的问题,是每个搞生产环境的人每天都会遇到的。
这篇是我 RHCA 备考系列的第 5 篇,前几篇把课程框架、工具方法论、CPU 子系统的调优思路过了一遍。这篇我打算完整复盘自己搭出来的一个虚拟内存故障实验,把从现象到定位、再到调优和验证的整条链路摊开讲。之所以选“swap 抖动”这个场景,是因为它太典型了:系统 load 很高,CPU 却不忙,内存看似还剩一大半,可应用就是慢得离谱。这种问题靠“看 top 猜答案”基本猜不中,必须沿着指标一层层往下摸。
如果你也想考 RHCA,或者你正在处理生产环境里“说不上哪里卡”的疑难杂症,这篇应该能给你一条可以照着走的思路。我已经把实验环境、排查命令、调优前后对照都记录下来,你可以直接拿去复现,也可以把这套方法移植到你自己的服务器上。
1.1 RH442 到底在训练什么能力
RH442 给我最大的冲击,是它不鼓励“改参数式调优”。很多人遇到服务器慢,第一反应是上网搜“Linux 性能优化十大参数”,然后照着 sysctl.conf 一顿改。课程反复强调的是另一套逻辑:先定义问题,再量化问题,然后用工具缩小范围,最后才动手改配置。换句话说,大部分性能问题不是靠某个神奇参数解决的,而是靠把“现象”翻译成“指标”,再用指标锁定“子系统”和“进程”。
课程里我印象很深的一个词是基线。调优前不留下基线数据,改完以后拿什么对比?凭感觉说“好像快了一点”,这在生产环境里是站不住脚的。RH442 的考试和实验都要求你先记录系统当前状态,再操作,再验证结果。这个习惯听起来简单,实际做起来特别考验耐心,但它恰恰是专业和业余的分水岭。
还有一个训练点是“知道工具看哪一层”。top 看整体负载,vmstat 看 CPU、内存、IO 的瞬时水位,pidstat 能按进程拆,perf 能往内核热点里钻,sar 负责拉长时间看趋势。工具本身不值钱,值钱的是你拿到一串数字后,能不能判断出问题出在哪个方向。这篇复盘的 swap 实验,就是一次完整的“指标翻译”过程。
1.2 为什么把这次实验单独写成第 5 篇
按照学习进度,第 5 篇正好进入内存子系统与虚拟内存管理的主题。RH442 的官方课程里,内存部分远不止 free 和 top 里的那两行,它会讲到页缓存、匿名页、回收机制、swap 设备、NUMA 亲和性,还有 systemd/cgroup 对资源的约束。这些知识点分开看都不难,但组合在一起,就会出现我接下来要复现的这种“看起来内存够用,实际上 swap 疯狂抖动”的诡异现象。
这个案例的根因落在一个很容易被忽略的点上:进程被 systemd 服务单元限制在一个过低的 MemoryMax 里。物理机内存再大,cgroup 这一层已经把进程的“可分配上限”卡死了,内核只能不停地把匿名页换到 swap。这正好把 RH442 涉及的几个重点串起来了:cgroup v2 的层次结构、memory.max 的回收逻辑、swap 参数的作用边界,以及调优后如何验证。
我把实验完整跑了一遍,过程中还踩了几个坑,比如误以为 vm.swappiness 是万能开关,比如一开始没排查 tuned profile 是否和手工参数冲突。这些经验写在文档里往往只有一句话,真正自己踩一遍才记得住。所以我决定把这第 5 篇写成一份带完整排查链路的记录,而不是知识点罗列。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基线环境与工具集合:我的调优实验台长什么样
既然是复现问题,环境就不能太随意。RH442 的学习环境最好能贴近真实服务器,但又不至于复杂到干扰判断。我最终用了 RHEL 9.3 的虚拟机,4 个 vCPU、8 GiB 内存、系统盘放在 virtio-scsi 控制器下,swap 单独划了一个 4 GiB 的分区。虚拟化层选 KVM,因为这样 CPU 型号和内存布局都稳定,不容易出现云主机上常见的 CPU steal 干扰。
2.1 实验环境的搭建细节
我建议你用一台干净的最小化安装系统来做这类实验,不要装桌面环境,也不要装一堆乱七八糟的监控代理。最小化系统带来的好处是:出问题时,你看到的现象几乎都能归因到目标服务上,不会被其他进程搅浑。磁盘方面,我特意把 swap 放在独立分区,而不是用一个 swap 文件,这样在做内存压力测试时,读写路径更接近传统物理服务器。
虚拟机的内存大小不用太大,8 GiB 足够。为了让“物理内存有富余但进程 swap 抖动”的现象能够出现,需要留出足够的空闲内存,否则系统整体内存不足导致的 swap 和 cgroup 限制导致的 swap 会混在一起,很难讲清楚。我另外给虚拟机开了 2 个 NUMA node 的拓扑,不过实际排查过程中 NUMA 并没有成为瓶颈,倒是后面验证阶段需要留意它。
装完系统之后,第一件事不是急着跑测试,而是把 tuned 的状态搞清楚。执行 tuned-adm active 看一下当前 profile,因为 RHEL 默认的 balanced 或 throughput-performance 会自带一批 sysctl 和电源管理策略。如果系统里 tuned 正在运行,你手工去改 /proc/sys/vm/ 下面的参数,可能过一阵子就被 profile 重新覆盖回去。这个坑我在早期实验里踩过一次,改完 swappiness 后验证数据发现没生效,查了半天才发现是 tuned 定时把参数拉回去了。
2.2 工具集合与各自负责的观测层
RHEL 默认源里已经带了大部分排查工具,不需要额外装太多东西。我用到的有 sysstat 系列、procps-ng 系列、systemd-cgtop、perf,以及几个压测工具。下面的表格是我在实际排查时习惯使用的分工,按“先全局、后局部、再深挖”的顺序排列。
| 工具/文件 | 主要作用 | 重点关注 |
|---|---|---|
| uptime / top | 看系统整体负载和进程粗略状态 | load average、CPU 使用率、进程 RES |
| vmstat | 快速判断 CPU、内存、IO 的瓶颈方向 | r、b、si、so、cs、us、sy |
| free | 看物理内存和 swap 的总量级 | available 列,不要只盯 free |
| sar -W / sar -B | 拉长时间看 swap 和页交换趋势 | pswpin/s、pswpout/s、pgscank/s |
| pidstat -r | 按进程观察内存行为和缺页中断 | minflt/s、majflt/s、VSZ、RSS |
| systemd-cgtop | 从 cgroup 视角看资源占用 | 内存、CPU 在控制组内的使用量 |
| /proc/pressure | 看资源饥渴程度,memory PSI 很灵敏 | some、full 的 avg10 数值 |
| perf / strace | 深挖内核态行为和系统调用 | 上下文切换来源、锁竞争等 |
这些工具不是每轮排查都要全用上,而是根据上一步的结论决定下一步往哪走。比如 top 看到 load 高但 CPU 不忙,那就该想到是不是有大量不可中断睡眠,或者进程在频繁换页,于是继续用 vmstat 看 b 列和 si/so 列。工具链条的意义就在这里:每一步都在缩小范围,直到最后锁定到具体的进程和配置项。
我在搭建实验台时还会做一个快照目录,把 sysctl -a、cat /proc/meminfo、cat /proc/cpuinfo、tuned-adm active、systemctl list-units 的输出都存下来。这个动作在考试和真实项目里都非常有用,因为性能调优往往不是一次就能成功,对比是离不开原始记录的。
3. 现象与定位:一次 swap 抖动问题是怎么被一步步揪出来的
这次实验我模拟了一个常见的“应用变慢”场景。实验环境里有一个由 systemd 管理的 Web 应用服务,我用一个脚本持续产生请求,让它的内存占用逐步爬升。最初的现象非常典型:从客户端访问接口,响应时间从几十毫秒涨到几秒,而且呈间歇性卡顿。登录服务器一看,load average 已经到了 8 左右,但 top 里 CPU 使用率并不算高,系统好像凭空背了很大的负载。
3.1 第一眼:load average 和 vmstat 给出的矛盾信号
我先执行了 vmstat 1 观察几秒钟,输出里的几列一下子吸引了注意力:
bash复制$ 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
6 1 310000 4200000 120000 1800000 12000 23000 1200 24000 9000 20000 12 35 40 13 0
这里有几个关键信息:r 列有 6,说明确实有大量线程在等待运行;si 和 so 分别是每秒换入和换出的内存量,数值很高,说明系统正在剧烈换页;cs 每秒 2 万次以上,上下文切换非常频繁;CPU 的 us 只有 12,sy 却有 35,内核态开销明显偏高。
free -h 一看,系统还剩 4 GiB 左右可用内存,swap 用了大约 300 MiB。单看 free 的输出,你可能会觉得内存完全够用,怎么会 swap 这么厉害?这就是这类问题的迷惑性:系统层面有内存,不代表进程层面能用到。如果只看 top 的前几行,很容易得出“内存没压力”的错误结论,从而把时间浪费在排查 CPU 和锁竞争上。
3.2 用 cgroup 视角找到受限的进程
接下来我用 pidstat -r 1 观察内存行为,发现那个 Web 服务的主进程每分钟都会产生大量 minor fault,RSS 不涨但 VmSwap 在持续增加。这已经指向一种可能:进程想获取更多内存,但被什么机制挡住了,于是内核反复把它的页面换到 swap 里,等它再访问时又换回来。
我用 ps -o pid,uid,cgroup,comm -p 查看进程的 cgroup 归属,发现它挂在 /system.slice/rh442-demo.service 下面。顺着路径去读控制组的状态:
bash复制$ cat /sys/fs/cgroup/system.slice/rh442-demo.service/memory.max
1073741824
$ cat /sys/fs/cgroup/system.slice/rh442-demo.service/memory.swap.max
2147483648
$ cat /sys/fs/cgroup/system.slice/rh442-demo.service/memory.current
1047322624
$ cat /sys/fs/cgroup/system.slice/rh442-demo.service/memory.swap.current
812000000
$ cat /sys/fs/cgroup/system.slice/rh442-demo.service/memory.events
low 0
high 0
max 3182
oom 0
oom_kill 0
谜底基本揭开了:服务单元把 MemoryMax 限制在 1 GiB,而当前内存用量已经顶到上限附近,memory.events 里的 max 计数一直在涨,说明内核不断尝试回收这个 cgroup 的内存。由于 MemorySwapMax 有 2 GiB,回收不掉的部分就被换到 swap 里,形成剧烈抖动。cgroup v2 的限制对进程来说是硬约束,物理机有空闲内存也帮不上忙。
3.3 根因确认:资源上限和真实需求的错位
root cause 说到这里已经很清楚了:不是系统缺内存,而是服务单元的内存上限设得太低,低于应用的真实需求。这类配置在生产环境里并不少见,可能是早期容量评估太保守,也可能是服务上线后业务增长但 unit 文件没有同步更新。
为了进一步验证,我查看了服务的 unit 配置和 systemd 的实际生效值:
bash复制$ systemctl cat rh442-demo.service
$ systemctl show rh442-demo.service -p MemoryMax -p MemorySwapMax -p TasksMax
确认 MemoryMax=1G 是配置里写死的。同时我检查了系统全局的 vm.swappiness,当前值是 30,算是一个比较中性的设置,并不是导致 swap 抖动的直接原因。真正的问题在于 cgroup 的 memory.max 太小,内核到了这个硬顶之后只能不停回收匿名页,而 swappiness 的取值只会影响“更倾向于回收文件页还是匿名页”,无法绕过 cgroup 的内存上限。
到这里,整个排查链路完成了闭环:从客户端响应慢,到 load 高、CPU 不忙,再到 vmstat 的 si/so 高,接着通过 pidstat 找到进程,再从 cgroup 文件确认限制,最后回到 unit 配置。每一步都有数据支撑,不是拍脑袋猜出来的。
4. 调整动作背后的参数逻辑:为什么不能只靠改 swap 开关
定位到根因后,接下来的问题是怎么调。很多人到这步可能直接去把 vm.swappiness 改成 0,觉得这样就不会用 swap 了,然后问题就“解决”了。这种调整往往治标不治本,甚至会把问题引到另一个方向。RH442 的课程里反复强调:改参数之前,先搞清楚这个参数到底控制什么,影响范围有多大,副作用是什么。
4.1 vm.swappiness 的语义和边界
vm.swappiness 控制的并不是“要不要用 swap”,而是内核在回收内存时对文件页和匿名页的偏好程度。文件页回收是干净的,直接丢掉,之后再从磁盘读;匿名页回收必须先把内容写到 swap,否则数据就丢了。swappiness 值越高,内核越倾向于把匿名页换出,从而保留更多文件页缓存;值越低,内核越倾向于回收文件页,尽量少碰匿名页。
我见过的不少教程喜欢说“数据库服务器把 swappiness 改成 1 或 0”,这个说法在一定场景下成立,比如应用的内存工作集稳定、大量读操作依赖文件缓存时。但如果你把 swappiness 盲目调低,面对一个本来就需要大量匿名页的进程,效果有限,因为系统里已经没有足够多的文件页可以回收了,内核最终还是得换出匿名页来满足内存需求。
回到这次的 swap 抖动:cgroup 的 memory.max 只有 1 GiB,进程想用 2 GiB。这种情况下,不管 swappiness 是 0 还是 100,内核都必须在 cgroup 的硬限制下腾出空间。差别只在于:如果系统里还有大量可回收的文件页,内核可以优先回收它们,但 anonymous 内存超过 memory.max 的那部分依然需要 swap 来承载。换句话说,swappiness 不是这个问题的杠杆点。
4.2 cgroup v2 的内存限制与 swap.max
这次真正要调整的是 cgroup 层面的资源约束。cgroup v2 将内存控制拆成了多个维度:memory.max 是匿名页加页缓存等所有内存的上限,memory.swap.max 是允许这个控制组使用的 swap 上限。两者相加,才是这个 cgroup 真正能用的总内存。以前我在 systemd 旧版本里见过 MemoryLimit 这个写法,它对应 cgroup v1 的 memory.limit_in_bytes,到了 RHEL 9 的 cgroup v2 环境,更推荐用 MemoryMax 和 MemorySwapMax。
调整方法有两种,一种改 unit 文件后 daemon-reload 再重启服务,另一种是直接用 systemctl set-property 在线修改。生产环境如果服务不能随便重启,可以用 set-property,它会写到 /etc/systemd/system.control/ 下的 drop-in 文件里,并且立刻调整 cgroup 参数。
bash复制systemctl set-property rh442-demo.service MemoryMax=2G
systemctl set-property rh442-demo.service MemorySwapMax=1G
我在实验里把 MemoryMax 从 1 GiB 提到了 2 GiB,同时把 MemorySwapMax 从 2 GiB 降到了 1 GiB。这样做的考量是:应用的真实工作集大约在 1.5 GiB 左右,给它 2 GiB 的内存上限已经不是那么紧张了;swap 保留一定空间,是为了应对瞬时尖峰,但没必要给它和物理内存一样大的 swap 额度。调完以后用 systemctl show 确认参数已经生效。
顺便说一下,如果某个服务确实不适合用 swap,更干净的方式是把它的 MemorySwapMax 设成 0,让 cgroup 在内存到顶时直接走 OOM 而不是陷入 swap 抖动。OOM 至少是明确的失败信号,监控系统能立刻报警;swap 抖动反而让系统处于半死不活的状态,CPU 被内核态回收消耗大半,应用响应时快时慢,非常难排查。
4.3 我为什么没有顺手做其他“优化”
定位到根因后,我心里其实闪过几个“顺手优化”的念头:把 transparent_hugepage 关掉,把 vm.vfs_cache_pressure 调低,甚至把 CPU 的调度器换成 performance。后来我忍住了,原因是这些调整和当前问题没有任何因果联系。THP 确实可能带来额外的内存开销,vfs_cache_pressure 也确实会影响目录项缓存回收,但它们都不是这次 swap 抖动的触发条件。
改无关参数的最大风险,是会污染验证结果。假如我同时改了 MemoryMax 和 THP,调优后系统变快了,我无法判断到底是哪个改动起了作用。生产环境的变更尤其要克制,一次只动一个变量,这是 RH442 教给我的最重要原则之一。所以这次实验最终只调整了 cgroup 限制,没有动任何全局内核参数。后续重新压测时,现象消失,指标恢复正常,就能很干净地归因到这一步改动上。
5. 验证调优结果:采样方法、指标对照和容易被忽略的雷区
调优动作做完了,不等于事情结束了。很多人改完参数,看一眼 top 觉得“好像不卡了”就走了,这种验证方式在 RH442 考试里会吃亏,在生产环境里更会埋雷。验证必须依赖数据,而且数据要覆盖足够长的时间窗口。
5.1 验证前先固定压测负载和采样基线
我的验证方案分成两步。第一步,用相同的压测脚本再打一轮请求,保持并发量和请求内容与调优前一致。这一步很关键,因为如果压测负载变了,对比就没有意义了。第二步,在压测启动前先记录一组空闲状态下的基线数据,再记录压测中的实时数据,最后取稳定阶段的平均值。
采样时长上,我至少持续了 10 分钟。为什么不是压 30 秒就下结论?因为 swap 抖动和内存回收都具有很强的突发性,短时间内的数据可能刚好落在某个波峰或波谷,得出片面的结论。10 分钟能让 si/so、CPU sy、上下文切换这些指标进入一个相对稳定的周期,统计出来的平均值才有参考价值。
除系统指标外,我还从应用侧记录了响应时间。性能调优最终要回答的问题是“用户侧体验有没有变好”,如果内核指标全好了但应用还是慢,那说明问题没有真正解决或者还有另一层瓶颈。我这边同时盯应用的平均响应时间和 p99 响应时间,因为平均时间容易被少数快请求拉低,p99 更能反映卡顿情况。
5.2 调优前后的关键指标对照
最终数据更能说明问题。下面是我从调优前后各取的一组稳定期平均值,压测负载完全一致:
| 指标 | 调优前 | 调优后 | 说明 |
|---|---|---|---|
| load average | 8.2 | 2.1 | 等待队列明显缩短 |
| vmstat si | 约 12000 KB/s | 基本为 0 | 不再持续换入 |
| vmstat so | 约 23000 KB/s | 基本为 0 | 不再持续换出 |
| cs(上下文切换/秒) | 约 20000 | 约 4500 | 内核态回收任务大幅减少 |
| sy(内核态 CPU) | 约 35% | 约 8% | CPU 从回收内存中解脱 |
| 应用 p99 响应时间 | 3.2 s | 220 ms | 用户侧体验恢复 |
| memory.events 的 max 计数 | 持续增加 | 增加停止 | cgroup 不再频繁触发上限回收 |
同时我看了 /proc/pressure/memory 这个 PSI 指标,调优前 full avg10 经常到 60% 以上,说明任务确实在被内存回收卡住,调优后 full avg10 降到 5% 以内。PSI 指标对这类问题非常敏感,是一种比 load average 更能反映“饥饿程度”的信号,非常适合用来验证内存相关调优。
5.3 验证阶段的几个雷区
第一个雷区是改了 cgroup 参数但服务没重启,导致进程还挂在旧的 cgroup 限制下。用 systemctl set-property 改的是 unit 的实时配置,但如果服务内部自己又创建了子 cgroup,或者服务已经跑在更早的 cgroup 层级里,可能不会立刻享受新限制。所以调完后一定要用 systemctl status 和 cat /proc/PID/cgroup 确认进程的控制组路径,再看 memory.max 的实际值。
第二个雷区是压测工具本身成为瓶颈。我在早期实验里用 ab 做压力工具,并发太高时,ab 所在的主机 CPU 率先被打满,导致测试结果不可信。后来我改用分布式或在独立压测机器上跑负载,才保证压测端不会干扰被测系统。自己一个人做实验时,至少要让压测进程的 CPU 占用保持在可控范围。
第三个雷区是虚拟化环境里的 CPU steal。如果宿主机资源超分,虚拟机里看到 CPU 使用率飙高,不一定是你调优的问题,可能是宿主机在抢占 CPU。实验过程中我用 top 的 st 列和 vmstat 的 st 列盯了一下,确保没有宿主机的干扰。这一点在生产云主机上尤其重要,很多“莫名其妙变慢”其实是邻居虚拟机在抢资源。
6. 从 RH442 到生产现场:一些值得长期养成的习惯
整个实验做完,我最大的体会是:RH442 教的不是某个参数怎么调,而是一套可以反复使用的决策框架。这个框架落到日常工作中,会转化成几个非常具体的习惯。如果你也在准备 RHCA,或者只是想把系统运维水平往上提一档,下面这几个习惯越早建立越好。
6.1 一次只改一个变量,保留完整的现场快照
“一次只改一个变量”这句话听起来像废话,但在真实排障过程中特别容易被打破。压力一大,人会本能地想把所有可能的优化都堆上去,结果问题解决了却说不清楚是哪个操作起效的。如果问题没解决,那更是灾难,因为你面对的是多个变量叠加的未知系统,回滚都不知道该回滚哪一项。
我现在每做一次调优,会建立一个目录,里面存下当时的内核参数、unit 配置、tuned profile、sysctl 输出和关键日志片段。这个习惯是从 RH442 实验里养成的,后来救过我一次:有一次生产环境出现类似的内存抖动,我翻出历史快照,对比发现是某次变更把服务的 MemoryMax 从 2G 改成了 1G,问题原因十分钟内就锁定了。
6.2 带着问题看指标,不要被工具牵着走
我见过太多人排查性能问题时,把 top、vmstat、iostat、sar 轮番跑一遍,收集了几屏数据,却不知道下一步该干什么。原因很简单:他们还没有提出一个明确问题,就急着用工具找答案。正确的姿势是先定义问题,比如“服务在下午三点开始响应变慢,高峰期 load 从 2 涨到 10”,然后根据这个问题预测最可能的方向,再用工具去证实或排除。
RH442 的考试和实验反复训练的就是这种“假设驱动”的思维。看到 high si/so,脑子里要能浮现出几个候选原因:物理内存不足、cgroup 限制、swap 设备太慢、NUMA 远端内存访问过多。然后用 free、cgroup 文件、iostat、numastat 一个个排除。工具只是用来帮你做排除的,真正起作用的还是你对系统原理的理解。
6.3 给同样在啃 RHCA 的同学一点个人建议
如果你准备考 RHCA,正在学 RH442,我的建议是不要只盯着题库和模拟题,一定亲手把实验环境搭起来,从零到尾排一次真实故障。RHCA 的考试方式和 RHCE 不太一样,它更看重你是否能在规定时间内独立完成一套完整的分析和调优流程。你平时在虚拟机里踩过的每一个坑,比如 tuned 覆盖了 sysctl、cgroup 文件路径记错、验证时没看 PSI,都会成为考场上的肌肉记忆。
另外,学 RH442 时尽量把每个实验写成自己的笔记,不要直接抄官方文档。抄一遍和亲手敲一遍,留在脑子里的深度完全不一样。我这篇 swap 抖动复盘就是自己实验后整理的,写的过程中还发现几个当时忽略的细节,比如 cgroup memory.events 里的 max 计数变化,比单纯看 memory.current 更能说明问题的严重程度。这种收获,是单纯看文档永远得不到的。
