第 20 章 磁盘 I/O 毛刺——“盘没忙,却要等几百毫秒”的骗局

第 18 章是内核”暂停类”卡顿,第 19 章是 OOM 连环杀。这一章下到块设备层,看一种特别能骗人的故障:服务 RT 曲线上那些突然多 50~200ms 的尖峰,可 iostat 一瞅 %util 才 30%,await 却飙到几百毫秒。 这又是全书反复出现的母题——平均值掩盖分布。你被 %util 这个”平均利用率”骗了:盘没”忙”,但有请求在排队等盘,等的时间被平均摊薄后 %util 看着不高,可那个倒霉的请求实实在在等了几百毫秒。 我们走完整迭代环,把 IO 调度器、nr_requests、写回拥堵、“换 NVMe 没变快”的老梗,一次性讲透。


一、I/O 毛刺的脸:await 高而 %util 低的矛盾

墨:老哥,先考你一个排障现场。iostat -x 1 输出里,你最该盯哪几个数来判断”盘是不是瓶颈”?

你:%util 吧?盘利用率高就是瓶颈。await 是平均等待时间,也看。

墨:这两个数的关系,坑了无数人。 先钉死定义:

  • %util:采样周期内,磁盘至少有一个请求在处理的时间占比。它是个比例/平均值——100% 表示”采样期里盘一刻没闲”,但不告诉你排队有多长。
  • await:每个 I/O 请求从发起到完成的平均耗时(含排队 + 设备处理)。
  • aqu-sz(avg queue size,新版 iostat 列):平均队列深度——此刻有多少个请求在排队等盘。这是比 %util 更直白的”堵不堵”信号。

你:%util 低、await 高,这俩不矛盾吗?

墨:不矛盾,而且这就是毛刺现场的招牌。 想象盘一次只处理 1 个请求(机械盘寻道慢),突然同时来了 20 个请求:它们排成一队,第 1 个很快完,第 20 个要等前面 19 个都跑完。这 20 个的平均等待被拉到很高(await 高),但盘全程都在干活没闲着——等等,那 %util 应该 100% 啊?

关键在于采样周期和突发性:毛刺是秒级突发(比如每 30 秒一波写回),采样 1 秒里,可能只有 0.3 秒在排队、0.7 秒盘闲着,于是 %util 显示 30%;可那 0.3 秒里排队的那批请求,队尾的请求实打实等了几百毫秒——await 把它记下来了,但 %util 这个”平均值”把尖峰稀释了。

你:所以 %util 低,不代表没人排队等盘?

墨:完全正确。这就是”平均值掩盖分布”在 I/O 上的具体化身。 真正能让你一眼看出”堵了”的,是 aqu-sz(队列深度)和 await——前者说”排了多少”,后者说”平均等多久”。%util 只在它接近 100% 时可靠(那时肯定堵),在它低的时候不能用来证明”盘没瓶颈”。这是第 08 章埋的雷,这里引爆。


二、事故全景:支付服务的”50ms 毛刺”

📦 案例:某支付清结算服务,SSD 云盘,MySQL 主库。RT 曲线上每隔一阵出现 50~200ms 毛刺,但 iostat %util 常年在 20%~40%。

现象(第 0 层)

APM 显示:支付落库接口 P99 平时 8ms,但周期性出现 60~200ms 毛刺,频率约每分钟几次。iostat -x 1 抓到的矛盾画面:

Device  r/s  w/s  rkB/s  wkB/s  aqu-sz  await  %util
sda     ...  ...   ...   ...     12.3   180.4   31%
sda     ...  ...   ...   ...      9.8   152.0   28%

%util 才 30%,await 却 180ms,aqu-sz 高达 12。

墨:注意这个三件套矛盾:%util 低(盘”不忙”)、await 高(请求等很久)、aqu-sz 高(队列里排着 12 个)。第一判断:盘没被榨干,但请求在排队等盘——瓶颈是”排队”,不是”盘吞吐”。这把方向直接指向量、调度、写回,而不是”换更快的盘”。

推断 1:是不是盘空间满了?(可能推错)

你:我先看 df,是不是盘写满了导致分配卡?

墨:查 df -h——使用率 55%,没满;inode 也没满(第 16 章 inode 耗尽是另一类坑,这里不是)。结论:推错。

推断 2:是不是慢 SQL 导致?(部分对,但没到根)

你:那是不是 SQL 慢,扫描行多,拖高 RT?

墨:查 slow log 和 DB 端——毛刺时刻 DB 的 Innodb_data_pending_reads(待处理物理读)确实涨,iowait 也高。但DB 的 I/O wait 是”跟着应用毛刺一起涨”的,且 DB 自己没发起重查询——又是”受害者同步涨”的特征,DB 不是源头,是被 I/O 排队连带。

结论:SQL 不是根因,但 I/O 排队是实锤。 继续查排队从哪来。

再观察:锁死”排队”的传动链

你:队列里排着 12 个请求,盘却只 30% 忙——到底谁在制造突发?

墨:对齐时间轴 + 看 iostat 的 w/s(写次数)和 pg/mm 类指标,发现毛刺和两件事强相关:

  • 周期性写回:每 30 秒一波大量 w/s 写入,aqu-sz 同步飙;
  • 突发读:某些查询缓存未命中,瞬间一堆随机读排队。

而盘是 SSD 云盘,默认 IO 调度器是 mq-deadline,且 nr_requests(队列深度)沿用云镜像默认的小值。机械盘/老内核的默认值,放在云 SSD 上,队列太浅,突发一来就堵在队列里。

推断 3:IO 调度器 + nr_requests 太小,突发排队

墨:第一股力量——队列深度和调度策略不匹配云盘。云 SSD 能并行处理很多请求,但默认 nr_requests 可能只有 128 甚至更小,且调度器把请求按” deadline”排,突发随机读写时队尾请求等很久。

解法:

# 看当前调度器
cat /sys/block/sda/queue/scheduler
# 云 SSD / NVMe:用 none(原 noop)让设备自己并行,别让内核多管闲事
echo none > /sys/block/sda/queue/scheduler
# 调大队列深度,给突发留出缓冲
echo 1024 > /sys/block/sda/queue/nr_requests
# 读预取(顺序读场景有益,随机小 IO 场景反而污染缓存,按需调)
echo 128 > /sys/block/sda/queue/read_ahead_kb

墨:调度器选择本身是个权衡(第 08 章详述):

  • none(noop,mq 下即 none):几乎不排序,直接甩给设备。适合 NVMe/SSD 这种本身能并行、自带队列的盘——内核再排序纯属添乱。换来:延迟低、吞吐高;代价:完全不做”合并相邻请求”的优化(对 SSD 无所谓,对机械盘有害)。
  • mq-deadline:保证请求有 deadlines,避免饿死,兼顾吞吐与公平。机械盘和通用场景的稳妥默认。
  • bfq:为”交互公平/桌面低延迟”设计,按进程公平调度,会牺牲一点吞吐换交互流畅。服务器数据库场景通常不如 none/mq-deadline。
  • kyber:针对快设备低延迟,自适应调节。

调整 1:上 none + 调大 nr_requests

墨:运维把云 SSD 调度器改成 none、nr_requests 调 1024。观察:aqu-sz 从 12 降到 2~3,await 从 180ms 降到 20ms 内,毛刺幅度减半。但仍有偶发 80~120ms 毛刺——说明排队只是第一股,还有第二股。

再观察:第二股——脏页写回拥堵

你:排队降了,怎么还有毛刺?

墨:继续盯,发现剩余毛刺和大量写 w/s 强相关,且卡顿瞬间 jbd2(ext4 journal 线程,第 07 章)CPU 占用尖峰。这就是第 07 章讲过的写回拥堵 + journal 连带:脏页堆积到 dirty_ratio 阈值,前台进程被逼同步刷盘 + 写 journal 事务,一卡几百毫秒。

推断 4:脏页阈值太大,前台被逼 sync

墨:第二股力量——vm.dirty_ratio(默认 20%)太大,允许脏页攒到内存的 20% 才触发前台同步刷盘,一旦触发就是”攒了一大坨一次性 sync”,形成写毛刺。

解法(深化第 12/13 章):

sysctl -w vm.dirty_background_ratio=5   # 后台更早开始异步刷
sysctl -w vm.dirty_ratio=10             # 前台被逼 sync 的阈值降低,单次刷的量变小,毛刺更平

调整 2:压低脏页阈值

墨:dirty_ratio 从 20 调到 10、dirty_background_ratio 到 5。观察:剩余毛刺从 80120ms 降到 3040ms。接近基线了,但还有极偶发的 50ms 尖峰。

再观察:第三股——“换 NVMe 没变快”的老梗

你:排队和写回都治了,怎么还剩尖峰?是不是该换更快的盘了?

墨:先别换盘,这是最经典的浪费。 我们查 MySQL 的 buffer pool 命中率:Innodb_buffer_pool_read_hit(或 1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)。结果:命中率 91%。

你:91% 不是挺高吗?

墨:91% 听着高,但算笔账——buffer pool 只有 8G,要管 50G 的热数据。每天请求量巨大,那 9% 的未命中 = 每秒几千次物理读落盘。盘再快,这几千次随机读也必然形成尖峰(尤其是云盘有突发 IOPS 配额,超了就排队)。瓶颈根本不在”盘慢”,在”缓存装不下工作集,硬读盘”。

推断 5:瓶颈在缓存命中率,不在盘

墨:第三股力量浮出:不是盘的问题,是 buffer pool 太小、工作集太大,物理读不可避免。换 NVMe 只能把”每次物理读”从 0.5ms 降到 0.1ms,但几千次/秒的物理读总量不变,尖峰形态还在——这就是”换 NVMe 没变快”的老梗真相(第 08 章那个事故同款)。

解法:扩 buffer pool 到能覆盖工作集(如 32G),或预热 + 调整工作集(归档冷数据)。运维把 innodb_buffer_pool_size 从 8G 调到 32G,重启预热。

改进:毛刺归零,收口

墨:三股力量依次按住(buffer池排队 / 脏页写回 / 缓存命中率),毛刺从 200ms 降到个位数,和基线齐平。复盘:

RT 毛刺 + %util低 + await高 + aqu-sz高
  → 排除 盘满/慢SQL(它们是受害者)
  → 第一股:队列浅+调度器不匹配云盘 → none + nr_requests 调大 (aqu-sz 12→2)
  → 第二股:脏页阈值大,前台 sync 拥堵 → dirty_ratio/bg 调小
  → 第三股(老梗):buffer pool 8G 管 50G,9% 物理读必成尖峰 → 扩 buffer pool

你:所以”换更快的硬件”往往是最后一步,不是第一步。

墨:对。而且这章最该刻进骨头的,是 %util 低 ≠ 没瓶颈。 aqu-sz 和 await 才是排队真相。再加一条:很多”盘慢”的锅,最后发现是”缓存没盖住工作集”——那是应用/配置问题,不是硬件问题。


三、配套参数:块层你能拧的旋钮

旋钮文件 / 参数默认拿什么换什么
IO 调度器/sys/block/<dev>/queue/schedulermq-deadline(多数)none(SSD/NVMe 低延迟) ↔ 失去合并优化(对 SSD 无妨)
队列深度/sys/block/<dev>/queue/nr_requests128 左右调大缓突发 ↔ 略多内存
读预取.../queue/read_ahead_kb128顺序读加速 ↔ 随机小 IO 污染缓存
脏页前台阈值vm.dirty_ratio20%调小写毛刺更平 ↔ 前台刷更频
脏页后台阈值vm.dirty_background_ratio10%调小更早异步刷 ↔ I/O 更频繁
设备队列(多队列).../queue/nr_hw_queues硬件决定多队列并行 ↔ —
readahead(文件系统挂载)mount -o readahead=—顺序读收益 ↔ 内存

墨:nr_requests 和 read_ahead_kb 常被一起调但作用完全不同:前者是”设备能同时挂着多少个请求”(治排队),后者是”顺序读时一次预取多少”(治吞吐/命中)。别混。


四、参数总表(这一章动过的”拿什么换什么”)

参数 / 文件默认本次取值拿什么换什么
queue/schedulermq-deadlinenone(云SSD)低延迟 ↔ 不做请求合并(SSD 无所谓)
queue/nr_requests~1281024缓突发排队 ↔ 略多内存
vm.dirty_ratio20%10%写毛刺平 ↔ 前台刷更频
vm.dirty_background_ratio10%5%更早异步刷 ↔ I/O 更频繁
innodb_buffer_pool_size8G32G覆盖工作集、灭物理读尖峰 ↔ 占更多内存

五、🔧 思考题(都配参考答案)

思考题 1(基础): iostat -x 里 aqu-sz=12、await=180ms、%util=30%。请解释这个看似矛盾的组合说明了什么,并指出你第一判断该往哪查。

【参考答案】

矛盾解释:%util=30% 是”采样期里盘至少有一个请求在处理的时间占比”,是个平均值,它被突发稀释了(可能只有 0.3 秒在排队、0.7 秒盘闲);而 aqu-sz=12 表示此刻队列里排着 12 个请求、await=180ms 是这些请求的平均等待——说明盘没被榨干,但请求在排队等盘,队尾请求实打实等了上百毫秒。 这正印证”平均值掩盖分布”:%util 低不能证明”没瓶颈”。第一判断:瓶颈是排队/调度/写回,不是盘吞吐——往 IO 调度器(queue/scheduler)、nr_requests、脏页写回(dirty_*)、以及应用层缓存命中率(buffer pool 大小)去查,而不是急着换更快的盘。

思考题 2(深入): 云 SSD 上为什么常把调度器从 mq-deadline 改成 none?机械盘反过来吗?说明各自的权衡。

【参考答案】

云 SSD / NVMe:设备自身能并行处理大量请求、自带硬件队列,内核再做”按 deadline 排序、合并”纯属多余,甚至因为多一道排序引入延迟。改 none(noop)让请求直接甩给设备并行处理,换来更低延迟、更高吞吐;代价是放弃内核层的请求合并优化——但对 SSD 而言合并收益极小(随机 4K 本就难合并),所以划算。 机械盘:寻道是物理动作,顺序相邻请求合并能大幅减少寻道,此时 mq-deadline(或老 deadline)的合并+截止时间保证更有价值,none 反而让随机 IO 寻道爆炸。所以机械盘通常保留 mq-deadline,不要改 none。 一句话:设备能并行→none;设备怕寻道→deadline/bfq。

思考题 3(权衡): 把 vm.dirty_ratio 从 20% 调到 5%,能消毛刺,但有什么代价?什么场景反而不该调太小?

【参考答案】

代价:dirty_ratio 是”脏页占内存比例到这值,前台进程被逼同步刷盘”的阈值。调小→前台更早被逼亲自 sync,单次刷的量变小,写毛刺更平;但换来:前台进程被卡去刷盘的频率变高,且后台刷(dirty_background_ratio)也更频繁,总体写 I/O 更碎、更频繁,对”突发大块顺序写(如批量导出、日志流水)“这类场景,反而因为频繁打断而吞吐下降。 不该调太小的场景:写密集且以顺序大块为主、且能容忍宕机丢少量脏数据的(备份、日志归档)——它们希望脏页多攒点一次性刷,吞吐优先。所以脏页阈值是经典权衡:写延迟敏感(数据库)调小,吞吐优先(批量写)调大。

思考题 4(进阶总账): 一个服务”RT 偶发毛刺,%util 常年低”。请给你的一套分层排查顺序(从最可能到最不可能),每层对应看什么、调什么。

【参考答案】

按”最可能且最便宜先查”的顺序:

  1. 应用层缓存命中率(最常见且最被忽视):查 MySQL Innodb_buffer_pool_read_hit、Redis 命中率、本地 cache 命中。命中率低→物理读落盘成尖峰(本案第三股)。调:扩 buffer pool / 加缓存 / 预热。多数”盘慢”锅其实是这层。
  2. 写回拥堵:iostat 看 w/s 与毛刺同步、jbd2 CPU 尖峰。调 dirty_ratio/dirty_background_ratio。
  3. IO 调度与队列:aqu-sz 高、%util 低→看 queue/scheduler(云盘改 none)、nr_requests 调大。
  4. 才到最后:换更快的盘。只有前三层都排除、确认是盘吞吐/IOPS 配额真不够时才换。 顺序的核心:先软(缓存/配置)后硬(换盘),且永远先看 aqu-sz/await 而非 %util。

六、第二个现场:云盘”突发 IOPS 配额”导致的周期性卡顿

墨:第 20 章第一个现场讲了”排队 + 写回 + 缓存命中”三股力量。这第二个现场,把镜头对准云盘特有的”突发 IOPS 配额”——这是云上最容易被误判成”盘慢”的坑,和第一个现场的”排队”是表亲。

📦 案例:某云主机挂载云 SSD,数据库每小时整点出现 30~60 秒 IO 毛刺,iostat看aqu-sz飙但%util 才 40%。

现象(第 0 层)

整点毛刺:await 从 2ms 飙到 200ms,aqu-sz 飙到 20+,但 %util 不高。iostat 和第一个现场像,但发生时间高度规律(每小时整点)。

推断 1:是不是第一个现场的队列问题?(可能推错)

你:又是 aqu-sz 高,是不是调度器/nr_requests 的问题? 墨:先按第一现场治:改 none 调度器、nr_requests 调大——毛刺照旧。说明根因不是”队列浅”,是”有规律的突发源头 + 云盘配额”。(这里推错是值得的:它排除了第一现场的机理,把嫌疑收敛到”外部配额 + 定时任务”。)

再观察:整点任务 + 云盘 IOPS 配额

墨:对齐时间轴——毛刺和整点的统计/备份任务完全吻合;再查云厂商文档,该云盘有**“基准 IOPS + 突发额度”**模型:平时积累突发 credits,整点大批量写瞬间把 credits 花光,超出基准 IOPS 的部分被云底层限流(throttle),请求排队,aqu-sz 飙、await 高,但盘本身没忙(%util 低——因为云底层在”主动限流”而非”盘处理不过来”)。

推断 2:云盘 IOPS 配额耗尽 + 限流(命中)

墨:这和第 20 章母题”平均值掩盖分布”同款,但根因是云厂商的 IOPS 配额,不是本地盘队列。证据:%util 低 + 整点规律 + 改本地调度器无效 + 云盘有突发模型。

调整

墨:三剂药:

  1. 错峰:把整点大批量任务打散到非整点、限流(QPS 限流 + 分批),避免瞬间把 credits 花光。
  2. 提配额:买更高基准 IOPS 的云盘档位(或开启”预置 IOPS”付费),让基准就够用。
  3. 应用层削峰:写操作加 rate limiter,把突发摊平,别让一瞬间灌爆配额(呼应第 24 章”缓冲治突发不治过载”——这里缓冲/限流是治突发的正解)。

改进:毛刺消失,收口

墨:打散 + 限流后,毛刺归零。复盘:云上 IO 毛刺,根因可能在”云厂商的配额模型”,不在你的内核参数。 排障时,%util 低 + 规律突发 + 改本地参数无效,就要怀疑云盘 IOPS/吞吐配额。这也呼应第 20 章那句话:“先软(配置/限流)后硬(换盘)“,而云盘场景”换盘档位”是最后一步但不是第一步——先确认是不是配额。


七、IO 排障的总账(把第 8、20 章收成武器)

墨:把块层 IO 相关的排障收成一张决策表,你以后任何”IO 毛刺/RT 高”现场照表走:

症状第一怀疑验证治本
aqu-sz高+await高+%util低请求排队等盘iostat -x 1调度器改 none(SSD)+nr_requests↑
w/s同步暴涨+jbd2 CPU高脏页前台 synciostat w/s,cat /proc/meminfo Dirtydirty_ratio/bg↓
规律突发+%util低+云盘IOPS 配额限流对齐定时任务+云盘模型错峰+限流+提配额
P99 毛刺+buffer hit低缓存未盖住工作集Innodb_buffer_pool_read_hit扩 buffer pool/预热
机械盘随机读慢寻道iostat r_await高换 SSD/合并 IO/减少随机
写放大(SSD)频繁小写/sys/.../statio批量写/合适 FS 块大小

墨:这张表的核心判断法是第 20 章的**“%util 低 ≠ 没瓶颈,aqu-sz+await 才是排队真相”**,加上”规律突发先怀疑外部配额/定时任务”。把这两个铁律焊死,IO 排障能从”瞎换盘”升级成”5 分钟定位”。



八、第三个现场:中断没绑核、RPS 没开,单 CPU 软中断成 IO 瓶颈

墨:前两个现场讲的是”盘这侧”的排队和配额。这第三个现场,把镜头拉到”CPU 这侧”——IO 数据回来了,但处理它的软中断全挤在一颗核上,这颗核被打满,吞吐反而上不去。这是第 09 章”软中断”机制在现场的又一次现身。

📦 案例:某高 PPS(每秒包数)的网关,迁移到多队列网卡 + 多核后,网络吞吐不升反降,top 看一颗核 si(软中断)100%,其余核闲着。

现象(第 0 层)

网络吞吐卡在预期的一半。top 按核心看:一颗核 si 长期 100%,其他核 si 接近 0。网卡是多队列的(ls /sys/class/net/eth0/queues/ 有 rx-0..rx-N),但软中断好像没分摊开。

推断 1:网卡队列不够 / 盘慢?(推错)

你:是不是队列数太少,或者后端处理慢? 墨:查 /proc/interrupts——所有网卡队列的硬中断都路由到了同一颗 CPU(IRQ affinity 全绑在一核);且即便多队列,软中断(NET_RX)的处理也只跑在那一核。盘侧 iostat 正常,后端 CPU 其他核闲。排除”盘慢/后端慢”,锁定”软中断单核瓶颈”。

推断 2:IRQ affinity + RPS 都没开(命中)

墨:传动链——现代网卡多队列,硬中断可由 IRQ affinity(/proc/irq/<n>/smp_affinity) 分散到多核;但软中断(NET_RX,真正处理包的逻辑)默认在哪个核收到硬中断,就在哪个核跑。若硬中断全挤一核,软中断也挤一核。解决是两层:

  • 硬中断分散:把各队列的 smp_affinity 绑到不同核(或用 irqbalance 自动)。
  • 软中断再分散:开启 RPS(Receive Packet Steering)——在 /sys/class/net/eth0/queues/rx-N/rps_cpus 写 CPU 掩码,让软中断能被调度到其他核并行处理(等效于”软件版多队列”)。高 PPS 场景 RPS 是标配。
  • 发送侧对应 XPS(Transmit Packet Steering):tx-N/xps_cpus,让发送也分散。

调整

墨:两剂药:

  1. 开 irqbalance 或手动设 smp_affinity,把多队列硬中断铺满多核。
  2. 开 RPS/XPS:echo <cpu_mask> > /sys/class/net/eth0/queues/rx-*/rps_cpus,把软中断处理摊到其他核。 治完后那颗 100% 的核降下来,吞吐翻倍。

改进:吞吐翻倍,收口

墨:这个现场和第一现场(“队列浅 + 调度器不匹配云盘”)是表亲——都是”资源(CPU/盘队列)明明有,却因没分散而挤在一点上”。它把第 09 章的”软中断单核瓶颈”从一个机制,变成了一个你亲手能抓的真凶。排障口诀:看到某核 si 100% 而其他核闲,先查 /proc/interrupts 的 IRQ affinity 和网卡的 RPS/XPS,别怪盘、别怪后端。 这也呼应第 05 章调度器——软中断在处理时是”谁收到谁处理”,不像普通任务能随便被迁移,所以必须靠 affinity/RPS 主动铺开。


实战补遗一:/sys/block 队列参数与 IO 调度器选错,把 NVMe 搞慢

墨:你换了 NVMe,4K 随机读反而比以前的企业 SSD 慢。你第一反应查固件、查盘健康,对吧?

你:不然呢?NVMe 理论上快十倍,慢了肯定是盘或驱动的问题。

墨:先别动固件。很多”换 NVMe 没变快”的锅,在 /sys/block/<dev>/queue/ 那几个队列参数和 IO 调度器 上——它们多为机械盘/HDD 设计的默认值,套到 NVMe 上反而加了无谓排队。这正好是第 8 章”块层”在 /sys 侧的延伸(不是 sysctl,是 /sys/block)。

现象:NVMe 的 4K 随机读比旧盘还慢

某服务把数据盘从 SATA SSD 换成 NVMe,fio 测 4K 随机读,QPS 反而降了 15%,iostat 看 NVMe 的 await 比预期高、%util 没满(说明不是盘满,是队列/调度在添乱)。

推断(可能推错):先查固件和驱动

第一推断:「NVMe 固件老 / 驱动没加载。」——dmesg 看 nvme 驱动正常,固件最新,推错。 第二推断:「是不是盘是假 NVMe(走 SATA 协议)?」——lsblk 确认是真实 nvme0n1,也不是。

真相:调度器 + nr_requests 给 NVMe 加了”机械盘税”

  • 调度器:cat /sys/block/nvme0n1/queue/scheduler 显示 [mq-deadline](被某调优脚本误设)。mq-deadline 为有寻道延迟的 HDD 设计,给 NVMe 这种”无机械头、并行度高”的盘加了一层不必要的请求排序/期限队列,引入额外延迟。NVMe 的正确选择是 none(或老名 noop)——让块层直接把请求丢给硬件队列,发挥并行度。
  • **nr_requests:**默认 128 合理;但有人为”吞吐”设成 1024,队列过深 → 请求在队列里等更久才下发,延迟升、吞吐没涨(尤其 4K 随机小 IO)。
  • **read_ahead_kb:**HDD 时代设大预读有益,NVMe 上随机读设太大反而预读无用数据、浪费带宽;随机负载应调小或按需。

调整:让 NVMe 走”无调度”快车道

  • echo none > /sys/block/nvme0n1/queue/scheduler(NVMe 本就无机械头,不需要调度);
  • nr_requests 保持默认 128(或小 IO 场景甚至更低),别盲目堆;
  • read_ahead_kb 按负载:随机读调小(如 4/8),顺序读可调大;
  • 持久化写 /etc/udev/rules.d/ 或 tuned 的 disk profile,别只 echo(重启丢,呼应第 0 章持久化坑);
  • 验证:改后 fio 重测,await 降、QPS 回预期。

再观察

改 none + nr_requests=128 + read_ahead_kb=8 后,4K 随机读 QPS 比旧 SSD 高 3 倍,await 从 120us 降到 40us。

改进(权衡:拿什么换什么)

动作拿什么换什么
NVMe 用 none 调度失去 HDD 式排序延迟降、并行度满
nr_requests 不堆队列浅小 IO 延迟稳
read_ahead_kb 调小预读少随机读带宽不浪费

🔧 思考题:为什么 HDD 需要调度器、NVMe 不需要?mq-deadline 在 NVMe 上到底加了什么”税”?

【参考答案】

HDD 有机械寻道,请求顺序安排(把相邻扇区合并、按期限防饿死)能大幅降寻道时间,所以调度器有收益。NVMe 是闪存、无寻道、内部多队列并行,请求本就该尽快交给硬件;mq-deadline 在 NVMe 上加的”税”是:① 一层软件请求排序/入队出队的延迟;② 期限队列可能让某些请求多等一个期限周期;③ 合并逻辑对随机小 IO 无收益反而延后下发。所以 NVMe 用 none 把块层变成”透传”,延迟最低。一句话:调度器解决的是”机械延迟”,NVMe 没有这个延迟,调度器就成了纯开销——这也再次验证第 8 章那句”瓶颈在哪一级,就在哪一级调,别在错的那级瞎忙”。


实战补遗二:/sys/block 的 wbt_lat(写回节流)把 NVMe 写吞吐限死

墨:你 NVMe 调好了调度器 none(第 20 章实战补遗),顺序写吞吐却还是上不去,iostat await 不高、aqu-sz 也不深。你开始怀疑盘。我说:去看 /sys/block/<dev>/queue/wbt_lat_usec。

你:写回节流?那不是 vm.dirty_* 的事吗,怎么又跑到 /sys/block 了?

墨:vm.dirty_* 管”应用写 → 多少变脏页”,而 wbt_lat(writeback throttling)管”块层把写下发到盘时,为了不让读延迟爆,主动节流写”。它在 /sys/block/<dev>/queue/,是块层又一扇门。NVMe 上它默认开着、且目标延迟设得偏保守,反而把顺序写吞吐压住。

现象:NVMe 顺序写吞吐腰斩,await/aqu-sz 都正常

某 NVMe 做顺序写基准,吞吐只有理论值一半。iostat await 低、aqu-sz 浅(说明不是排队、不是调度器),%util 也没满。诡异:盘”不忙”但写得慢。

推断(可能推错):先查调度器和队列深度

第一推断:「nr_requests / 调度器问题。」——已是 none、队列默认,且 aqu-sz 浅说明不是队列堵。推错。 第二推断:「文件系统 mount 选项(ext4 data=ordered 等)。」——mount 正常,不是。

真相:wbt_lat 在块层悄悄节流写

wbt_lat_usec(默认如 2000us on 某些内核)是块层 writeback throttling:当读延迟因写回而升高时,主动限制写的下发速率,保读延迟。设计初衷是”HDD/混部时写别饿死读”。但 NVMe 上:顺序写基准里没有读竞争,wbt 仍按它的延迟模型节流写 → 吞吐被压。而且 wbt 的节流在”下发”层,所以 aqu-sz 看不到堆积(它直接不放行),await 也不高(请求没排队只是被限流)——正好解释”盘不忙但写得慢”。

调整:关掉或放宽 wbt(按场景)

  • 纯写负载 / NVMe 无读竞争:echo 0 > /sys/block/nvme0n1/queue/wbt_lat_usec 关 wbt,吞吐回满;
  • 混部(读写共存)且重视读延迟:保留 wbt,但按读延迟目标调大 wbt_lat_usec(放宽写节流);
  • 监控:对比”开/关 wbt”的写吞吐与读延迟,取平衡点;
  • 持久化(第 0 章坑):写 udev rule 或 tuned,别只 echo。

再观察

关 wbt_lat_usec 后,顺序写吞吐从理论值 50% 回到 95%,await 仍低(无读竞争),读延迟在纯写场景无影响。

改进(权衡:拿什么换什么)

动作拿什么换什么
关 wbt(NVMe 纯写)读可能被写略挤写吞吐满
放宽 wbt_lat读延迟略升写不那么被压
保留 wbt(混部)写吞吐受限读延迟稳

🔧 思考题:wbt_lat 和 vm.dirty_* 都在”管写”,为什么不能合成一个?它们在哪两层?

【参考答案】

两层不同:vm.dirty_*(第 18 章)在页缓存层——管”应用写的页多久变脏、后台回写节奏”,影响的是”脏页在内存里待多久、fsync 卡不卡”;wbt_lat 在块层(下发到盘那一跳)——管”脏页已经要写回时,下发速率要不要为读延迟让路”。一个管”内存里的脏页”,一个管”下发到盘的节流”,职责不同不能合并:调 dirty_* 解决不了”块层下发时被 wbt 限流”(本节),调 wbt 也解决不了”脏页触顶前台 fsync”(第 18 章)。所以写路径上有两道独立的闸门,排障时按”卡在内存脏页 还是 卡在下发节流”分别看——这正是第 8/20 章”块层”视角和第 18 章”写回”视角的衔接:一个在页面,一个在队列。本书反复强调”瓶颈在哪一级就在哪级调”,写路径的瓶颈可能在页缓存层(dirty_*)也可能在块层队列(wbt),两扇门都要认得。


实战补遗三:read_ahead_kb 误配——顺序读变”小碎读”的毛刺

墨:块层有个 read_ahead_kb(在 /sys/block/<dev>/queue/read_ahead_kb),默认 128。它干啥的?为什么调错会让”顺序读”反而变慢?

你:预读吧?内核猜你要连续读,提前多读点进页缓存,减少后续 IO 次数?

墨:对。它告诉内核:“进程读了一个扇区,你顺手把后面 read_ahead_kb KB 也预读进来”。对顺序读(日志回放、大文件扫描、数据库全表扫)这是白嫖的加速。但对随机读(数据库点查、KV 读),预读是浪费——你读第 100 页,内核却把 101–132 页也读进来,而这些你根本不用,白白占带宽、挤掉真需要的页。

真实事故:一个数据分析服务,从对象存储拉大文件到本地盘做顺序解析,迁移到新机型后,解析速度掉了 40%。iostat 看 r/s(读 IOPS)很高、但 rkB/s(读吞吐)不算高——意味着读被拆成了很多小请求,而不是大块顺序读。

推断:顺序读被拆小,第一个怀疑 read_ahead_kb 太小,内核没预读,进程每读 4K 就发一次 IO 请求(小碎读)。验证:cat /sys/block/nvme0n1/queue/read_ahead_kb 发现新机型被某个”优化脚本”设成了 4(说是”减少预读浪费”)——它照搬了随机读场景的建议,却用在了顺序读服务上。

调整:把 read_ahead_kb 提到 512 或 1024(顺序读场景),并确认调度器是 none(NVMe 见上一节)。再观察:r/s 下降、rkB/s 上升(每次 IO 搬更多数据),解析速度回到迁移前水平。权衡表:

场景read_ahead_kb理由
顺序大文件读/ETL/备份512–2048预读命中高,一次搬大块,吞吐拉满
数据库随机点查0–16预读几乎全浪费,关掉省带宽
混合负载128(默认)折中,两边都别太亏

你:read_ahead_kb 和 vm.dirty_*(写侧)是不是一对?一个管读预读,一个管写回?

墨:没错,这是块层 I/O 路径上对称的两个旋钮:read_ahead_kb 管”读的时候多往前抱一点”(减少读 IOPS),dirty_*/wbt_lat_usec 管”写的时候攒一批再落 / 落的时候别挤读”(第 14、20 节讲)。一个在读路径前端、一个在写路径后端,共同决定这块盘的”I/O 性格”。调盘一定要读、写一起看:只调 read_ahead 不理 wbt,随机读会饿死在写的节流里;只压 dirty_* 不理预读,顺序读会变碎。

🔧 思考题:容器里你能 echo 1024 > /sys/block/nvme0n1/queue/read_ahead_kb 吗?这个参数是节点级还是容器级?

【参考答案】

这是**节点级(块设备级)**参数,挂在宿主的 /sys/block/<dev>/queue/ 下,容器里(除非特权容器 + 挂了宿主 sysfs)写不进去,而且即使写进去也是影响整块物理盘、所有容器共享。所以:

  • 容器里调 read_ahead_kb 是”找错门”——和”容器里调 zone_reclaim_mode”一样的坑(第 23 章讲过);
  • 正确做法:在宿主机/init 阶段按这块盘的主要负载类型设好,或交给 Kubernetes 的 Device Plugin / 节点初始化脚本统一配置;
  • 更细的”按容器区分 I/O 性格”,得靠 cgroup v2 的 io.max/io.weight(限带宽、调权重),而不是改块设备队列参数。

这也再次印证全书的主题:分清参数作用域(进程级/容器级/节点级/子系统级)比记住参数值更重要。把节点级参数当容器级去调,是最常见的”改了没生效还以为自己写错”的来源。


实战补遗四:SSD 写放大与 NVMe 多队列——“我只写了 1KB,凭什么盘忙成这样”

墨:前面讲了 read_ahead_kb(读侧)和 wbt_lat_usec(写回节流)。写侧还有个 SSD 特有的幽灵:写放大(Write Amplification)。你知道应用写 1KB,SSD 实际写了多少吗?

你:SSD 按页(通常 4K)写,1KB 至少写 4K 吧?放大 4 倍?

墨:比那复杂。SSD 不能”原地改”一页,只能整块(block,如 128 个 4K 页)擦除后重写。如果你改了某页里 1KB,SSD 控制器要把整个 block(512KB~几 MB)读出来、改这 1KB、再整块写回去——这一下写放大就可能 100~1000 倍。再加上 FTL(闪存转换层)的垃圾回收(GC)后台搬数据,放大更猛。

真实事故:一个 KV 存储,应用层写入量监控显示”写 50MB/s”,但 iostat 看盘 wkB/s 是 400MB/s——8 倍写放大。盘的写入寿命(smartctl 的 Percentage Used)掉得飞快,且写延迟偶发尖刺(GC 搬数据时卡)。

推断:8 倍放大来自”小随机写 + SSD 块擦除”。验证:iostat -x 看 w/s(写 IOPS)极高而 wkB/s 不算高(说明每次写很小、很碎),且 aqu-sz 偶发高(GC 搬数据时批量写堵);smartctl -a /dev/nvme0 看 Data Units Written 远大于应用层统计。根因:KV 的 value 小且随机更新,每次更新触发 SSD 块重写的放大链。

调整:① 应用层批量写 + 顺序化:把随机小写攒成批量顺序写(或改用 LSM-tree 类引擎如 RocksDB,它本就把随机写转顺序写,写放大从 100x 降到 ~10x);② 给 SSD 留 OP(Over-Provisioning)空间(少分区一点,让 FTL 有更多备用块做 GC,降低放大和 GC 卡顿);③ 确认 wbt_lat_usec 把”应用写”和”SSD 内部 GC 写”分层(第 20 节)。再观察:写放大从 8x 降到 2x,盘寿命消耗速度降 4 倍,写延迟尖刺消失。

你:NVMe 的”多队列”和写放大是两回事吧?它解决的是什么?

墨:对,别混。NVMe 多队列(nvme 的 mq + 每核队列)解决的是CPU 侧并发:老 SATA 盘一个队列深度 32,多核抢一把锁,NVMe 给每核一个队列(深度 64K+),并发提交 IO 不被锁瓶颈——这是”能不能喂饱盘”的问题。写放大是SSD 介质侧的物理特性,和队列无关。两者叠加的场景:队列调好了(并发高),但小随机写多(放大高),盘还是”忙且寿命掉”——所以要两套一起治:队列管”并发效率”、写放大管”写入量真实性”。

🔧 思考题:fstrim/discard(TRIM)和写放大有关系吗?为什么 SSD 用久了会变慢、TRIM 能缓解?

【参考答案】

有关系,但 TRIM 缓解的是”放大 + GC 负担”,不是消除放大。SSD 的页删了(文件删除/LVM 释放),操作系统认为这块空了,但 SSD 控制器不知道(它只听写入命令),仍把那些”逻辑删除但物理未擦”的页当有效数据,GC 时傻乎乎搬它们——这就是”用久变慢”的根因:无效页越积越多,GC 搬的无效数据越多,写放大和延迟越高。

fstrim(或挂载 discard)的作用是告诉 SSD 控制器”这些块真不要了”,控制器把它标记无效,GC 时就不用搬、直接擦除复用——降低 GC 负担、缓解变慢、减少无效放大。所以:

  • 新盘空块多,GC 轻松,快;
  • 旧盘不 TRIM,无效页堆积,GC 苦,慢;
  • 定期 fstrim(或 mount -o discard 实时)让控制器知情,维持盘速。

注意:TRIM 不减少”你这次写”的放大(那靠批量/顺序写解决),它减少的是”长期运行中无效数据造成的 GC 放大”。两者互补:应用层减少源头放大 + TRIM 减少历史垃圾放大 = SSD 长期高性能。又是”参数/机制各管一层”的呼应。



实战补遗五:fstrim 与 wbt_lat——SSD 回收和写回节流两道不同的阀门

墨:老哥,你知道 SSD 用久了会”越用越慢”吗?还有,你听过 wbt_lat_usec 吗?

你:SSD 慢是磨损吧?wbt_lat 是块层的啥?

墨:两件事,且都跟”写”有关但层次不同。这节把 SSD 的 TRIM 回收、和块层 wbt_lat(写回限流)讲清楚——后者常被和 vm.dirty_* 搞混,其实是不同层的两道阀门。

现象:一台跑了两年的 SSD 机器,顺序写吞吐从新盘 500M/s 掉到 150M/s,且写延迟毛刺变多。另外,某次调 vm.dirty_ratio 后,发现块层还有个独立的”写节流”在起作用,以为是 dirty 参数没生效,其实不是一回事。

推断:SSD 慢是磨损/寿命到了。

可能推错:第一,SSD 变慢多半不是”磨损”而是 WRITE AMPLIFICATION(写放大)+ 未回收的无效块。SSD 不能像 HDD 那样直接覆盖,要先”擦除整块(block,通常几 MB)再写”。你删了文件,SSD 控制器并不知道哪些页无效,直到你发 TRIM(DISCARD) 命令告诉它”这些 LBA 废了可回收”。没 TRIM,控制器以为所有页都有效,GC 时要把有效页搬来搬去,写放大飙升、写慢。第二,wbt_lat_usec 是块层(blk-throttle 的写回限流 wbt),控制”写请求在块层排队等待的延迟上限”,跟 vm.dirty_*(页缓存层脏页阈值)是上下游两道独立阀门。

调整:

  • TRIM:fstrim -v /mount/point 手动发 TRIM,或挂盘时 mount -o discard(实时 TRIM,但每次删都发,略增开销),或定时 fstrim.timer(systemd 周级 TRIM,推荐)。看 cat /sys/block/nvme0n1/queue/discard_granularity 确认支持。TRIM 后 SSD GC 轻松,写放大降,吞吐回升。
  • wbt_lat_usec:cat /sys/block/nvme0n1/queue/wbt_lat_usec,默认 NVMe 是 0(关闭,因为 NVMe 快);SSD/SATA 有值(如 2000 微秒)。它限制”同步写(如 O_DIRECT、fsync)在块层排队的延迟”,超过就节流发源(让上层的脏页回写慢下来),避免把慢盘写爆导致前端卡。这是块设备层的阀门,vm.dirty_* 是页缓存层的阀门,二者独立但串起来:dirty 页满了→内核回写→回写请求到块层→块层 wbt 再限流。

再观察:那次”调 dirty 没生效”的误会,根因是块层 wbt 在 dirty 之下又限了一道,脏页回写被块层节流,表现为”写延迟还在”。两道阀门要一起看:vm.dirty_background_ratio(开始后台回写)+ vm.dirty_ratio(前台同步等)+ wbt_lat_usec(块层回写排队延迟上限)。NVMe 上 wbt_lat_usec=0(不限制)是对的,因为 NVMe 延迟本就低;给 NVMe 设 wbt 反而自废武功。

改进:

  • SSD 必开 TRIM(定时 fstrim.timer),恢复写性能、降写放大。
  • NVMe 用 none 调度器 + wbt_lat_usec=0(关限流,NVMe 够快不需)。
  • SATA SSD 保留默认 wbt,别瞎关(关了慢盘会被回写冲爆)。
  • 排查写延迟:分清是 vm.dirty_*(页缓存层卡)还是 wbt_lat_usec(块层卡)还是盘真慢(fio 测),三层各看各的指标。

🔧 思考题:为什么 NVMe 上建议 wbt_lat_usec=0 关掉写回限流,而 SATA SSD 反而要留着?TRIM 和”删文件立刻空间释放(df 看到)“是一回事吗?

【参考答案】

NVMe vs SATA 的差异在延迟量级:NVMe 走 PCIe,随机写延迟亚毫秒、队列深,块层没必要再限流(wbt 限流是针对”慢盘会被回写冲爆”设计的保险,NVMe 冲不爆),关掉反而去掉一层无谓的节流、吞吐更高。SATA SSD 延迟高一个量级、且老控制器易在回写洪峰下写放大失控,留 wbt 当”块层刹车”反而稳。所以”关不关 wbt”取决于盘够不够快,不是一刀切。

TRIM 与 df 释放空间不是一回事:

  • df 显示的”已用/可用”是文件系统层的逻辑空间——你删文件,文件系统立刻把对应的 inode/块标记为空闲,df 马上就看到空间回来了。这一步跟 SSD 物理层无关。
  • TRIM 是告诉 SSD 控制器”这些 LBA 对应的物理页现在无效了,你可以 GC 回收”。你不发 TRIM,文件系统逻辑上空间已释放(df 已回来),但 SSD 控制器不知道,仍把这些页当”有效”保留,GC 时被迫搬移,写放大高、写慢。TRIM 解决的是物理层的写放大,不是逻辑层的空间可见性。

一句话:df 早回来 ≠ SSD 真回收;要 SSD 写性能不退化,得定期 TRIM 把”逻辑已删、物理还占着”的页真正交还给 GC。mount -o discard 实时发但增开销,fstrim.timer 周期发是平衡之选(对写密集且删频繁的工作负载尤其重要,如容器层 overlay、日志轮转后)。

补充:wbt 全称 writeback throttle,是 blk-mq 时代的机制(替代老的 throttle cgroup),作用于写回(writeback)路径,不是对所有写;读路径和 O_DIRECT 不经 wbt。它和 io.max(cgroup v2 的 IO 限速)也不同层——wbt 是设备级自适应,io.max 是 cgroup 级硬配。排查写毛刺时这三层(vm.dirty / wbt / io.max)要分开看。

下一章预告

墨:第 20 章我们拆了磁盘 I/O 毛刺,你记住了 %util 低不等于没瓶颈,aqu-sz+await 才是排队真相;还见识了”换 NVMe 没变快”的老梗——很多盘慢锅其实是缓存没盖住工作集。

下一章(第 21 章)我们杀回网络,专治两类最磨人的网络故障:(a) 高并发短连接下 TIME_WAIT 堆积把端口耗尽、新连接建不上;(b) TCP 重传率高、RT 莫名涨,调了一周 tcp_* 没找到根。这两类都会用到 tcp_tw_reuse/tcp_tw_recycle(后者已删的坑)、net.ipv4.ip_local_port_range、tcp_retries2、tcp_mtu_probing 等参数,我们照样走完整迭代环,把”为什么是三次握手""TIME_WAIT 为什么不能随便删”这些设计意图讲透。


本章完。网络重传与 TCP 调优见第 21 章。