磁盘调度算法:从机械硬盘原理到Linux I/O性能优化实战

📅2026/7/13 2:07:10 👁️次浏览
磁盘调度算法:从机械硬盘原理到Linux I/O性能优化实战
那天下午团队里一位刚接触后端开发不久的同事跑过来指着监控面板上一段持续波高的磁盘 I/O 等待时间曲线问我“这服务器是不是硬盘坏了怎么查询一会儿快一会儿慢的完全没规律。”我让他把同一时段数据库的慢查询日志拉出来对比发现一个有趣的现象很多查询本身并不复杂但它们要读取的数据块在物理磁盘上离得特别远。这就像一本厚厚的书你要查的资料偏偏分散在第 1 页、第 300 页和第 50 页光来回翻书的时间就远超过阅读时间本身。这个问题背后其实就是操作系统里一个经典且至关重要的机制——磁盘调度算法。它不像内存管理或者进程调度那样常被提起却直接决定了数据存取的效率尤其是在高并发 I/O 的场景下。今天我们就从一次真实的性能问题出发把磁盘调度算法到底在解决什么问题、为什么需要它、以及不同算法各自的适用场景彻底讲清楚。1. 先理解机械硬盘的“物理局限”为什么是性能瓶颈的关键要弄明白磁盘调度算法为什么存在首先得回到机械硬盘HDD的工作方式上。虽然现在 SSD 越来越普及但在大量温冷数据存储、企业级归档等场景机械盘凭借其成本优势依然占据重要地位。而机械硬盘的物理结构决定了它有一种“与生俱来”的延迟。1.1 机械硬盘的三大耗时操作当应用程序发起一个读取磁盘数据的请求时硬盘需要完成三个机械动作寻道时间Seek Time磁头移动到数据所在的磁道柱面上。这是最耗时的部分磁头是机械臂需要横向移动。旋转延迟Rotational Latency盘片旋转将目标扇区转到磁头下方。这取决于硬盘转速比如 7200 RPM 的硬盘旋转一圈大约 8.3ms平均延迟约 4.15ms。传输时间Transfer Time实际读取或写入数据的时间通常最快与数据量大小有关。如果把一次磁盘 I/O 请求的总时间公式简化一下大概是总时间 寻道时间 旋转延迟 传输时间其中寻道时间是最大的变量也是磁盘调度算法主要优化的对象。顺序读写时磁头几乎不需要大范围移动而随机读写时磁头就像唱机的指针在不同磁道间来回跳跃大量时间浪费在了路上。1.2 “先来先服务”的朴素策略为什么在磁盘 I/O 上行不通最直观的调度策略就是 FIFOFirst-In-First-Out也就是先来的 I/O 请求先被处理。这很公平但效率可能非常低。举个例子假设当前磁头在 50 号磁道接下来收到的请求序列是100, 10, 80, 30, 120如果按 FIFO 顺序服务 磁头移动路径是50 → 100 → 10 → 80 → 30 → 120 总寻道距离 |50-100| |100-10| |10-80| |80-30| |30-120| 50 90 70 50 90 350单位磁道可以想象磁头就像没头苍蝇一样在盘片上来回“折返跑”。这种服务策略只考虑了公平性完全没有考虑磁头的当前位置和移动效率。在高负载的磁盘 I/O 场景下这会导致平均响应时间急剧上升吞吐量下降。因此操作系统需要更智能的算法来重新排列这些 I/O 请求的服務顺序以减少不必要的磁头移动。2. 四大经典磁盘调度算法的工作原理与适用场景为了解决 FIFO 的效率问题人们设计了几种经典的磁盘调度算法。它们核心思想相似对等待队列中的 I/O 请求进行重排序让磁头以更“经济”的路径移动。但各自的策略和侧重不同。2.1 SSTF最短寻道时间优先看似高效实则可能“饿死”远端请求SSTF 算法非常直观总是选择离当前磁头位置最近的请求先服务。还是上面的例子磁头在 50请求序列100, 10, 80, 30, 120SSTF 的调度过程会是当前在 50最近的是 80距离 30吗不是 30距离 20吗不最近的是 10距离 40吗不对仔细算离 50 最近的是 80距离 30还是 30距离 20我们重新排序计算所有请求与 50 的距离100(50), 10(40), 80(30), 30(20), 120(70)。最近的是 30距离 20。服务 30移动 20。当前在 30剩余请求 [100, 10, 80, 120] 与 30 的距离100(70), 10(20), 80(50), 120(90)。最近的是 10距离 20。服务 10移动 20。当前在 10剩余 [100, 80, 120] 与 10 的距离100(90), 80(70), 120(110)。最近的是 80距离 70。服务 80移动 70。当前在 80剩余 [100, 120] 与 80 的距离100(20), 120(40)。最近的是 100距离 20。服务 100移动 20。最后服务 120移动 20。总寻道距离 20 20 70 20 20 150。相比 FIFO 的 350效率提升巨大SSTF 的优缺点非常鲜明优点平均寻道时间短吞吐量相比 FIFO 有显著提升。缺点可能导致“饥饿”Starvation。如果不断有新的请求到达磁头当前位置附近那么那些位于磁盘两端的请求可能永远得不到服务。在高负载下这是一个严重问题。2.2 SCAN电梯算法公平性与效率的折中SCAN 算法模仿电梯的工作方式磁头在一个方向上如从低磁道号向高磁道号移动服务所有沿途的请求直到到达磁盘的一端最内圈或最外圈然后掉头向反方向移动继续服务沿途请求。假设磁头初始在 50向磁道号增大方向移动。请求序列100, 10, 80, 30, 120SCAN 调度过程从 50 出发向高磁道号移动。沿途服务大于等于 50 的请求先服务 80再服务 100最后服务 120到达磁盘末端。掉头向低磁道号移动。沿途服务小于 120 的请求服务 30再服务 10到达磁盘另一端。移动路径50 → 80 → 100 → 120 → 30 → 10 总寻道距离 30 20 20 90 20 180。SCAN 算法的特点优点解决了 SSTF 的饥饿问题对所有位置的请求都相对公平因为每个请求最多等待磁头一个完整的来回扫描。缺点对最近扫描过的区域不公平。假设磁头刚从低磁道号扫到高磁道号一个新请求到达低磁道号它必须等待磁头走到最高端再掉头回来等待时间可能较长。2.3 CISCAN循环扫描算法进一步优化公平性CISCAN 是 SCAN 的改进版旨在提供更一致的等待时间。它与 SCAN 的区别在于当磁头到达磁盘一端时立即返回到另一端的开头而不是中途就开始服务请求。可以理解为磁头只在单向移动时服务请求返程是“空跑”快速回到起点。同样例子磁头在 50向磁道号增大方向移动。请求序列100, 10, 80, 30, 120CISCAN 调度过程从 50 出发向高磁道号移动。沿途服务大于等于 50 的请求80, 100, 120到达末端。立即跳回到磁盘开始端比如磁道 0但返程不服务任何请求。从 0 开始再次向高磁道号移动。沿途服务请求10, 30。移动路径50 → 80 → 100 → 120 → 0 → 10 → 30 总寻道距离 30 20 20 120 10 20 220。虽然寻道距离可能变长但公平性更好。CISCAN 的特点优点等待时间分布比 SCAN 更均匀消除了 SCAN 中对两端请求的响应时间差异过大的问题。缺点返程空跑可能略微增加不必要的磁头移动。2.4 LOOK 与 C-LOOK更实用的“智能电梯”算法LOOK 和 C-LOOK 是 SCAN 和 CISCAN 的优化版本。它们不会机械地走到磁盘的物理端点而是在移动方向上如果没有更远的请求就提前掉头。LOOK磁头在一个方向上移动服务沿途请求。当该方向没有等待的请求时立即掉头。C-LOOK磁头在一个方向上移动服务沿途请求。当该方向没有等待的请求时立即跳转到另一端有请求的最小磁道号开始继续单向扫描。在我们的例子中磁头在 50请求100, 10, 80, 30, 120LOOK向增大方向从50出发服务80, 100, 120。此时增大方向无请求掉头。向减小方向移动服务30, 10。路径50-80-100-120-30-10距离同SCAN为180但避免了走到物理端点。C-LOOK向增大方向从50出发服务80, 100, 120。增大方向无请求直接跳到最小请求磁道10的位置然后向增大方向服务10, 30。路径50-80-100-120-10-30距离30202011020200。公平性类似于CISCAN但减少了空跑距离。LOOK/C-LOOK 是实际系统中常用的算法因为它们避免了不必要的磁盘两端移动在效率和公平性之间取得了很好的平衡。3. 算法对比与选型如何根据场景做判断了解了原理我们该如何选择这取决于你的应用对延迟和公平性的要求。算法优点缺点适用场景FIFO简单绝对公平寻道性能差吞吐量低几乎不用于现代磁盘I/O调度SSTF平均响应时间短吞吐量高可能饥饿响应时间变化大方差大负载较轻且I/O请求分布均匀的系统SCAN无饥饿吞吐量较好两端请求的响应时间差异大需要兼顾吞吐量和一定公平性的通用系统C-SCAN响应时间更均匀公平性好返程空跑吞吐量略低于SCAN对请求响应时间一致性要求高的场景如多媒体流LOOK/C-LOOK综合性能最好兼顾效率与公平实现稍复杂现代操作系统的默认或常用选择如Linux的CFQ、Deadline调度器借鉴其思想注意上述讨论基于机械硬盘HDD。对于固态硬盘SSD由于其没有机械部件数据访问时间几乎与存储位置无关因此这些基于寻道优化的磁盘调度算法对SSD性能提升微乎其微。SSD的I/O调度更关注于并发请求处理、垃圾回收、磨损均衡等。4. 从理论到实践在Linux中查看和设置I/O调度器理论最终要服务于实践。在 Linux 系统中磁盘调度算法是以 I/O Scheduler 的形式存在的。你可以方便地查看和修改它。4.1 查看当前系统的I/O调度器对于某个磁盘例如 sda可以这样查看# 查看 /sys/block/sda/queue/scheduler cat /sys/block/sda/queue/scheduler输出可能类似[mq-deadline] kyber bfq none方括号[ ]括起来的即为当前正在使用的调度器。mq-deadline 为每个请求设置截止时间防止饥饿适合多种负载。bfqBudget Fair Queueing 试图为进程提供公平的带宽分配适合桌面交互式应用。kyber 针对快速设备如 SSD的轻量级调度器主要优化延迟。none 不使用任何调度算法直接下发给设备常用于SSD。4.2 如何临时更改I/O调度器# 将 sda 的调度器改为 bfq echo bfq | sudo tee /sys/block/sda/queue/scheduler这种更改在重启后失效。4.3 如何永久更改I/O调度器可以通过内核命令行参数或 udev 规则来设置。例如使用 udev 规则创建文件/etc/udev/rules.d/60-ioscheduler.rules加入以下内容# 对 SSD以 nvme 开头和 sdX 且旋转速率0 的磁盘设置 none 或 kyber 调度器 ACTIONadd|change, KERNELsd[a-z]*, ATTR{queue/rotational}0, ATTR{queue/scheduler}kyber # 对 HDD旋转速率1设置 mq-deadline 或 bfq 调度器 ACTIONadd|change, KERNELsd[a-z]*, ATTR{queue/rotational}1, ATTR{queue/scheduler}mq-deadline然后重新加载 udev 规则sudo udevadm control --reload-rules sudo udevadm trigger5. 超越单机分布式系统中的“磁盘调度”思想磁盘调度算法的思想——对无序请求进行重排序以优化整体效率——并不仅限于单机操作系统。在分布式系统、数据库、网络传输等领域你都能看到类似的思想。数据库的写操作优化许多数据库的 WALWrite-Ahead Logging会合并多次写操作顺序写入日志文件这类似于 SCAN 算法对随机写进行顺序化。分布式存储系统的数据布局系统会有意地将可能被同时访问的数据如一个文件的多个块放置在相近的物理位置同一机架、同一存储节点这本质上是在数据存储阶段就避免了“长距离寻道”。CDN 的内容分发将热门内容缓存到离用户最近的边缘节点减少数据传输的“寻道时间”网络延迟。理解了磁盘调度算法的核心逻辑就能以一种更抽象的视角去看待很多系统的性能优化问题如何通过合理的排序和布局将随机的、低效的访问模式转变为顺序的、高效的访问模式。回到开头那个服务器 I/O 等待高的问题。最终的解决方案并不是去调整操作系统的磁盘调度算法它可能已经是较优的 LOOK/C-LOOK 变种而是通过优化数据库的索引设计避免产生大量跨磁道的随机读请求并从应用层面对查询进行合并让磁盘的“磁头”能够更连续地工作。这正说明了真正的高手不仅要理解工具调度算法本身更要理解问题I/O 模式的根源从而在更早的环节做出更优的设计。