Linux 块设备层中的Multi-queue分析

目前,百万甚至千万级别的IOPS数据访问量已成为一个大趋势,并且当前的SSD展现着越来越高效的性能表现。在这种趋势下linux的块设备层渐渐成为了整体系统性能提升的主要瓶颈。本文主要讨论的是在多核系统下,如何利用Multi-queue减小块设备层对整体性能的影响。

传统的块设备层针对于千/万级别的IOPS数据访问量是有所余力的,但是面对与日俱增的大数据访问,特别是在底层SSD提供更高速的访问性能以及上层系统多核化的情况下,传统快设备层将会严重影响整体系统的性能。

针对快设备层的优化,有人曾提出通过绕过快设备层的方式来实现上层系统与底层SSD的性能匹配。显然,该方法存在较大的复杂性,同时也将快设备层中的请求调度等功能移除了。然而,从linux 3.13版本的内核开始,Multi-queue已经被添加到代码中用以减小快设备层对系统性能的影响。其主要思想是为每个核配置一个请求队列,从而均衡多核之间的负载,并减少对请求队列的锁竞争。

首先,针对于传统的快设备层,如图1所示,假设底层仅有一个硬件存储设备,那么块设备层将实例化一个请求队列。所有应用程序发送的IO请求将全部提交至该队列中,然后交由该队列进行相应的处理,例如合并,调度等。现在,从这个角度出发,我们可以发现多个并行的应用程序在请求队列上存在锁竞争的问题,从而严重影响了SSD处理请求的效率。

图 1 传统快设备层结构

其次,针对于目前的多核系统,将会存在更多的中断,上下文切换以及远程访问的问题。因此,传统的单个请求队列将成为主要竞争资源,从而进一步限制了底层SSD的处理效率。

因此,Multi-queue应运而生,结构图如图2所示。其主要思想在于将单个请求队列上的资源竞争分散至多队列上。该多队列主要分为以下两种:软件队列以及硬件分配队列。

软件队列:软件队列原则上可为每个核或者每个socket配置一个处理队列。假设一个NUMA系统有4个sockets,每个socket有6个核,那么最少可配置4个软件队列,最多配置24个软件队列。在对应的每个软件队列中可进行请求调度,添加标记以及计数等功能。在此基础上,每个socket或者核直接将请求发送至其对应的软件队列中, 从而可避免单请求队列造成的锁竞争问题。

硬件队列:硬件队列主要负责与底层设备驱动的匹配,即存在多少个设备驱动,则配置对应数量的硬件队列。其将负责将来自软件队列的请求发送至驱动层。

图 2 Multi-queue块设备层结构

Multi-queue中的调度问题:目前Multi-queue在设计过程中寄希望于底层SSD已经能够提供足够的性能使得随机访问与顺序访问呈现类似的访问性能,并且,其同样希望当前插入到各个软件队列中的数据具有强局部性,因此,当前linux内核代码中的软件队列没有提供相关的调度设计。但是,若底层存储设备是机械硬盘时,那么在软件队列中的请求调度是十分有必要的。另外,在Multi-queue的环境下,公平性调度的意义已经不大,因为每个核或者socket都配有一个软件队列,那么来自于多个核的请求将能够被均衡负载。

当请求加入到硬件队列后,其会被打上一个唯一标签,该标签会随后传入驱动层,主要用于判断此请求是否已被处理完成。

除了上述工作之外,Multi-queue的作者还对应地提供了计数功能以及修改了blktrace的相关机制代码,从而能够提供一定的统计功能以及请求纪录功能。

在实际测试中,基于Multi-queue的快设备层能够取得接近最优话的性能。如下图所示,其中MQ表示Multi-queue块设备层,SQ表示传统的单队列情况, Raw表示最优化的情况。其实验平台信息如表1所示,分别取1,2,4,8 sockets系统。底层设备采取空设备模拟来消除SSD的性能对测试的干扰。在软件队列和硬件队列配置上,每个core配置一个软件队列,每个socket配置一个硬件队列。

从实验结果中,我们可以发现,MQ能够取得极大的性能提升。

图 3 IOPS和时延测试

表格 1 实验平台参数

若要开启Multi-queue功能,则按以下命令操作:

在/etc/default/grub中修改

GRUB_CMDLINE_LINUX=”scsi_mod.use_blk_mq=1″

Sudo update-grub

参考文献:

M. Bjorling, J. Axboe, D. Nellans, and P. Bonnet. Linux block IO: Introducing multi-queue SSD access on multi-core systems.

Multi-queue 代码分析

源码结构如下图:

当要写一个page页到底层设备时,需要通过submit_bh进行提交。该函数主要负责对初始化一个bio以及对其进行相关的封装处理。其函数调用主要的流程为:submit_bh -> submit_bh_wbc -> submit_io ->generic_make_request。一旦bio进入generic_make_request则说明其将在块设备层中被进行相关处理工作。

generic_make_request函数如下:

*****************************************************************

blk_qc_t generic_make_request(struct bio *bio)

{

struct bio_list bio_list_on_stack;

blk_qc_t ret = BLK_QC_T_NONE;

if (!generic_make_request_checks(bio)) //判断当前bio是否有效

goto out;

if (current->bio_list) {

bio_list_add(current->bio_list, bio);

goto out;

}

//上述过程要求当前的make_request_fn每次只能被触发一次,因此,通过current->bio_list判断当前是否有bio在其中,若有则将当前这个加入到尾部等待被处理,若没有则可直接处理该bio

//还有一个值得注意的地方是,通常情况下,current->bio_list是为NULL的,因此,上述if语句将会在进行递归调用generic_make_request时候会执行。而该递归调用的地方即后面的q->make_request_fn函数里面,该函数会判断当前的bio是否超过了最大能处理能力范围,若是则将其拆分,拆分后的剩余bio将会再次被加入到generic_make_request函数中,而此时,current->bio_list中已经包含了原始的超过最大处理能力的bio,为了避免再次出发q->make_request_fn函数,则在上述if语句中先退出,完成拆分后满足处理能力大小的bio

BUG_ON(bio->bi_next);

bio_list_init(&bio_list_on_stack); //初始化该双向链表

current->bio_list = &bio_list_on_stack; //当前为NULL

do {

struct request_queue *q = bdev_get_queue(bio->bi_bdev); //获得bio对应的设备队列

if (likely(blk_queue_enter(q, false) == 0)) { //判断当前的设备队列是否有效能够响应该请求

ret = q->make_request_fn(q, bio); //将bio进行进一步处理,放入块设备层的处理队列中

blk_queue_exit(q);

bio = bio_list_pop(current->bio_list);

} else {

struct bio *bio_next = bio_list_pop(current->bio_list);

bio_io_error(bio);

bio = bio_next;

}

} while (bio);

current->bio_list = NULL; /* deactivate */ //clear this bio list and make_request function is avalible again

out:

return ret;

}

*************************************************************

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值