令牌桶算法
令牌桶算法是一个非常老牌的 I/O 控制算法,在网络、存储 I/O 上都有着广泛的应用。即:一个固定容量的桶装着一定数量的令牌,桶的容量即令牌数量上限。桶里的令牌每隔固定间隔补充一个,直到桶被装满。一个 IO 请求将消耗一个令牌,如果桶里有令牌,则该 IO 请求消耗令牌后放行,反之则无法放行。对于限制 IO 请求 bps,只需让一个 IO 请求消耗 M 个令牌即可,N 即为此 IO 请求的字节数。
令牌桶算法可以达到以下效果:
- 令牌桶算法可以通过控制令牌补充速率来控制处理 IO 请求的速率;
- 令牌桶算法允许一定程度的突发,只要桶里的令牌没有耗尽,IO 请求即可立即消耗令牌并放行,这段时间内 IO 请求处理速率将大于令牌补充速率,令牌补充速率实际为平均处理速率;
- 令牌桶算法无法控制突发速率上限和突发时长,突发时长由实际 IO 请求速率决定,若实际 IO 请求大于令牌补充速率且速率恒定,则:
突发时长 = 令牌桶容量 / (实际 IO 请求速率 - 令牌补充速率)
。
在令牌桶算法的描述中,有一个条件是无强制约束的,那就是在桶里的令牌耗尽时,无法放行的 IO 请求该怎么处理。对于处理网络层报文,实际无外乎三种方式:
- 第一种是直接丢弃(Traffic Policing,流量监管)。
- 第二种则是排队等待直到令牌桶完成所缺失数量令牌补充后再消耗令牌放行(traffic shaping,流量整形)。
- 第三种为前两者的折中,设定有限长度队列,在队列已满时丢弃,否则遵循第二种处理方式。当然对于不可随意丢弃的 IO 请求,处理方式一般为第二种。
此外,一般在实际的实现中,难以做到严格按照固定时间间隔一次补充一个令牌。可考虑的替代方案一般为,加大补充令牌的时间间隔,减少补充次数,但一次补充多个令牌,单次补充令牌的个数与时间间隔成正比。
对于令牌桶算法,还可以考虑一种极端的情况。令牌桶算法支持突发的能力是由令牌桶的容量决定的,以限制 IO 请求 iops 为例,假设我们将令牌桶容量仅设为 1,此时令牌桶算法支持突发的能力也就不复存在了,IO 请求速率上限即被严格控制为令牌补充速率。
漏桶算法漏桶算法有两种类型的定义:leaky bucket as a meter 和 leaky bucket as a queue。
leaky bucket as a meter:同样以限制 IO 请求iops为例,替换上述的网络层报文,我们可以理解如下:一个桶,容量固定。桶以固定的速率漏水,除非桶为空。一个 IO 请求将往桶里增加固定量的水,假如增加的水量将导致桶里的水溢出,则该 IO 请求无法放行,反之则放行。
再把令牌桶的理解放在这里对比下:一个固定容量的桶装着一定数量的令牌,桶的容量即令牌数量上限。桶里的令牌每隔固定间隔补充一个,直到桶被装满。一个 IO 请求将消耗一个令牌,如果桶里有令牌,则该 IO 请求消耗令牌后放行,反之则无法放行。
实际上,把漏水换成补充令牌,把加水换成消耗令牌,再细细品读,我们可以发现,这两段描述的含义就是一样的,仅仅是角度不一样而已。所以 leaky bucket as a meter 与 token bucket 可认为是等价的。
leaky bucket as a queue:漏桶实际为一个有限长队列。当一个报文到达时,假如队列未满,则入队等待,反之则被丢弃。在队列不为空时,每个固定时间间隔处理一个报文。
虽然描述更为简单,dan 可以看到 leaky bucket as a queue 比 leaky bucket as a meter 的要求要严格得多,leaky bucket as a queue 可以达到以下效果:
- 可以通过控制处理时间间隔来严格控制速率上限;
- 不支持任何程度的突发;
- 对超出限定速率的报文可能做入队等待处理,也可能做直接丢弃处理,当队列长度设置为足够小,设置为 0 时,可认为等同于全部做直接丢弃处理(Traffic Policing),而队列长度设置为足够大时,可认为等同于全部做入队等待处理(Traffic Shaping)。
如此一来,我们可以看到 leaky bucket as a queue 实际效果与令牌桶容量为 1 的令牌桶算法是一致的,也就是说 leaky bucket as a queue 其实可视为令牌桶算法的一种特例(不支持突发)。
前端 QoS:通过 QEMU 的块设备 IO 限速机制进行限速QEMU 早在 1.1 版本就已支持块设备的 IO 限速,提供 6 个配置项,可对上述 6 种场景分别进行速率上限设置。在 1.7 版本对块设备 IO 限速增加了支持突发的功能,以总 iops 场景为例:支持设置可突发的总 iops 数量。在 2.6 版本对支持突发的功能进行了完善,可控制突发速率和时长。
QEMU 的块设备 IO 限速机制主要是通过漏桶算法实现。在 2.6 版本中不仅支持突发,可支持控制突发速率和时长。
QEMU 同样使用了多个桶,以对 6 种场景独立进行 QoS 限速。
QEMU 通过大桶和突发小桶的设计,支持了对突发速率和突发时长的控制。有突发流量到来时,限速分为 3 个阶段:
- 首先是突发小桶未满时,未做速率限制,但此突发小桶的容量仅设置为突发速率值的 1/10,所以此阶段所经历的时间特别短,效果可忽略不计;
- 其次是突发小桶已满而大桶未满的阶段,速率即被限制为突发速率,由于大桶在不断地以基本速率漏水,所以实际的突发时长要大于设置的突发时长,此阶段的实际突发时长为:
实际突发时长 = 大桶容量 / (突发速率 - 基本速率上限)
,而大桶容量 = 突发速率 * 设置突发时长
。 - 最后是大桶已满的阶段,此时速率就被限制为基本速率上限了。
在不考虑突发的情况下,从算法效果来看,QEMU 实现的漏桶算法实际为桶容量很小的令牌桶算法,当然也因为桶容量足够小,所以基本可视为我们狭义理解上的漏桶算法。
与 Librbd 固定时间间隔补充令牌不同,QEMU 在处理每个请求时同步执行漏水操作,每秒执行漏水操作的次数实际与每秒 IO 请求数量相等,漏水频率一般远大于 Librbd 补充令牌的频率。这也形成了一个特点,在 QoS 限速情况下,QEMU 处理 IO 请求的时间点分布比较均匀,而 Librbd 则相对集中在补充令牌的时间点上。同时 IO 请求延迟的分布特点也与 Librbd 有明显的不同。
后端 QoS:通过 librbd 的镜像 IO 限速机制进行限速Ceph 在 13.2.0 版本(M 版)支持对 RBD 镜像的 IO 限速,此版本仅支持总 iops 场景的限速,且支持突发,支持配置突发速率,但不可控制突发时长(实际相当于突发时长设置为 1 秒且无法修改)。在 14.2.0 版本(N 版)增加了对读 iops、写 iops、总 bps、读 bps、写 bps 这 5 种场景的限速支持,对突发的支持效果保持不变。
Ceph Librbd 的镜像 IO 限速机制使用的是令牌桶算法。所以,Librbd 的限速机制支持突发,支持配置突发速率,但不支持控制突发时长,这与令牌桶算法定义描述的效果近乎一致。而从实际的实现来看,Librbd 的令牌桶算法实现也的确与定义比较匹配。
Librbd 使用了 6 个令牌桶,所以可对 6 种场景独立进行 QoS 限速,若这些场景的 QoS 限速全部配置启用,那么一个 I/O 请求需在每一个令牌桶消耗相应数目的令牌后才可被放行(当然,写请求在限制读速率的桶无需消耗令牌,反之亦然)。
令牌桶的容量由基本速率上限值或突发速率值决定,而且突发速率值的唯一作用就是用于配置令牌桶容量。从上面的内容可知,令牌桶算法的效果分析来看,只要令牌桶容量不是足够小,该令牌桶算法即支持突发。所以即使未设置突发速率,令牌桶容量由于为基本速率值,实际也是支持突发的:突发时长 = 基本速率值 / (实际 IO 请求速率 - 基本速率)
。
此外,令牌桶算法本身无法控制突发速率上限,所以设置所谓的突发速率其实并没有对真正对速率上限进行限制,只是通过增大令牌桶容量而客观地增大了突发时长:突发时长 = 突发速率值 / (实际 IO 请求速率 - 基本速率)
。
此处以限制 iops 来举个实际例子,基本速率设置为 1000iops,突发速率设置为 2000iops,假如实际 IO 请求速率恰好为 2000iops,那么突发时长实际就支持 2 秒。而且实际 IO 请求速率并没有得到限制,假如达到了 5000iops,那么实际突发时长仅为 0.5 秒,即 0.5 秒后,速率就被稳定限制在 1000iops。可见 Librbd 的突发速率概念还是很容易给人带来误解的,实际应理解为可突发的 I/O 请求量。
Librbd 的令牌桶算法实现中,加大了补充令牌的时间间隔,一次补充多个令牌,此方式有助于减小算法的运行开销。在令牌补充的一瞬间到令牌消耗完这段时间内,IO 请求由于获得了足够的令牌数量,可被快速放行,而一旦令牌耗尽,超过 QoS 限速的请求则陷入排队等待的状态,等待时间即为令牌补充的时间间隔,甚至更多。
总结Librbd 的限速算法与 QEMU 相比有两大不同,一是 Librbd 无法像 QEMU 一样控制突发速率上限和突发时长,二是 Librbd 补充令牌的方式和 QEMU 漏水的方式不同,前者导致请求被处理的时间点较为集中,类似于一批一批地处理,后者则使请求被处理的时间点分布均匀,类似于一个一个地处理。
从测试结果上可以看到,同等条件下,IO 请求平均时延接近于相等,而 QEMU 的限速机制让每个 IO 请求的延迟都接近于平均时延,Librbd 的限速机制则使大部分 IO 请求有较低的延迟,但有一定比例的 IO 请求延迟很高,接近于补充令牌的时间间隔。