<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>IO · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/io/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/io/index.xml" rel="self" type="application/rss+xml"/><item><title>iotop 能看出是哪个进程在烧磁盘</title><link>https://www.xbcatm.com/zh/posts/linux-iotop-io-waiter/</link><pubDate>Wed, 15 Mar 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/linux-iotop-io-waiter/</guid><description>iotop 通过读取内核统计观察每个进程的 IO，于是能把忙碌的磁盘怪到具体某个 pid 头上。</description><content:encoded>&lt;p&gt;磁盘突然飙升却找不到明显的写入者时，iotop 把吞吐直接指回那个在捣乱的进程。系统级工具只给症状——await 高、util 打满——不给嫌疑人：五十个进程共享磁盘，平均值平等地怪每一个。终结调查的永远是按进程的问题——谁、多少、何时开始——iotop 从内核计数器实时回答，应用藏都没处藏。&lt;/p&gt;
&lt;p&gt;把它放在系统级视图旁边，而不是代替。iostat 或 vmstat 说磁盘是瓶颈，iotop 点名是谁造成的。两者合起来把存储悬案压成两条命令：确认资源，锁定消费者。&lt;/p&gt;
&lt;h2 id="逐个进程的计数器"&gt;逐个进程的计数器&lt;/h2&gt;
&lt;p&gt;iotop 从内核读每个进程的 IO，于是实时读写速率能对应到一个 pid，而不是一个说不清来源的负载均值。加 &lt;code&gt;-o&lt;/code&gt; 只显示真正在做 IO 的进程，一眼看清。默认视图读写分开，还带 swap-in 百分比，把真吃磁盘的和仅仅在场的分开——DISK READ 高、写零的是在扫，写高的在产； throttle、迁移还是杀，取决于这个区分。&lt;/p&gt;
&lt;p&gt;注意计数器量的到底是什么：穿过 IO 层的按进程字节，含还没落盘的缓冲写。进程写速率巨大、磁盘看着却闲（页缓存吸收），或速率平平、磁盘打满（大量小同步写），都可能。iotop 跟设备 util 一起读：进程侧定罪加设备侧饱和才是完整画面，任一半单独都误导。&lt;/p&gt;
&lt;h2 id="看到谁在扫盘"&gt;看到谁在扫盘&lt;/h2&gt;
&lt;p&gt;按带宽排序，那个罪魁就现形了，无论是日志囤积、索引器还是失控备份。跟住这个 pid，就能定位到确切的进程（往往还有文件），再动手不迟。惯犯名单：debug 级别没轮转的日志、排在高峰的重建索引、同盘读写备份、坏镜像上循环解包的容器层。修法各不同——轮转、改期、分开、清理——杀进程治标，病因明天定时再来。&lt;/p&gt;
&lt;p&gt;动手前把 pid 解析到完整上下文：命令行、工作目录、lsof 看开文件、父进程。一个烧写的 java 进程背后可能是一个 PID 命名空间里的十个应用；修要修对那一个。先定罪后动手——每个以遗憾收场的存储 war story 都跳过了第一步。&lt;/p&gt;
&lt;h2 id="先看再动"&gt;先看再动&lt;/h2&gt;
&lt;p&gt;iotop 只观察、不改变任何东西，所以你可以在决定节制、迁移或停掉之前先把元凶查清楚。再配 strace 或 lsof 打到这个 pid，把文件和系统调用一并钉死。组合是决定性的：iotop 说 18412 进程写 400MB/s，lsof 说写进一个膨胀的日志文件，strace 显示 4KB 的 O_APPEND 写——诊断完毕；修法（日志级别、轮转、异步 appender）不言自明，不用开会争。&lt;/p&gt;
&lt;p&gt;健康时录基线，故障时对比才有意义。平时某天 &lt;code&gt;iotop -b -n 60&lt;/code&gt; 落文件五分钟，就是这台机器的&amp;quot;正常&amp;quot;数字：平时谁领跑、多少速率算寻常。没有它，每次 spike 都像史无前例；有了它，多数 incident 几秒归类：&amp;ldquo;又是 indexer，提前一小时&amp;rdquo;。
把 iotop 批输出和系统监控同一时段的曲线放一起看，进程侧和设备侧互相印证，结论才站得住。单看任何一边，都可能把相关当因果。
告警阈值按基线设，而不是拍脑袋，基线之外的 spike 才值得半夜起床。
把 Top 写入者的变化趋势也画出来，渐变比突变更早暴露问题。&lt;/p&gt;
&lt;p&gt;基线文件要标清楚采集条件：采集时段、业务负载水位、iotop 的采样间隔与累计方式，否则下次对比就是拿苹果比橙子。建议每周固定一个低峰窗口复采一次，覆盖掉业务自然增长带来的漂移；复采后把新旧两份排名并排 diff，只看名次变化超过三位的进程，逐个确认是业务上线还是异常冒头，再决定跟进还是放行。确认结论也要回写基线备注。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;最经典的元故障是权限盲：没特权跑，iotop 进程细节全问号，别用户的进程直接不显示——而真凶恰跑在别用户下，隐形到底。一开始就带够特权（root 或对应 capabilities），不然工具开脱的恰是你看不见的进程。无特权 iotop &amp;ldquo;什么都没查到&amp;rdquo;，只代表你自己的进程没事。&lt;/p&gt;
&lt;p&gt;第二种是突发 IO 的采样幻觉：一分钟暴力一秒的进程，5 秒平均看着无辜，1 秒平均看着有罪，反之亦然。数字和症状对不上就换 delay 档；&amp;ldquo;过去一小时谁伤的盘&amp;quot;问累计总量（&lt;code&gt;-a&lt;/code&gt;）不问瞬时速率。聚合方式对，问题才对；默认聚合只答它自己假设的那个问题。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;非交互地抓谁在做 IO，带上下文：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;iotop -b -n 5 -o -d 2 | head -n 30
pid=$(iotop -b -n 1 -o | awk &amp;#39;NR&amp;gt;5 {print $1}&amp;#39; | head -n 1); ls -l /proc/$pid/exe; cat /proc/$pid/cmdline | tr &amp;#39;\0&amp;#39; &amp;#39; &amp;#39;
iostat -x 1 3 | grep -E &amp;#34;Device|sd|nvme&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;batch 抓排名不占终端，pid 解析点名头号写者的二进制，iostat 确认设备侧真打满。&lt;/p&gt;
&lt;h2 id="延伸阅读"&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/notes/certificate-expiry-is-a-date-not-a-dashboard-color/"&gt;证书过期是一个日期，而不是仪表盘上的颜色&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/notes/head-should-describe-the-same-representation-as-get/"&gt;HEAD 应描述与 GET 相同的表示&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/iotop.8.html"&gt;iotop 手册&lt;/a&gt;，iotop 官方参考，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/vmstat.8.html"&gt;vmstat 手册&lt;/a&gt;，说明系统级 CPU 与 IO 等待报告，已核验 2026-09-05。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>磁盘调度器给 I/O 排好队，让磁头移动更省</title><link>https://www.xbcatm.com/zh/posts/disk-scheduling-elevators-serve-fairly/</link><pubDate>Tue, 09 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/disk-scheduling-elevators-serve-fairly/</guid><description>磁盘调度器会对请求重新排序与合并，让读写磁头移动得更有章法，用一点点延迟换取机械盘上更大的吞吐。</description><content:encoded>&lt;p&gt;blk-mq 给块层准备了两级队列：每个 CPU 一个软件暂存队列，每条设备队列一个硬件派发队列，而 I/O 调度器就夹在两者之间。一个请求会走最短路径——直接进硬件队列——除非该设备挂了调度器，或者这个请求还能被合并；这两种情况下它要先在软件队列里停留。调度器是整个栈里唯一由内核决定“请求以什么顺序到达设备”的地方。&lt;/p&gt;
&lt;p&gt;排序在机械盘上价值极大，在闪存上几乎为零，但调度器还有第二件活，而且这件事在 NVMe 时代依然存在：决定谁先等、谁后等。这两件事的方向并不一致——为磁头效率而重排根本不关心公平，而公平本身要付出寻道的代价——所以当前内核里的四种调度器不是拿来排名的，而是拿来匹配设备与负载的。&lt;/p&gt;
&lt;h2 id="磁头才是预算"&gt;磁头才是预算&lt;/h2&gt;
&lt;p&gt;在 7200 RPM 的盘上，平均每个请求的旋转开销是 4.17 ms，传输一个 4 KiB 块只要 0.026 ms，所以几乎全部服务时间都是寻道加旋转。fixture 重放了 2000 个随机 4 KiB 读、覆盖 100000 条磁道：按到达顺序派发时磁头走过 67907411 条磁道、整轮耗时 21954 ms，约 91 IOPS；按磁道升序派发时只走 99977 条、耗时 8645 ms，约 231 IOPS。同样的请求、同样的设备，磁头行程少了 679 倍，吞吐涨了 2.5 倍。&lt;/p&gt;
&lt;p&gt;拆开看才说明问题：寻道在到达顺序那一轮占服务时间的 62%，在排序那一轮只占 3%，而旋转与传输两边都固定在 4.19 ms 每个请求。NVMe 上同一批请求总共只要约 40 ms，且完全没有寻道，所以排序在那里一分钱也买不到。&lt;/p&gt;
&lt;h2 id="每种调度器是干什么的"&gt;每种调度器是干什么的&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;none&lt;/code&gt; 就是先到先服务：在内部队列很深且没有竞争的设备上它是对的，在机械盘上是错的，因为寻道那部分开销原封不动。&lt;code&gt;mq-deadline&lt;/code&gt; 按扇区排序，并加上分方向的超时——读的超时远早于写，每批的数量由 &lt;code&gt;fifo_batch&lt;/code&gt; 限住——于是 &lt;code&gt;none&lt;/code&gt; 那种无上界的尾部延迟被收进了边界。&lt;code&gt;kyber&lt;/code&gt; 直接盯延迟：哪个队列的请求没达到读或写的目标延迟（&lt;code&gt;read_lat_nsec&lt;/code&gt;、&lt;code&gt;write_lat_nsec&lt;/code&gt;），它就把那个队列限流，所以一个批量写入方单靠自己无法把交互读推过目标线。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bfq&lt;/code&gt; 是公平那一侧的答案：一个支持 cgroup 的比例分配调度器，按权重把设备吞吐——而不只是设备时间——分给进程和分组。它默认偏向低延迟而非高吞吐，并且会让设备空转，这在机械盘上有帮助，在深队列闪存上则可能赔掉吞吐。用 &lt;code&gt;cat /sys/block/&amp;lt;dev&amp;gt;/queue/scheduler&lt;/code&gt; 看当前调度器，列表里带方括号的就是生效的那一个；用 &lt;code&gt;echo bfq &amp;gt; /sys/block/sda/queue/scheduler&lt;/code&gt; 切换，并靠 udev 规则让它在重启后保持，因为队列属性是每块设备重新创建的。&lt;/p&gt;
&lt;h2 id="公平与排序是两个问题"&gt;公平与排序是两个问题&lt;/h2&gt;
&lt;p&gt;当批量写入方与轻量交互客户端按 90/10 的比例发请求时，先到先服务给轻客户端分到 0.099 的服务量，平均排队位置在 1901 个请求之后——它不是被饿死，而是被淹死。换成分组公平排队，无论混合比例如何它都拿到 0.5 的服务量，代价由那个写入方承担。磁头位置一点没变，变的只是仲裁。&lt;/p&gt;
&lt;p&gt;所以在闪存上通常选 &lt;code&gt;bfq&lt;/code&gt; 做隔离、选 &lt;code&gt;mq-deadline&lt;/code&gt; 要尾部有界，只有单一负载独占设备时才用 &lt;code&gt;none&lt;/code&gt;。在机械盘上，排序本身才是更大的收益。用 cgroup v2 的说法，&lt;code&gt;io.weight&lt;/code&gt; 给一个组一个份额，&lt;code&gt;io.max&lt;/code&gt; 给它一个上限；这两者只有在下面的调度器愿意执行顺序时才有意义。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;最经典的是机械盘跑在 &lt;code&gt;[none]&lt;/code&gt; 上，通常因为主机是从 NVMe 镜像克隆来的，或者 udev 规则匹配了 &lt;code&gt;sd*&lt;/code&gt; 而设备其实是 &lt;code&gt;nvme0n1&lt;/code&gt;。特征是 &lt;code&gt;%util&lt;/code&gt; 顶在 100% 附近、&lt;code&gt;r/s&lt;/code&gt; 很低、&lt;code&gt;await&lt;/code&gt; 到几十毫秒——设备是忙，不是饱和，而且忙在寻道上。用 &lt;code&gt;cat /sys/block/&amp;lt;dev&amp;gt;/queue/rotational&lt;/code&gt; 确认：值为 &lt;code&gt;1&lt;/code&gt; 却没有重排调度器，就是那个 bug。&lt;/p&gt;
&lt;p&gt;第二种是平均值健康、延迟却崩了。同一个设备上的备份任务或 &lt;code&gt;dd&lt;/code&gt; 把交互读饿住，p99 爆掉而均值几乎不动。&lt;code&gt;none&lt;/code&gt; 和 &lt;code&gt;mq-deadline&lt;/code&gt; 都护不住交互客户端，&lt;code&gt;bfq&lt;/code&gt; 或 cgroup 的 &lt;code&gt;io.max&lt;/code&gt; 可以。在闪存上还要盯 &lt;code&gt;slice_idle&lt;/code&gt;：在没有磁头可保的设备上，为了让单个进程保持顺序而空转，纯粹是白赔吞吐。另外，&lt;code&gt;rotational&lt;/code&gt; 为 1 的盘如果只跑 &lt;code&gt;none&lt;/code&gt;，&lt;code&gt;iostat -x&lt;/code&gt; 里的 &lt;code&gt;aqu-sz&lt;/code&gt;（平均队列深度）也会明显偏高，因为请求在队列里等磁头，而不是被合并成一次扫描。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;看清设备是什么，以及它正跑着哪个调度器：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;for d in /sys/block/sd* /sys/block/nvme*n*; do echo &amp;#34;$d rot=$(cat $d/queue/rotational) sched=$(cat $d/queue/scheduler)&amp;#34;; done
cat /sys/block/sda/queue/iosched/read_expire /sys/block/sda/queue/iosched/fifo_batch
iostat -x 2 5
echo mq-deadline &amp;gt; /sys/block/sda/queue/scheduler
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第一条把每块设备的旋转标志与当前调度器配对打印，方括号里是生效的那个；&lt;code&gt;rot=1&lt;/code&gt; 配 &lt;code&gt;[none]&lt;/code&gt; 就是失败特征，&lt;code&gt;rot=0&lt;/code&gt; 配 &lt;code&gt;[none]&lt;/code&gt; 则正常。第二条给出 &lt;code&gt;mq-deadline&lt;/code&gt; 的超时与批量参数。&lt;code&gt;iostat -x&lt;/code&gt; 用来区分“饱和”和“只是忙”：&lt;code&gt;%util&lt;/code&gt; 高、&lt;code&gt;r/s&lt;/code&gt; 低、&lt;code&gt;await&lt;/code&gt; 高，说明在排队和寻道，而不是设备到了极限。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/block/switching-sched.html"&gt;Switching Scheduler&lt;/a&gt;，调度器名称、sysfs 路径与切换方法，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/block/blk-mq.html"&gt;Multi-Queue Block IO Queueing Mechanism (blk-mq)&lt;/a&gt;，软件暂存队列与硬件派发队列的关系，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/block/bfq-iosched.html"&gt;BFQ (Budget Fair Queueing)&lt;/a&gt;，比例分配行为、cgroup 支持与空转的取舍，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/block/kyber-iosched.html"&gt;Kyber I/O scheduler tunables&lt;/a&gt;，Kyber 据以限流的读写目标延迟，已核验 2026-09-16。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>