uber-go漏桶限流器使用与原理分析

转载自:uber-go漏桶限流器使用与原理分析

uber在Github上开源了一套用于服务限流的go语言库ratelimit, 该组件基于Leaky Bucket(漏桶)实现。

我在之前写过《Golang限流器time/rate实现剖析》,讲了Golang标准库中提供的基于Token Bucket实现限流组件的time/rate原理,同时也讲了限流的一些背景。

相比于TokenBucket,只要桶内还有剩余令牌,调用方就可以一直消费。而Leaky Bucket相对来说比较严格,调用方只能严格按照这个间隔顺序进行消费调用。(实际上,uber-go对这个限制也做了一些优化,具体可以看下文详解)

还是老规矩,在正式讲其实现之前,我们先看下ratelimit的使用方法。

ratelimit的使用

我们直接看下uber-go官方库给的例子:

rl := ratelimit.New(100) // per second

prev := time.Now()
for i := 0; i < 10; i++ {
  now := rl.Take()
  fmt.Println(i, now.Sub(prev))
  prev = now
}

在这个例子中,我们给定限流器每秒可以通过100个请求,也就是平均每个请求间隔10ms。
因此,最终会每10ms打印一行数据。输出结果如下:

// Output:
// 0 0
// 1 10ms
// 2 10ms
// 3 10ms
// 4 10ms
// 5 10ms
// 6 10ms
// 7 10ms
// 8 10ms
// 9 10ms

基本实现

要实现以上每秒固定速率的目的,其实还是比较简单的。

在ratelimit的New函数中,传入的参数是每秒允许请求量(RPS)。
我们可以很轻易的换算出每个请求之间的间隔:

limiter.perRequest = time.Second / time.Duration(rate)

以上limiter.perRequest指的就是每个请求之间的间隔时间。

如下图,当请求1处理结束后, 我们记录下请求1的处理完成的时刻, 记为limiter.last
稍后请求2到来, 如果此刻的时间与limiter.last相比并没有达到perRequest的间隔大小,那么sleep一段时间即可。

[图片上传失败...(image-4b37d-1574514204743)]

对应ratelimit的实现代码如下:

sleepFor = t.perRequest - now.Sub(t.last)
if sleepFor > 0 {
    t.clock.Sleep(sleepFor)
    t.last = now.Add(sleepFor)
} else {
    t.last = now
}

最大松弛量

我们讲到,传统的Leaky Bucket,每个请求的间隔是固定的,然而,在实际上的互联网应用中,流量经常是突发性的。对于这种情况,uber-go对Leaky Bucket做了一些改良,引入了最大松弛量(maxSlack)的概念。

我们先理解下整体背景: 假如我们要求每秒限定100个请求,平均每个请求间隔10ms。但是实际情况下,有些请求间隔比较长,有些请求间隔比较短。如下图所示:

[图片上传失败...(image-81ea99-1574514204743)]

请求1完成后,15ms后,请求2才到来,可以对请求2立即处理。请求2完成后,5ms后,请求3到来,这个时候距离上次请求还不足10ms,因此还需要等待5ms。

但是,对于这种情况,实际上三个请求一共消耗了25ms才完成,并不是预期的20ms。在uber-go实现的ratelimit中,可以把之前间隔比较长的请求的时间,匀给后面的使用,保证每秒请求数(RPS)即可。

对于以上case,因为请求2相当于多等了5ms,我们可以把这5ms移给请求3使用。加上请求3本身就是5ms之后过来的,一共刚好10ms,所以请求3无需等待,直接可以处理。此时三个请求也恰好一共是20ms。
如下图所示:

[图片上传失败...(image-994a4e-1574514204743)]

在ratelimit的对应实现中很简单,是把每个请求多余出来的等待时间累加起来,以给后面的抵消使用。

t.sleepFor += t.perRequest - now.Sub(t.last)
if t.sleepFor > 0 {
  t.clock.Sleep(t.sleepFor)
  t.last = now.Add(t.sleepFor)
  t.sleepFor = 0
} else {
  t.last = now
}

注意:这里跟上述代码不同的是,这里是+=。而同时t.perRequest - now.Sub(t.last)是可能为负值的,负值代表请求间隔时间比预期的长。

t.sleepFor > 0,代表此前的请求多余出来的时间,无法完全抵消此次的所需量,因此需要sleep相应时间, 同时将t.sleepFor置为0。

t.sleepFor < 0,说明此次请求间隔大于预期间隔,将多出来的时间累加到t.sleepFor即可。

但是,对于某种情况,请求1完成后,请求2过了很久到达(好几个小时都有可能),那么此时对于请求2的请求间隔now.Sub(t.last),会非常大。以至于即使后面大量请求瞬时到达,也无法抵消完这个时间。那这样就失去了限流的意义。

为了防止这种情况,ratelimit就引入了最大松弛量(maxSlack)的概念, 该值为负值,表示允许抵消的最长时间,防止以上情况的出现。

if t.sleepFor < t.maxSlack {
  t.sleepFor = t.maxSlack
}

ratelimit中maxSlack的值为-10 * time.Second / time.Duration(rate), 是十个请求的间隔大小。我们也可以理解为ratelimit允许的最大瞬时请求为10。

高级用法

ratelimit的New函数,除了可以配置每秒请求数(QPS), 其实还提供了一套可选配置项Option。

func New(rate int, opts ...Option) Limiter

Option的类型为type Option func(l *limiter), 也就是说我们可以提供一些这样类型的函数,作为Option,传给ratelimit, 定制相关需求。

但实际上,自定义Option的用处比较小,因为limiter结构体本身就是个私有类型,我们并不能拿它做任何事情。

我们只需要了解ratelimit目前提供的两个配置项即可:

WithoutSlack

我们上文讲到ratelimit中引入了最大松弛量的概念,而且默认的最大松弛量为10个请求的间隔时间。

但是确实会有这样需求场景,需要严格的限制请求的固定间隔。那么我们就可以利用WithoutSlack来取消松弛量的影响。

limiter := ratelimit.New(100, ratelimit.WithoutSlack)

WithClock(clock Clock)

我们上文讲到,ratelimit的实现时,会计算当前时间与上次请求时间的差值,并sleep相应时间。
在ratelimit基于go标准库的time实现时间相关计算。如果有精度更高或者特殊需求的计时场景,可以用WithClock来替换默认时钟。

通过该方法,只要实现了Clock的interface,就可以自定义时钟了。

type Clock interface {
    Now() time.Time
    Sleep(time.Duration)
}
clock &= MyClock{}
limiter := ratelimit.New(100, ratelimit.WithClock(clock))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容