由于公司的分布式存储产品(基于Ceph)需要提供QoS特性,之前也相关经验,所以打算先分析QEMU QoS的特性及其实现原理,然后基于此,给出Librbd QoS的定义,内容如下:
由于GNU Linux
系统的cgroup
不支持网络设备(nfs
,ceph
)的资源控制,为提供一套通用的I/O
流控机制。QEMU
实现了一个独立的流控
模块来控制磁盘I/O
操作,该模块能实现磁盘TPS
、OPS
及潮汐式
流量控制,具体特性如下:
QEMU QoS
特性
磁盘I/O
的两个方面: 每秒的数据量(TPS
)以及每秒的I/O
操作(OPS
):
QEMU
能分别为TPS
和OPS
提供全局配置或者针对读写的独立配置。- 全局配置和针对读写的差异配置不能同时使用
- 支持同时控制
TPS
和OPS
- 支持
潮汐
式I/O
流量控制 - 支持
I/O
大小控制 - 支持基于磁盘组的流控控制
QEMU
默认不开启流量控制
全局配置
及读写差异配置
QEMU
针对TPS
及OPS
分别提供全局配置及读写配置。全局配置(*-total
)同时控制读写I/O
操作,而读写配置(*-read
, *-write
)则通过设置不同的参数分别控制读写I/O
操作。
全局配置和读写配置不能同时使用
潮汐式流量控制
除了上述标准的TPS
及OPS
控制,QEMU
还支持潮汐式流控,允许磁盘I/O
在指定长度的时间(*-total-max-length
)内超过标准配置,出现I/O
波峰(*-total-max
),这在计划内的流量高峰期很有用(如:系统启动,服务启动)。
控制I/O
大小
在执行OPS
控制时,通常不考虑I/O
的大小,所有的I/O
都同等对待,从TPS
时序图上看,就是一个波浪形的流量线。QEMU
支持配置(io-size
)来平滑,磁盘流量。大于io-size
的请求将会正确的分解为多个I/O
,而小于io-size
的请求会被当做一个I/O
。
磁盘组流控
前述的各种配置都是针对单个磁盘的。此外,QEMU
还提供了磁盘组流控功能,加入相同流控配置组的所有磁盘共享相同的限制。默认情况下,每个磁盘设备都会创建一个以自身名字为组名的配置组,同时一个设备只能加入一个配置组。
QEMU QoS
原理
leaky bucket algorithm
是网络世界中流量塑型(Traffic Shaping
)或速率限制(Rate Limiting
)时常用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。通过漏桶算法,突发流量可以被整形(Shaping
)以便为网络提供一个稳定的流量。
QEMU
中也采用漏桶
算法实现I/O
限制。这就像是一个漏水的桶,倒入桶中的水就是要执行的I/O
, 桶满了,I/O
就不能被加入,被堵塞了。举下面的例子来说明:
iops-total = 100
iops-total-max = 2000
iops-total-max-length = 60
- 水从桶中漏走的速度是 100 IOPS
- 往桶中倒水的速度是 2000 IOPS
- 桶的大小是 2000 * 60 = 12000
- 如果
iops-total-max
没有设置,桶的大小就是 100 * 60 = 600
开始的时候桶是空的,所以可以以 2000 IOPS的速度往桶里面加入I/O
,直到桶满。桶满后,就只能以漏水的速度往桶里面加入I/O
,也就是 100 IOPS。如果之后加水的速度小于漏水的速度,到某个时刻桶就会空,这个时候就又可以以 2000 IOPS的速度往桶里面加入I/O
。
需要注意的是,桶是一直在漏水的,所以桶加满水的时间通常是大于60秒。另外,如果加水的速度小于2000 IOPS,桶加满水的时间也是大于60秒。
QEMU QoS
实现
QEMU
的I/O
控制由块层实现,流控模块实现包含在Throttle.h/.c
,Throttle-groups.h/.c
文件中,完整的块设备I/O
限制,还依赖于协程coroutine
及定时器(timer
)。
QEMU 协程
协程(coroutine
)是一种实现堆栈切换的机制,通常用来实现用户态线程间的协作。针对不同的系统,QEMU
提供了多种协程实现方式(gthread
,signalstack
,ucontext
)等,代码实现在coroutine-*
文件中,QEMU
中主要提供了三个接口:qemu_coroutine_create
,qemu_coroutine_enter
,qemu_coroutine_yield
来使用协程
qemu_coroutine_create
创建协程qemu_coroutine_enter
执行协程qemu_coroutine_yield
阻塞,等待退出
其中,qemu_coroutine_yield
可以不主动调用,QEMU
执行完用户代码后,会主动执行qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE)
在(coroutine_trampoline
)函数里。如果调用了qemu_coroutine_yield
,如在异步IO中,此协程会被挂起,等到I/O
完成时,在回调(callback
)中调用qemu_coroutine_enter
进入该协程运行,刚好该协程运行的是COROUTINE_TERMINATE
那句,从而完成了协程的退出。
这里不打算解析具体代码,只通过图表的形式,以ucontext
实现为例,展示QEMU coroutine
的工作原理:
用户代码调用
qemu_coroutine_create
创建协程
1.1 调用qemu_coroutine_new
创建协程,返回coroutine
对象
1.1.1 切换到coroutine_trampoline
执行,设置协程环境信息(self->env
)
1.1.2siglongjmp
跳转到qemu_coroutine_new
的sigsetjmp
处继续执行
1.1.3 返回协程对象用户代码调用
qemu_coroutine_enter
执行协程
2.1 调用qemu_coroutine_switch
,执行堆栈切换
2.1.1siglongjmp
跳转到coroutine_trampline
的sigsetjmp
处继续执行
2.2 执行co->entry
用户代码
2.3 调用qemu_coroutine_switch
,执行堆栈切换,回到之前的qemu_coroutine_switch
的sigsetjmp
处继续执行
2.4 返回到qemu_coroutine_enter
,操作完成,删除当前协程
I/O
控制
QEMU
通过协程来执行I/O
请求,在Block-backend.c
文件的blk_aio_prwv
函数中调用前述方法完成协程的创建和执行,协程入口函数为:blk_aio_write_entry
,流程图如下:
在I/O
的后续处理过程中Exceed Limits
部分,由QEMU Throttling
模块实现。在设备初始化过程中,如果开启了I/O throttling
, 就会在blockdev.c
的blockdev_init
方法中初始化throttling
配置,设置读写timer
并与设备关联,类图如下:
要点如下:
- 每个设备关联一个
ThrottleGroup
(如果开启了I/O
流量限制), 所有的组由全局变量throttle_groups
管理 - 每个
ThrottleConfig
可包含达6个I/O
限制配置(LeakyBucket
) - 每个设备分别关联读写两个定时器(
timer
),当超过I/O
限制时,I/O
协程被阻塞,定时器被设置并添加到event loop
,定时器超时后,会重新进入协程继续I/O
处理
I/O throttling
模块内部的处理逻辑如下:
Ceph Librbd QoS
定义
默认情况下,Ceph Rbd
并不提供QoS
功能,然而在Ceph Cluster
集群存储能力一定的情况下,为避免某个Rbd image
的突发流动对其他的image
带来影响,限制某个Rbd
设备的I/O
流量,就很有必要。
结合上述对QEMU QoS
的特性及原理分析,我们将librbd QoS
定义如下:
- 提供基于
TPS
和OPS
的I/O
限速功能 - 为
TPS
和OPS
提供全局配置或者针对读写的独立配置(两种配置不能同时设置) - 支持
潮汐
式I/O
流量控制 - 支持
I/O
大小控制