引言
限流算法中的漏桶/令牌桶被大家所熟知。但生产中进行实现时,真的要通过语言的并发能力模拟一个生产者注水、一个消费者取水吗?如果是不支持并发能力的语言又该怎么办呢?本文带大家从令牌桶/漏桶算法开始,了解实际生产中用到的算法变体:GCRA。
1. 基本限流算法:令牌桶与漏桶
令牌桶 token bucket
令牌桶算法需要创建一个固定容量的桶,再以固定的速度向桶中添加令牌。检查请求是否触发限流,就是要去检查桶中是否有足够的令牌:如果够,则放行请求,并且从桶中删除特定数量的令牌;如果不够,则需要阻拦请求并进一步做其他处理(拒绝、异步、重试等)。
需要注意的是:
- 请求的平均速度等于向桶中添加令牌的速度
- 对请求突发情况的容忍度决定于桶的容量
算法描述:
- 存在参数r和b:r即rate,b即burst。
- 每1/r秒向桶中添加一个令牌。
- 桶中最多容纳b个令牌。如果添加令牌时桶满了,则丢弃该令牌。
- 1个消耗为n的求到达时:
- 如果桶中至少存在n个令牌,则移除n个令牌,并放行请求
- 如果桶中不足n个令牌,则不移除任何令牌,并阻拦请求
漏桶 leaky bucket
漏桶有固定的容量,流量到达时将被桶整流塑形:当桶不空时,流量从桶底以固定速度流出;如果桶满了,流量将溢出并进一步做其他处理。
更具体的描述是:
- 漏桶是一个固定容量的桶,并以固定的速度漏水。
- 桶如果空了就停止泄露。
- 流量到达时将尝试向桶中注水,以确认流量是否能被确认。
- 如果桶满了,上述注水将失败(溢出),且流量不被确认。
漏桶还有两种不同的基础实现方式:基于计数器、基于队列。
基于计数器的实现(Meter,仪表,计量器)。计数器的值代表了水位,流量到达时计数器增加相当于加水,计数器以固定速度减小相当于漏水。加水成功流量才被放行。这种实现接近于上述算法。
在基于队列的实现中,桶以固定长度队列的形式实现,流量流入队列、遵循FIFO,在另一端以固定速度流出。这和计数器实现的一个核心区别是,只要桶不空,流量将被严格塑形为固定的速度,并消除任何突发情况。需要注意的是,这种塑形过于严格,会给流量带来微小的时间位移,在一些实时网络传输场景中这会引发问题。
总结
令牌桶与漏桶在算法底层其实没有差别,二者更倾向于是一类算法的镜像实现:令牌桶提前加水、流量到达时取水,漏桶流量到达时加水、主动漏水。甚至,漏桶本身的两种实现方式,也有不同的特性。在后面的代码实现case中,我们能看到更多的实现层面的差异。
2. 进阶限流算法:GCRA
GCRA为通用信元速率算法(generic cell rate algorithm)是对漏桶算法(计数器模式,而非队列模式)的改进,在实现上更加优雅、能够直接应用于生产。GCRA有两种等价的描述方式:漏桶模式和虚拟调度模式。漏桶模式的描述更便于我们入门理解,虚拟调度模式更加优雅、便于深入理解和实现。
漏桶模式
连续状态漏桶continuous state leaky算法定义了一个有限容量的桶,桶以每时间单位1的速度排水,并在每次确认请求后注水T。如果请求到达时桶内水位(X')小于等于τ,则确认请求,否则拒绝请求:
- 请求到达时,根据上一次的水位X、上一次请求确认时间LCT、本次请求到达时间t,计算出新水位X'
- 新水位X'超过限制τ,则拒绝请求
- 新水位X'小于等于τ,则确认请求,并结合请求带来的注水量T计算最终水位
- 计算最终水位时,如果之前很久没有请求到达,X'可能为负,此时要取X'=max(0,X')
- 注意算法是先确认请求再注水,所以桶的容量(计数器上限)为T+τ
虚拟调度模式
虚拟调度模式中,算法已经很难看到原始漏桶的影子了。算法通过比较请求的"理论到达时间(TAT,Theoretical Arrival Time )"与实际到达时间t来判定其能否被确认:
- 算法定义了T,预期的请求间隔时间(可以推导出预期请求速度是1/T)
- 算法定义了容忍度τ,即请求可以早于TAT最多τ时间
- t<TAT-τ则说明请求到达的过早,不能被确认
- 请求被确认时,算法更新下一次的TAT=max(t,TAT)+T
- 注意计算TAT时的max操作,这是为了配合容忍度τ:请求可能早于TAT,但没有早于TAT-τ。
总结
GCRA算法是漏桶的生产改进版本。简单学习漏桶、令牌桶算法后,似乎我们在实现时需要一个旁路的定时器取漏水或注水。但实际生产中,没有什么语言能方便地支持这种并发实现,哪怕是go可以用goroutinue做到,性能也极其有限。反倒是时钟信息,集合任何系统、语言都然支持。所以GCRA才是真的能拿到生产上应用的算法。