本文分析了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
排查发现,当时有两个写入源撞在了一起:
- 一个数据备份脚本在往本地写未压缩的大型归档文件。
- 业务运行的 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 始终稳定在一个相对平稳的低位,再也没有出现之前坐过山车式的瞬间暴涨。
应用程序与运维层面的配合
单改内核参数可以兜住底线,避免机器被大批脏页拖入死机状态,但在业务侧写脚本或做架构时,还有两点需要避坑:
- 大文件备份避免全走 Page Cache:写定时备份或者日志导出脚本时,如果数据量以 G 为单位,尽量给输出流加上限制,或者使用
rsync --bwlimit这类限速参数。如果用 Python 或 Go 写备份工具,可以考虑给文件描述符加上posix_fadvise(标记为POSIX_FADV_DONTNEED),让系统在写完一段后立刻丢弃页面缓存,不跟业务争抢内存。 - 监控告警别只盯 CPU 和磁盘空间:给 Prometheus / Node Exporter 补齐两条针对性的告警规则:
- 磁盘
w_await或r_await持续 3 分钟超过 50ms。 - 系统进程处于
D状态的数量持续 2 分钟大于 10。
- 磁盘
这两项指标通常比磁盘空间报警和接口 504 报警能更早预示存储瓶颈,能在整机彻底失去响应前留出足够的排查窗口。
文章标题:服务器 CPU 不高但系统卡成 PPT?排查 Linux 磁盘 I/O 打满与脏页回写我踩过的几个坑
文章链接:https://www.llbbs.cn/jishujaocheng/201.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?