LWN:利用BIO caching来提供IO速度!
关注了就能看到更多这么棒的文章哦~
More IOPS with BIO caching
By Jonathan Corbet
September 6, 2021
DeepL assisted translation
https://lwn.net/Articles/868070/
很久很久以前,块存储设备(block storage device)速度非常慢,经常拖慢了整个系统的速度。为了能从存储设备中获得最佳性能,我们花了很大的力气来对所有的 request 调整顺序。为此而让 CPU 多耗费一些时间就是值得的。但是后来存储设备的速度大增,这个交换就不那么值得了。后来,那些花里胡哨的 I/O 调度机制不再是人们关注重点了,现在的工作重点变成了优化代码能使 CPU 跟上存储设备的速度。针对 5.15 内核最近就合并了一个 block layer 的改动,正好展示了为了从当前硬件中能获得最佳性能而必须做出哪些权衡。
在 block layer 中,每个 I/O 操作都是使用一个 struct bio 来代表的。这种结构的每一个实例通常都被称为 "BIO"。BIO 中包含了一个指向相关 block 设备的指针、指明即将要传输哪些 buffer、一个指向在操作完成后所要调用的函数指针以及大量的的辅助信息。针对系统中发起的每一个 I/O 操作,都需要分配一个 BIO、管理维护好、最终释放掉。对于那些拥有速度飞快的 block device 的大型、繁忙系统来说,每秒钟可以产生数百万次的 I/O 操作(IOPS),因此有大量的 BIO 结构在源源不断地进行这个从生到死的循环。
内核中的 slab allocator 针对这种重复分配和释放相同大小的结构的场景进行了优化。看起来它应该最适合作为 block 子系统中分配的 BIO 的来源。但事实证明 slab allocator 的速度还不够快,它已经成为拖慢 block I/O 的瓶颈了。因此,block 子系统维护者 Jens Axboe 准备一组 patch 来解决这个问题。
他实现的是对 BIO 结构的一个简单缓存(cache)。它是由一组 linked list(链表)组成的。系统中每个 CPU 都有一个这样的 linked list。每当需要一个新的 BIO 时(并且需要满足其他一些条件,详见下文),都会检查当前 CPU 中的链表。只要在那里发现有空闲的 BIO,那么就可以从链表中移出来而直接使用,就不必再调用 slab allocator 了。如果链表是空的,那当然只能跟往常一样通过 slab 进行分配了。在需要释放一个 BIO 的时候,它会被加入到当前 CPU 的链表中去。如果链表变得太大了(超过了 576 个 cached BIO),那么就会把 64 个 BIO 一次性交还给 slab allocator。
这个机制很简单,这也是它为什么速度很快的原因了。block layer 就不需要调用 slab allocator 了,而是直接从相应的 per CPU 链表中获取可用的 BIO,都不需要调用任何函数了。使用了 per-CPU 链表就可以避免使用 lock 机制,这进一步提高了速度。链表的管理模式就跟堆栈一样,这也使得所分配出来的 BIO 还存在于 CPU cache 中的可能性大大提高了。最终都带来了显著的性能提升。
至少对于某些工作负载来说是看到很大性能提升的。如前所述,BIO cache 机制很简单,所以它没有确保发生中断时也是安全的。per-CPU 的数据结构只有在 kernel 执行 critical section 时不会被抢占的情况下,才可以确保不用 lock 也是安全的。而 interrupt 当然也是一种 preemption(抢占),在这种情况下就会导致问题。如果一个 block-driver 的中断处理程序 (interrupt handler) 正在分配或释放一个 BIO,而此时有其他的内核代码也在做类似的动作,那么很可能会出问题,而用户也就不会因为提升这些性能而感激作者。
当然,BIO cache 可以做成是不怕 interrupt 的,而且后面也许有一点就会实现成这样。但是禁用中断也会带来性能损失,这是一个不这么实现的很好的理由。如果不关闭 interrupt 的话,BIO cache 就只能在那些绝对不会在 interrupt handler 同时进行调用的情况下使用了。好消息是,这里有一种情况下这一点是可以得到保证的,那就是当使用 block-layer I/O polling 的时候。polling 操作会先关闭来自存储设备的 interrupt,完全只在这里循环操作直到 I/O 请求完成为止。对于那些快速设备(fast device)来说,这实际上是很合理的做法。在非常希望能尽量提高 I/O 速率的情况下,系统管理员很可能会启用 polling。因此针对这个场景再提供一些额外的性能提升,那就会是很有意义的。
把 slab allocator 的动作从这个循环操作中移除掉,就可以大大改善性能,但这里还有一个问题需要改正。block layer 有一个叫做 bio_init() 的函数,其中主要有下面这个操作:
memset(bio, 0, sizeof(*bio));
人们可能认为 memset() 是对这样一个中等大小的结构进行初始化的最快速的方法了,但事实证明并非如此。所以 Axboe 还多做了一个 patch,用一系列直接对 BIO 中每个 field 设置为 0 的操作替换了 memset() 调用。changelog 中指出,这个改动使得分配加上初始化 BIO 的总时间减少了一半(当然是在使用 BIO cache 的情况下)。
Axboe 说,在这些改动都到位之后,block layer 的性能提高了大约 10%。在他的测试系统中每个 CPU core 上都可以达到超过 350 万 IOPS 了。这里节省了大量的 block 分配释放,肯定会让管理 storage server 的经理们非常高兴了。这组 patch 展示了在当前硬件上要想优化 I/O 吞吐量可以做(而且必须做)哪些事情。但它也表明,可能是时候要在这些已经高度优化的 slab allocator 上投入更多精力来进行优化了。如果内核开始针对每个子系统中的对象都做缓存,从而绕过 allocator,那么系统的整体性能就会受到影响。同时,基本上所有人都会喜欢这种提高 block-I/O 性能的做法。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~