一、引言
块 I/O 调度长期以来都是操作系统内核面临的重要问题。在传统机械硬盘(HDD)时代,磁头寻道和旋转延迟对性能有显著影响,因此内核需要投入大量精力对 I/O 请求进行排序,以减少寻道开销并提高吞吐量。
随着固态硬盘(SSD)和 NVMe 设备的普及,存储设备的性能发生了巨大变化。现代 NVMe 设备能够并行处理大量 I/O 请求,每秒完成数百万次 I/O 操作。在这种环境下,I/O 调度面临着新的挑战:除了提高吞吐量,还需要在多个竞争的进程、容器和控制组之间公平地分配设备资源,同时避免调度机制本身成为性能瓶颈。
Linux 内核的 blk-iocost 正是为解决这类问题而设计的一种块 I/O 资源控制机制。
与简单限制 IOPS 或带宽的方式不同,blk-iocost 通过成本模型估算不同 I/O 请求消耗的设备资源,并利用虚拟时间机制控制请求的派发节奏。它还能够根据设备负载动态调整不同控制组的资源分配。
近期,Tao Cui 提出了一系列补丁,尝试允许通过 BPF 程序自定义 blk-iocost 的成本计算逻辑,从而改善传统成本模型在复杂工作负载下的局限性。
本文将介绍 blk-iocost 的设计目标、核心算法、内核实现思路,以及 BPF 扩展所带来的可能性。
二、blk-iocost 的设计目标
2.1 为什么需要 I/O 成本模型?
假设一台服务器运行着三个工作负载:
-
数据库主要执行连续的大块读写。
-
一个容器执行大量随机小块 I/O。
-
一个后台任务执行混合读写操作。
这些工作负载共享同一块 SSD。
如果只按照 I/O 请求数量分配资源,就无法准确反映它们对设备造成的实际负载。即使两个控制组执行相同数量的 I/O 请求,它们消耗的设备时间也可能存在很大差异。
如果只按照传输的数据量进行限制,也存在类似问题。相同大小的顺序访问和随机访问,可能具有不同的执行开销。
因此,blk-iocost 的基本思路是:为每个 I/O 请求分配一个代表预期资源消耗的成本,然后根据各控制组的权重和累计成本决定请求何时可以被派发。
2.2 与其他块 I/O 控制机制的区别
Linux 提供了多种 I/O 控制和调度机制,它们解决的问题并不完全相同。
| 机制 | 主要用途 |
|---|---|
io.max |
限制控制组的 IOPS 或带宽上限 |
io.weight |
在竞争时按照相对权重分配 I/O 资源 |
blk-iocost |
根据 I/O 成本和控制组权重管理资源分配 |
mq-deadline |
按照截止时间等策略组织和调度块 I/O 请求 |
blk-iocost 与 cgroup v2 的 io.weight 密切相关。io.weight 是控制组配置接口之一,而 blk-iocost 则提供了基于成本模型的资源控制实现。
需要注意,blk-iocost 并不是用来完全取代块层 I/O 调度器的。它主要解决不同控制组之间如何分配 I/O 资源的问题,而请求的具体排序、队列管理和硬件执行仍然涉及块层、I/O 调度器及设备驱动等组件。
三、核心原理:成本模型与虚拟时间
理解 blk-iocost,需要掌握两个关键概念:Cost 和 Virtual Time。
3.1 I/O 成本计算
blk-iocost 会为每个 I/O 请求计算成本。成本用于估算该请求预计消耗多少设备资源,尤其是设备时间。
可以用一个简化的函数表示:

其中:
-
operation表示读、写及其他操作类型。 -
size表示请求的数据大小。 -
location表示请求访问的磁盘位置。 -
其他参数可能涉及设备特征和请求属性。
这里的成本是调度模型使用的抽象量,并不一定等于实际测量到的 I/O 延迟。
传统机械硬盘的随机访问通常涉及较大的寻道开销,因此顺序访问和随机访问的成本差异十分明显。在 SSD 上,随机访问的代价通常较低,但顺序访问与随机访问之间仍可能存在性能差异。
根据 Tao Cui 的补丁说明,当前讨论的成本模型会对被判断为顺序访问的请求分配较低成本,对被判断为随机访问的请求分配高得多的成本。相关说明提到,在特定模型下,两类请求的成本比例最高可以达到 112 倍。
这个比例是成本模型中的相对计费差异,并不意味着所有 SSD 的随机 I/O 实际耗时都必然是顺序 I/O 的 112 倍。
成本计算的准确性非常重要,因为它直接影响不同控制组最终能够获得多少 I/O 资源。
3.2 虚拟时间机制
每个块设备维护设备级虚拟时间,每个控制组也维护自己的虚拟时间。
当一个控制组执行 I/O 请求时,其虚拟时间会根据请求成本向前推进。成本越高,对应的虚拟时间消耗通常也越大。
为了直观理解,可以借助一个简化的加权模型:

其中:
-
表示控制组的虚拟时间增量。 -
表示请求成本。 -
表示控制组权重。
假设两个控制组执行成本相同的请求,Group A 的权重为 2,Group B 的权重为 1。
则在这个简化模型中:


Group A 每次执行相同成本的请求,消耗的虚拟时间更少,因此在持续竞争时,可以获得更多请求执行机会。
实际的 blk-iocost 实现还涉及设备虚拟时间、控制组状态、资源分配参数和运行时调节逻辑。上述公式仅用于说明成本与权重之间的关系,并不是内核完整的虚拟时间计算公式。
3.3 如何决定请求何时派发?
可以将其工作过程概括为:
-
进程发起块 I/O 请求。
-
块 I/O 控制层获取请求属性和所属控制组。
-
成本模型计算请求成本。
-
控制器根据控制组的虚拟时间和当前设备状态评估请求是否可以派发。
-
满足调度条件后,请求被允许继续进入块设备执行路径;否则,可能需要等待。
设备级虚拟时间提供了一个衡量整体资源消耗的参考,而控制组虚拟时间用于衡量各控制组相对于其资源份额的消耗情况。
这种设计使控制器能够在不同请求成本之间进行比较,而不是简单地统计请求数量。
需要注意,虚拟时间并不是一个要求每个请求严格等待定时器到期的真实时钟。它是调度和资源核算机制的一部分,实际派发过程还会受到队列状态、设备负载和并发请求等因素影响。
四、blk-iocost 的两阶段设计
blk-iocost 的另一个重要特点是将成本计算与整体资源规划分开。
4.1 第一阶段:请求成本计算
第一阶段位于请求处理路径中,负责为每个 I/O 请求确定成本。
由于高速 NVMe 设备每秒能够处理大量请求,这个阶段必须足够高效。如果每次 I/O 都需要执行复杂的全局分析,就可能显著增加 CPU 开销,降低设备吞吐量。
因此,blk-iocost 使用预先计算的参数,使请求成本计算尽可能轻量化。
4.2 第二阶段:周期性规划
第二阶段是规划阶段,通常每隔几毫秒运行一次,用于调整整体资源分配。
主要任务包括:
-
调整不同控制组的权重和有效资源份额。
-
将未被充分利用的资源重新分配给更繁忙的控制组。
-
调整设备虚拟时间的推进速率。
-
在设备利用率与响应时间之间寻找平衡。
例如,如果 Group A 暂时没有 I/O 请求,而 Group B 正在处理大量请求,规划阶段就可以调整资源分配,让 Group B 利用更多可用资源。
同时,如果设备过于繁忙、响应时间开始恶化,控制器还需要调整运行节奏,避免单纯追求吞吐量而导致延迟失控。
这种设计的优点是:请求路径保持高效,而资源分配则可以通过周期性规划适应不断变化的工作负载。
五、传统成本模型的局限性
尽管 blk-iocost 的设计比较完善,但成本模型仍然面临一个根本问题:如何准确判断请求的实际资源消耗?
5.1 多个顺序 I/O 流可能被误判为随机访问
假设同一个控制组中有两个进程,分别执行顺序 I/O。
进程 A:
100, 101, 102, 103, 104, ...
进程 B:
800, 801, 802, 803, 804, ...
如果两个进程的请求交错提交,控制器可能观察到:
100, 800, 101, 801, 102, 802, 103, 803, ...
从整个控制组的请求序列来看,这种访问模式可能看起来像随机访问。
但实际上,每个进程都在执行顺序 I/O。
如果控制器将这些请求判定为随机访问,就会为其分配更高的成本。结果是,该控制组可能受到不必要的惩罚,整体 I/O 性能下降。
5.2 局部随机访问可能被误判为顺序访问
相反,如果随机请求集中在一个较小的地址范围内,某些启发式算法可能会把它们误判为顺序访问。
这些请求因而获得较低成本,使相关控制组在资源竞争中得到不公平的优势。
5.3 不同设备的行为存在差异
不同的 SSD、NVMe 设备和存储系统,可能对请求大小、读写类型、请求位置及访问模式表现出不同的性能特征。
例如,设备内部并行度、控制器实现和闪存管理机制,都可能影响请求的实际执行成本。
因此,一个统一的成本模型很难在所有设备和工作负载上都做出最准确的判断。
这引出了一个自然的问题:能否保留 blk-iocost 的资源分配框架,同时允许用户根据具体工作负载定制成本计算逻辑?
BPF 正是这项扩展工作的关键。
六、通过 BPF 自定义成本模型
Tao Cui 提交的补丁系列尝试让 blk-iocost 支持一种新的 BPF struct_ops 接口,使用户可以加载自己的成本计算程序。
这组补丁主要修改成本计算部分,而保留原有的规划阶段。
6.1 核心接口
补丁提出的接口如下:
struct iocost_model_ops {
u64 (*calc_cost)(u64 opf, u64 nbytes,
sector_t sector,
struct blkcg *blkcg,
u64 model_flags);
void (*blkcg_online)(struct blkcg *blkcg);
void (*blkcg_offline)(struct blkcg *blkcg);
/* Model name, selected through io.cost.model */
char name[16];
};
其中最重要的成员是 calc_cost(),它负责计算一个 I/O 请求的成本,也是必需实现的操作。
各参数含义如下:
-
opf:请求的操作标志,包括读写类型及其他操作属性。 -
nbytes:请求大小,以字节为单位。 -
sector:请求访问的扇区位置。 -
blkcg:发起请求的块 I/O 控制组。 -
model_flags:模型标志,可能包含IOCOST_COST_F_MERGE,表示当前请求正在合并到另一个已有请求中。
函数的返回值就是分配给该请求的成本。
另外两个回调是可选的:
-
blkcg_online():控制组进入相关管理状态时执行。 -
blkcg_offline():控制组退出相关管理状态时执行。
如果 BPF 程序需要为控制组创建、维护或清理自己的状态,可以利用这些回调。
6.2 为什么采用 struct_ops?
BPF 并不只是用于跟踪系统事件,也可以在内核提供的特定扩展接口中实现可替换的操作逻辑。
struct_ops 允许 BPF 程序实现内核定义的操作结构体,再由内核在相应时机调用这些实现。
在这个场景下,内核定义 iocost_model_ops 接口,而 BPF 程序实现 calc_cost() 等回调。
这种方式与简单地通过 kprobe 观察函数调用不同:它提供的是一种明确的策略扩展接口,而不仅仅是事件观测能力。
6.3 加载程序并不意味着自动启用
补丁设计中,加载 BPF 程序本身不会自动把新成本模型应用到所有块设备。
程序提供一个模型名称,之后还需要通过相应控制组目录中的 io.cost.model 接口选择该模型。
这使模型加载和模型选择成为两个不同的步骤,也为不同设备或控制组采用不同的模型留下了空间。
需要强调的是,以上接口来自所讨论的 RFC 补丁系列。实际部署时必须确认目标内核是否已经合并相应功能,以及接口和参数是否与补丁版本一致。
七、示例:利用 BPF 识别多个顺序 I/O 流
补丁系列还提供了一个示例程序,尝试改善包含多个顺序 I/O 进程的控制组的性能。
该程序使用 BPF map 记录观察到的 I/O 流,并判断新请求是否符合已跟踪流的顺序访问特征。
如果请求匹配其中任意一个流,就可以按照顺序 I/O 的成本规则进行计费。
示例程序最多尝试跟踪四个相互独立的 I/O 流。不过,如果同一个控制组还存在随机 I/O,实际能够跟踪的顺序流数量可能会更少。
其设计思路可以概括为:
-
记录各个 I/O 流的访问状态。
-
根据新请求的扇区位置及其他信息判断其与已有流的关系。
-
如果请求符合某个流的顺序访问特征,就使用相应的成本策略。
-
如果无法可靠地识别请求,则需要由程序的其他判断逻辑决定如何处理。
这样,即使多个进程的顺序请求在全局序列中交错出现,成本模型仍然有机会识别每个进程各自的访问模式。
不过,这种方案也有局限性。例如,有限数量的流跟踪状态无法覆盖所有复杂工作负载;流识别算法还需要考虑请求合并、并发提交以及访问模式变化等情况。
因此,BPF 扩展能够提供更大的定制空间,但并不意味着任意自定义模型都必然优于内核默认模型。
八、blk-iocost 的内核实现与学习路径
对于希望深入 Linux 内核的开发者,可以从以下几个方面研究。
8.1 核心源码
首先阅读 block/blk-iocost.c,重点理解:
-
成本模型如何为请求分配成本。
-
控制组如何维护虚拟时间和资源状态。
-
请求派发如何受到成本和虚拟时间的影响。
-
规划阶段如何调整权重及设备运行参数。
其次研究 cgroup v2 的块 I/O 控制接口,理解 io.weight、io.max、io.stat 和 io.cost 相关接口之间的关系。
最后研究块层请求的提交、合并和派发路径,理解成本计算在整个 I/O 生命周期中的位置。
8.2 BPF 扩展的研究方向
对于 BPF 成本模型,可以进一步关注:
-
struct_ops如何将 BPF 回调注册到内核。 -
calc_cost()的调用频率和执行开销。 -
BPF map 如何保存多个 I/O 流的状态。
-
请求合并对成本计算和流识别的影响。
-
控制组在线、离线时的状态管理。
-
自定义模型与默认模型在不同负载下的性能差异。
还应关注并发访问、状态同步、BPF 验证器限制,以及自定义成本模型是否可能导致某些控制组获得过多或过少的资源。
8.3 如何验证效果?
要评估一个自定义模型是否有效,不能只观察总吞吐量。
建议同时测量:
-
IOPS 和带宽。
-
平均 I/O 延迟。
-
P95、P99 等高分位延迟。
-
不同控制组之间的资源分配公平性。
-
设备利用率。
-
CPU 开销和调度路径开销。
测试时应比较默认模型与自定义模型,并覆盖顺序读写、随机读写、多进程交错访问及混合负载等场景。
只有当吞吐量、延迟和公平性等指标共同表明效果改善时,才能判断新的成本模型真正解决了问题。
九、总结
blk-iocost 是 Linux 内核中一种基于成本模型的块 I/O 资源控制机制。它的核心不是简单地限制 IOPS 或带宽,而是估算请求消耗的设备资源,并通过虚拟时间和周期性规划,在竞争的控制组之间分配 I/O 资源。
其设计可以归纳为三个核心部分:
-
Cost: 估算不同 I/O 请求的资源消耗。
-
Virtual Time: 根据请求成本和控制组资源份额管理请求派发。
-
Planning: 周期性调整资源分配和设备运行节奏。
传统成本模型的问题在于,它难以准确识别所有复杂的访问模式,也很难适应所有设备的性能特征。
通过 BPF struct_ops 扩展成本计算逻辑,可以在保留现有资源分配框架的同时,为特定工作负载提供更灵活的策略。例如,通过跟踪多个独立 I/O 流,可以改善多进程顺序访问被误判为随机访问的问题。
这项工作的核心价值是将稳定的内核资源管理机制与可定制的成本策略分离,使 Linux 块 I/O 控制具备更强的适应能力。
对于 Linux 内核开发者而言,blk-iocost 也是一个很好的研究案例:它展示了如何在高性能 I/O 路径中平衡计算开销、公平性、动态资源分配,以及通过 BPF 扩展内核行为的灵活性。
