侧边栏壁纸
  • 累计撰写 142 篇文章
  • 累计收到 2 条评论

服务器 CPU 不高但系统卡成 PPT?排查 Linux 磁盘 I/O 打满与脏页回写我踩过的几个坑

2026-10-1 / 0 评论 / 7 阅读
AI 摘要由 AI 生成

本文分析了Linux服务器CPU空闲但系统卡顿的问题,指出磁盘I/O打满和脏页回写策略是导致问题的原因。文章通过实际案例,解释了Load Average与CPU的关系,并详细介绍了如何使用iostat等工具排查磁盘瓶颈,强调关注平均等待时间而非仅吞吐量。

前阵子线上有台机器突然在群里报接口超时。我登录上去准备查日志,发现敲命令极其卡顿,连敲个 ls 都要等两三秒。

我下意识敲了个 top,结果屏幕显示的信息让我愣了一下:CPU 空闲率 id 还有将近 80%,内存也剩余不少,但右上角的 Load Average 居然飙到了 46。

CPU 明明闲着,为什么整台服务器会卡成假死状态?当时顺着排查下去,才发现是一个非常典型但又容易被忽略的连锁反应:磁盘 I/O 打满,把大量进程拖进了不可中断睡眠状态,再叠加上 Linux 默认的脏页回写策略,最终让整台机器的调度彻底僵住。

把这次排查的关键节点、指标含义和内核参数调优过程记录下来,给遇到同类情况的朋友提供一个参考。

很多人误解的 Load Average 与 CPU 关系

很多人看负载指标,习惯把 Load Average 和 CPU 核心数画等号。比如 8 核机器,负载到了 8 就觉得 CPU 跑满了,超过 8 就觉得 CPU 不够用了。

但在 Linux 系统里,这个理解只对了一半。

Load Average 统计的是处于“可运行状态”(Task Runstate,即 R 状态)以及“不可中断睡眠状态”(Task Uninterruptible Sleep,即 D 状态)的平均进程数。

  • R 状态:进程正在 CPU 上跑,或者在就绪队列里等 CPU 调度。
  • D 状态:进程通常正在等待硬件资源返回,最常见的就是等待磁盘 I/O、NFS 读写或者内核锁。在这个状态下,进程不响应任何信号,你就算在终端执行 kill -9 也杀不掉它。

当我用下面的命令扫了一下当前系统的进程状态分布:

ps -eo state,pid,cmd | awk '{print $1}' | sort | uniq -c

终端输出了几十个状态为 D 的业务进程和系统刷盘线程。这也就解释了为什么 CPU 空闲率很高而负载却有几十:不是 CPU 算不过来,而是大量进程被堵在磁盘读写门口,根本没机会进 CPU 运行。

再回看 top 首行,CPU 这一栏的 %wa(iowait)指标当时已经超过了 65%。wa 本质上也是 CPU 处于空闲状态、但在等待未完成的 I/O 请求的时间占比。

用 iostat 抓出瓶颈:别只看读写吞吐量

既然怀疑是磁盘问题,就不能只靠 df -h 看空间剩余了,得直接看 I/O 的实时调度表现。系统默认如果没有装 sysstat 包,需要先装上:

# Debian/Ubuntu
apt-get install -y sysstat

# CentOS/RHEL
yum install -y sysstat

执行扩展统计命令,每秒刷新一次:

iostat -xz 1 5

参数说明:-x 输出扩展指标,-z 过滤掉这段时间完全没有 I/O 活动的空闲设备。

输出结果里有几列关键数据:

Device    r/s     w/s     rMB/s     wMB/s   rrqm/s   wrqm/s  %rrqm  %wrqm  r_await  w_await  aqu-sz  %util
vda      0.00  420.00      0.00     45.20     0.00   180.00   0.00  30.00     0.00   485.60   28.50  99.80

这里有两个很普遍的认知坑:

坑 1:%util 到 100% 并不代表吞吐到了物理极限

以前我总觉得 %util 达到 99% 或 100% 就是磁盘彻底干不动了。其实 %util 只表示在统计周期内,设备有 I/O 请求在处理的时间百分比。

对于老式的单盘机械硬盘,%util 接近 100% 确实说明磁盘寻道能力饱和了;但如果是多盘构成的 RAID、高端 NVMe SSD 或是云厂商的分布式块存储,底层有很强的并发处理通道。即使 %util 到了 100%,也可能只是说明有持续请求在提交,不一定意味着带宽跑满。

真正要看的是 r_await 和 w_await(平均每个读写请求在队列等待和处理的总毫秒数)。

正常情况下,SSD 的写入延迟通常在几毫秒以内,云盘一般在十几毫秒左右。但在上面的监控里,w_await 飙到了 485 毫秒,平均每个写操作要等半秒钟,同时等待队列长度 aqu-sz 堆到了 28。这意味着请求积压非常严重,磁盘处理已经严重滞后。

坑 2:只看带宽,忽略了每秒 I/O 次数(IOPS)

从上面的数据可以看到,每秒写入量其实只有 45MB/s 左右,远没有达到云盘的吞吐上限。

但是 w/s(每秒写次数)加上合并的请求后,已经碰到了云服务器给系统盘分配的底层 IOPS 限制。一旦 IOPS 配额耗尽,云存储就会在底层对 I/O 进行限速(throttling),导致应用层等待队列像滚雪球一样越来越长。

用 iotop 和 pidstat 定位具体进程

知道了是写 I/O 堵塞,下一步是揪出到底是哪个进程在制造压力。

直接用 iotop 观察,加上 -o 参数只显示正在产生 I/O 的进程:

iotop -oP

如果系统没有装图形界面的工具,也可以直接用 pidstat,看是哪个进程在排队:

pidstat -d 1 5

排查发现,当时有两个写入源撞在了一起:

  1. 一个数据备份脚本在往本地写未压缩的大型归档文件。
  2. 业务运行的 PostgreSQL 数据库恰好在这个时间点触发了 Checkpoint,后台进程在疯狂往磁盘刷 dirty buffers。

但这还不是导致系统连 SSH 敲命令都卡住的最终原因。一个普通备份脚本就算再能写,顶多自己慢,凭什么把其他毫不相干的进程甚至系统基础命令都拖垮?

这就牵扯到了 Linux 内核的 Page Cache 脏页回写机制。

致命元凶:Linux 默认脏页回写触发的同步阻塞

Linux 对磁盘写操作做了非常积极的内存缓存。当应用调用 write() 写入数据时,内核并不会立刻把数据落盘,而是先写进内存里的 Page Cache,把这部分内存标记为“脏页”(Dirty Pages),然后就直接给应用返回成功。

这种机制在平时能大幅提高写入性能,但在大内存机器且面临短时间大流量写入时,会变成一颗定时炸弹。

查看当前系统的脏页控制参数:

sysctl -a | grep dirty

默认通常会看到类似这样的配置:

vm.dirty_background_ratio = 10
vm.dirty_ratio = 20
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500

这两个 ratio 参数的默认逻辑是这样的:

  • vm.dirty_background_ratio (10%):当系统里的脏页占总可用内存的比例达到 10% 时,内核的后台刷盘线程(flusher / kworker)启动,在后台异步往磁盘写数据,此时应用进程还可以继续写入。
  • vm.dirty_ratio (20%):当写入速度远快于磁盘消化速度,脏页比例继续攀升达到 20% 时,内核会触发强制同步阻塞机制。所有新发起写操作的进程,必须暂停自己的业务,自己去执行刷盘,把脏页压回阈值以下才能继续工作。

这里的问题出在大内存上。

如果是 4G 内存的小机器,20% 也就 800MB,磁盘很快就能刷完。但那台服务器配了 64G 内存,20% 意味着内核允许堆积超过 12G 的脏页。

备份脚本和数据库同时写入,在几十秒内就将 Page Cache 堆满了 12G 脏页。一旦跨过 vm.dirty_ratio 的红线,内核直接踩了刹车:所有正在写磁盘的进程全部被卡住,转为同步刷盘;而机械盘或者受限云盘的吞吐只有几十兆,要把这十几 G 的数据硬塞进磁盘,需要整整两三分钟。

在这两三分钟里,系统里的其他进程(包括打日志的 Nginx、系统自身的 auditd、甚至你终端命令产生的文件写入)只要调了写操作,就会立刻陷入 D 状态阻塞,整机瞬间表现为卡死。

内核参数优化与落地实操

搞清楚机理之后,解决思路就很明确了:不能让系统把那么多脏页囤到最后一刻才被迫同步刷盘,必须尽早让后台线程小步快跑地平滑落盘。

修改 /etc/sysctl.conf,根据实际场景调整策略。

策略 1:如果机器内存很大,直接用绝对字节数替代比例

在 32G、64G 或更高内存的服务器上,使用百分比(ratio)太粗放了,推荐改用 bytes 限制:

# 当脏页积攒到 128MB 时,后台线程立即开始异步刷盘
vm.dirty_background_bytes = 134217728

# 无论何时,脏页最多只能积攒到 512MB,超过后才阻塞写操作
vm.dirty_bytes = 536870912

注意:设置了 bytes 参数后,内核会自动将对应的 ratio 参数设为 0,两者互斥生效。

策略 2:在普通配置机器上,适当调低 ratio

如果是 8G、16G 内存的云服务器,可以直接调低百分比:

# 提高后台刷盘的主动性,降到 3% 到 5%
vm.dirty_background_ratio = 5

# 降低强制阻塞的阈值,防止一次性刷盘数据量过大
vm.dirty_ratio = 10

另外,缩短脏页在内存中的最长驻留时间(单位是百分之一秒,3000 表示 30 秒):

# 脏页存活超过 15 秒就必须刷盘
vm.dirty_expire_centisecs = 1500

# 刷盘守护进程每 2.5 秒检查一次
vm.dirty_writeback_centisecs = 250

配置修改完成后,执行命令让其立即生效:

sysctl -p

可以通过监控文件随时观察内存中的脏页实时规模:

watch -n 1 'cat /proc/vmstat | egrep "dirty|writeback"'

输出中的 nr_dirty 代表当前未写回的脏页数(以 4KB 页面为单位),nr_writeback 代表正在被写回的页面数。调优后可以看到 nr_dirty 始终稳定在一个相对平稳的低位,再也没有出现之前坐过山车式的瞬间暴涨。

应用程序与运维层面的配合

单改内核参数可以兜住底线,避免机器被大批脏页拖入死机状态,但在业务侧写脚本或做架构时,还有两点需要避坑:

  1. 大文件备份避免全走 Page Cache:写定时备份或者日志导出脚本时,如果数据量以 G 为单位,尽量给输出流加上限制,或者使用 rsync --bwlimit 这类限速参数。如果用 Python 或 Go 写备份工具,可以考虑给文件描述符加上 posix_fadvise(标记为 POSIX_FADV_DONTNEED),让系统在写完一段后立刻丢弃页面缓存,不跟业务争抢内存。
  2. 监控告警别只盯 CPU 和磁盘空间:给 Prometheus / Node Exporter 补齐两条针对性的告警规则:
    • 磁盘 w_await 或 r_await 持续 3 分钟超过 50ms。
    • 系统进程处于 D 状态的数量持续 2 分钟大于 10。

这两项指标通常比磁盘空间报警和接口 504 报警能更早预示存储瓶颈,能在整机彻底失去响应前留出足够的排查窗口。

评论一下?

OωO
取消