高并发限流之令牌桶算法

Token Bucket RateLimiter

Posted by alovn on July 13, 2019

业务中有时需要做一些优惠活动, 经常会带来较大流量,面对突然到来的爆发流量,除了缓存、消息队列、系统扩容、降级之外,也要考虑做限流来保障系统的稳定运行,避免整个系统崩溃。被限流的消息将直接被丢弃,并回复”系统繁忙,请稍后再试”。生活中也能看到相似的场景:家里的电闸一般会安装上保险丝,一旦有大功率用电,超出负载保险丝就会烧断,保护电器不被烧坏。同理我们也需要为我们的系统安装上『保险丝』。

常见的限流算法

常见的限流算法有两种:漏桶算法(Leaky Bucket) 和 令牌桶算法(Token Bucket)。

漏桶算法

漏桶算法的思路比较简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大就会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

leaky-bucket 上面为漏桶算法的示意图,漏桶算法可以很好的限制容量池的大小,从而防止流量暴增。

令牌桶算法

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

token-bucket token-bucket2 令牌桶算法通过发放令牌,根据令牌的频率来做请求频率限制,容量限制等。

两种算法的区别

两者主要区别在于“漏桶算法”能够强行限制数据的传输速率,由于限制了传输速率,当面对突发流量时会大量拒绝。

而“令牌桶算法”在能够限制数据的平均传输速率外,可以允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,所以它适合于具有突发特性的流量。

Golang中的令牌桶算法

Java中常用的是Google开源的 guava。在Golang中Google有提供实现:

1
go get golang.org/x/time/rate

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
    "net/http"

    "golang.org/x/time/rate"
)

var limiter = rate.NewLimiter(2, 5)

func limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            var httpCode = http.StatusTooManyRequests
            http.Error(w, http.StatusText(httpCode), httpCode)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", helloHandler)
    http.ListenAndServe(":3000", limit(mux))
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello"))
}

源码实现可以查看 rate.NewLimiter。 参数配置r为发送速率,每秒会有r个令牌放入桶中,桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌就会被丢弃。

示例代码: golang-token-bucket-ratelimit

这是一个全局的限制,实际应用中也会常见基于IP地址、ApiKey或用户ID标识进行限制的。

参考资料

  1. 维基百科:Token_bucket
  2. 维基百科:Leaky_bucket