Skip to content
This page has been auto-translated and may contain errors.View in English

漏桶算法

一个底部有洞的桶。水以任意速率倒进去,却只能以稳定的速率从洞里流出。倒得比漏得快,桶就会被填满;再继续倒,就会溢出。

水就是请求,洞就是你的处理速率。

漏桶会把到达的请求放进一个队列,再以固定速率处理它们,所以无论请求到达时有多不均匀,最终打到服务器上的流量都是平滑的。

看着它被填满又排空

容量为五个请求,每秒处理两个,从空桶开始:

到达排队处理之后队列中被丢弃
144220
24已有3个,加起来共5个231
300210
400100

第二秒正是容量吃紧的地方。此时四个请求到达,队列里已经有两个,只有三个能放得下,第八个请求直接被丢弃。

由此可以得出两个特性:

  • 输出速率永远不变。 不管到达了八个请求还是一个都没有,都是每秒两个。
  • 队列遵循先进先出。 最早到达的请求会最先被处理。

令牌桶说:你想什么时候花掉令牌都行。漏桶说:排队去,我会按我的节奏处理你。

Juno看着它被填满又排空 和之前所有算法相比,最大的不同在于:忙碌时到达的请求不会被拒绝,而是要等待。

只有当队列本身被填满时,请求才会被丢弃,所以漏桶是在吸收一波突发流量,而不是拒绝它。

Juno看着它被填满又排空 等待是实实在在的成本,而且这个成本落在客户端身上。一个请求排在另外四个请求后面,每秒处理两个,那它就要等两秒才能轮到自己开始处理。

从调用方的角度看,这和服务器很慢没有任何区别。

所以排队的请求仍然需要设置超时。如果不给等待时间设一个上限,一波突发流量就会变成一堆早已放弃、却还占着连接的客户端。

Juno看着它被填满又排空 实测数据:一个容量为20、每秒漏出5个的桶,25个请求同时到达,结果是接受了20个、丢弃了5个,其中第20个请求要等4秒才被处理。

最后这个数字才是设计时真正要考虑的。队列满了,加上排空速度又慢,排在最后的请求就要等很久,而4秒早已超过大多数客户端重试或放弃的时限。

请求没有被拒绝,不代表这份容量就是白占的。

这也说明,容量应该根据可接受的延迟来定,而不是凭内存大小拍脑袋。以每秒5个的速率来算,如果最坏情况要控制在2秒以内,队列应该设为10,而不是20。

它的优势和劣势

优势劣势
把突发流量平滑成恒定的输出速率对合理的流量峰值没有任何灵活性
队列能防止下游过载把系统稳定性置于用户体验之上
简单易懂,容易实现即便服务器本可以应付,客户端仍要等待
无论来多少请求,负载都是可预测的延迟会随队列深度增长

这些劣势其实是同一种权衡从不同角度看到的结果。漏桶通过让客户端等待来保护服务器,哪怕服务器当时还绰绰有余,它也照样让客户端等。

这适合网络带宽管理、视频流传输,以及需要平稳处理请求的服务器场景——在这些场景里,可预测的速率比对突发流量的快速响应更重要。

Juno它的优势和劣势 这张表里的每一行都源自同一个决定:输出速率是固定的,不会自我调整。

当你需要可预测的负载时,这就是它的优势;而当客户端在等待一个服务器本可以立刻处理的请求时,这就成了它的劣势。

Juno它的优势和劣势 当你要保护的东西无法承受突发流量时——比如一个有自身限流的下游 API、一个并发一高就会性能下降的数据库、一个按调用次数收费的支付服务商——这时候就该用漏桶。

而当你要保护的东西能扛得住突发流量、你又希望客户端感觉响应迅速时,就该用令牌桶。关键问题不在限流器前面是什么,而在它后面保护的是什么。

Juno它的优势和劣势 严格的先进先出这一点,其实值得商榷。在持续过载的情况下,它会造成队头阻塞:一个慢请求会拖慢它后面所有请求,哪怕那些请求本身处理起来很轻量。

这也意味着,一个把队列灌满的客户端,其优先级会一直高于稍后才发来单个请求的客户端——单靠漏桶,客户端之间是没有公平性可言的。按客户端分别设置桶可以解决这个问题,但也会成倍增加你要维护的状态。

还值得指出它和另一种东西的相似之处:漏桶本质上就是一个带速率限制的有界工作队列,限制作用在处理它的worker上。

如果你已经在用带并发限制的任务队列,那你其实已经拥有了漏桶的大部分能力,问题就变成了:这个限流器到底该放在那里,还是放在请求处理路径上。

动手试一试

一个容量为5、每秒漏出2个请求的漏桶,从空桶开始。

  1. 六个请求同时到达。每个请求会怎样?
  2. 如果第六个到达的请求能被处理,它要等多久才会被处理?
  3. 同样是这六个请求,但改成每秒到达一个,分散着来。会发生什么?
  4. 对于一个调用按请求次数收费的支付服务商的接口,哪种算法更合适,为什么?
对照一下你的答案
#答案原因
1五个进入队列,一个被丢弃由于是瞬间涌入,队列能容纳五个,且此时还没来得及漏出任何一个
2它根本进不了队列被丢弃的正是第六个。第五个是最后一个被接受的,按每秒两个的速率,它要等两秒半
3全部六个都被处理,没有丢弃,也不用等待每秒一个的到达速率比每秒两个的漏出速率慢,所以队列始终没有堆积起来
4漏桶瓶颈在下游,而且每次调用都要花钱,所以你需要的是可预测的对外请求速率。用令牌桶的话,一波突发请求会直接变成一波突发扣费

第三题是最值得记住的一点:所有这些算法对限额之内的流量都不会做任何处理,它们只在边界处才会显现出作用。

接下来往哪走

一共五种算法,每一种在请求超出限度时,最终都是无声地施加拒绝或等待。

节流(Throttling)让这种等待变得刻意而且可见——在客户端接近限额时逐渐放慢它,而不是等到达到限额才拒绝它,然后把这两种做法整合成一套体系。