图解漏桶(LeakyBucket)限流器的实现原理

大家好,我是「Go学堂」的渔夫子,今天就来聊聊漏桶(LeakyBucket)的实现原理

leaky bucket也叫漏桶,就是将请求先放到一个桶中进行排队,然后按一个固定的速率来处理请求,即所谓的漏出。

桶具有一定的容量,即最大容量的请求数,当排队请求的数量超过桶的容量时,再进来的请求就直接过滤掉,不再被处理。换句话说就是请求先在桶中排队,系统或服务只以一个恒定的速度从桶中将请求取出进行处理。如果排队的请求数量超过桶能够容纳的最大数量,即桶装满了,则直接丢弃。

算法的实现有很多种,本文基于计数原理的实现来进行讲解。计数原理实际上就是当请求到来时,基于当前时间和预订的处理速率来计算该请求应该等待多长时间才能被处理

该算法主要的属性字段有以下三个:

  • 处理请求的速率rate。该值代表多久处理一个请求。实际上就是指处理完该请求后,要等待多久才能处理下一个请求。 比如我们初始化时指定服务每100ms处理一个请求,也就是每处理1个请求,需要等待100ms才能处理下一个请求。
  • 桶的最大容量capacity。该值代表我们最多允许多少个请求排队,超过该值,就直接返回,不用等待了。这个在生活中有很多类似场景:有一次我们去公园排队坐游船,排了很长的队伍。管理员过来告诉我们,只有前20个人能排上号,20号后面的就可以不用排了。
  • 桶中最后一个排队请求被处理的时间last。该值有两个作用:
    • 第一个作用是当有新请求进来的时候,就可以计算出新请求需要被处理的时间:last+rate
    • 第二个作用是根据last、当前时间t以及速率rate计算当前还有多少个请求等待被处理:
waitRequests = (last - t) / rate

根据上面几个关键属性字段,我们就可以对LeakBucket定义了,如下:

type LeakyBucket struct {
    rate int64 //处理请求的速率
    capacity int64 //桶的最大容量
  last time.Time //桶中最后一个排队请求被处理的时间
}

然后,LeakyBucket还有一个函数Limit函数:

func (t *LeakyBucket) Limit() (time.Duration, error) {

}

该函数的主要作用就是计算流入请求能够被处理的等待时间。针对该函数有以下两点说明:

  • 接收到的每个请求都需要调用该函数,每个调用一次就相当于有一个请求流入桶中。
  • 该函数返回值代表调用者要处理该请求需要等待的时长,调用者需要进行time.Sleep这么长时间才能进行处理,也就是通过Sleep控制了消耗的速度。

因此,我们可知请求流入和流出的流程如下:

image

请求是如何排队的?

假设现在LeakyBucket是一个空桶,按100ms处理一个请求的速率漏出,容量大小为5。现在同时有5个请求流入桶中,我们看看每个请求经过Limit是如何计算各自的预计处理时间以及等待时间的。

  • 第一个请求进来,不用等待,直接就会被处理。处理的时间就是last=当前时间t
  • 第二个请求,因为第一个请求刚刚排队并被处理了,那么就需要按处理速率等待,那么被处理的时间就是第一个请求被处理的时间+rate,即last=t+100ms
  • 第三个请求,因为第二个请求仍然在排队,所有应该在处理了第二个后,再等待100ms才能被处理,即last=第二个的处理时间+100ms=t+300ms
  • 第四、五个请求依次类推,如下图:
image

我们将上图转换在时间轴上看着会更直观一些:

image

以上情况是我们假设在空桶的状态下,同时流入了n个请求,从第2个请求开始,处理时间都在前一个请求的处理时间上+rate。

但如果新的请求流入是在最后一个请求流入后的50ms流入的,即last+50ms的时间点。那么新流入的请求被处理的时间就不应该是last+rate了,这样该请求的处理时间距离上次处理请求的时间就是:

(last+rate) - (last+50ms)= rate+50ms

这样计算的话就比rate多了50ms。那该如何计算呢? 实际的被处理时间应该是先计算当前时间距离上次被处理时间的间隔,然后再跟rate进行比较,看看比rate差多少,然后补全该差值即可,即:

delta = 当前时间t - last

last = 当前时间t + (rate - delta)

如下图所示:

image

如何计算桶满了?

当新请求流入后,发现桶已经满了,就不再排队了,而是直接被丢弃掉。那如何计算桶是否已经满了呢?其实就是根据当前时间now,和桶中排队的最后一个请求被处理的时间last之间的差值,再除以速率rate,就能计算出正在等待被处理的请求有多少个了,然后和capacity进行比较即可:

wait = (last - now) / rate
if wait > capacity {
  //表示桶已经满了
}else {
    //未满,还可以继续排队
}

这里桶的容量实际上是代表业务能够接受请求被处理的最大等待时间。比如一个web用户访问一个页面,最多愿意等10秒,那如果请求等待处理的时间是11秒,那即使等到被处理了,用户也已经流失了,也就失去了等待的意义。

好了,几个关键点介绍完了,下面我们直接贴上部分实现的代码,完整的代码可参考https://github.com/uber-go/ratelimithttps://github.com/mennanov/limiters/blob/master/leakybucket.go

type LeakyBucket struct {
    rate int64 //处理请求的速率
    capacity int64 //桶的最大容量
  last time.Time //桶中最后一个排队请求被处理的时间
  mu sync.Mutex
}

func (t *LeakyBucket) Limit(ctx context.Context) (time.Duration, error) {
    //这里进行加锁,保证每个请求按顺序依次处理
  t.mu.Lock()
    defer t.mu.Unlock()

  now := time.Now().UnixNano() //当前时间的纳秒数
    if now < t.last {
        // 说明已经有请求在排队了,那么新请求进来排队后被处理的时间就是rate后
        t.last += t.rate
    } else {
        // 说明为桶为空,也许是初始状态,也许是所有的请求都被处理完了.
        
    var offset int64 //代表等待处理该请求的时间需要等待多久
        delta := now - state.Last // 代表当前时间距离上次处理请求的时间过了多久
        if delta < t.rate {
      //说明还没有到下次处理请求的时间,则需要等待offset后才能到
            offset = t.rate - delta
        }
    //如果delta > t.rate 说明当前时间距离上次处理请求的时间已经超过了rate,offset为0,新的请求就应该被马上处理
        t.last = now + offset //更新该请求应该被处理的时间
    }

    wait := t.last - now //计算桶是否已经满了
    if wait/t.rate > t.capacity {
    //桶满了,返回error,调用者根据需要是直接丢弃还是等待wait长的时间。一般是直接丢弃。
   
    t.last = now - offset //因为这里要丢弃该请求,所有要保持新请求排队前的状态
        return time.Duration(wait), ErrLimitExhausted
    }
  
  //排队成功,返回要等待的时间给调用者,让调用者sleep进行阻塞就能实现按rate的速率处理请求了
    return time.Duration(wait), nil
}

总结

LeakyBucket的核心思想就是按固定的速率处理请求,所以不支持突增的流量。因为即使有再多的流量,也是按固定的速率被处理。算法的实现中本质上就是按固定的处理速率计算该请求能够被处理的时间以及需要等待的时间。

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

推荐阅读更多精彩内容