本篇文章承接上一篇go-zero 如何扛住流量冲击(一)。

上一篇介绍的是 go-zero 中滑动窗口限流,本篇介绍另外一个 tokenlimit ,令牌桶限流。

应用

const (    burst   = 100    rate    = 100    seconds = 5)store := redis.NewRedis("localhost:6379", "node", "")fmt.Println(store.Ping())// New tokenLimiterlimiter := limit.NewTokenLimiter(rate, burst, store, "rate-test")timer := time.NewTimer(time.Second * seconds)quit := make(chan struct{})defer timer.Stop()go func() {  <-timer.C  close(quit)}()var allowed, denied int32var wait sync.WaitGroupfor i := 0; i < runtime.NumCPU(); i++ {  wait.Add(1)  go func() {    for {      select {        case <-quit:          wait.Done()          return        default:          if limiter.Allow() {            atomic.AddInt32(&allowed, 1)          } else {            atomic.AddInt32(&denied, 1)          }      }    }  }()}wait.Wait()fmt.Printf("allowed: %d, denied: %d, qps: %d\n", allowed, denied, (allowed+denied)/seconds)

tokenlimit

从整体上令牌桶生产token逻辑如下:

  • 用户配置的均匀发送速率为r,则每隔1/r秒一个令牌被退出到桶中;
  • 假如桶中最多能够寄存b个令牌。如果令牌达到时令牌桶曾经满了,那么这个令牌会被抛弃;
  • 当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑;

go-zero 在两类限流器下都采取 lua script 的形式,依赖redis能够做到分布式限流,lua script同时能够做到对 token 生产读取操作的原子性。

上面来看看 lua script 管制的几个要害属性:

argumentmean
ARGV[1]rate 「每秒生成几个令牌」
ARGV[2]burst 「令牌桶最大值」
ARGV[3]now_time「以后工夫戳」
ARGV[4]get token nums 「开发者须要获取的token数」
KEYS[1]示意资源的tokenkey
KEYS[2]示意刷新工夫的key
-- 返回是否能够活取得预期的tokenlocal rate = tonumber(ARGV[1])local capacity = tonumber(ARGV[2])local now = tonumber(ARGV[3])local requested = tonumber(ARGV[4])-- fill_time:须要填满 token_bucket 须要多久local fill_time = capacity/rate-- 将填充工夫向下取整local ttl = math.floor(fill_time*2)-- 获取目前 token_bucket 中残余 token 数-- 如果是第一次进入,则设置 token_bucket 数量为 令牌桶最大值local last_tokens = tonumber(redis.call("get", KEYS[1]))if last_tokens == nil then    last_tokens = capacityend-- 上一次更新 token_bucket 的工夫local last_refreshed = tonumber(redis.call("get", KEYS[2]))if last_refreshed == nil then    last_refreshed = 0endlocal delta = math.max(0, now-last_refreshed)-- 通过以后工夫与上一次更新工夫的跨度,以及生产token的速率,计算出新的token数-- 如果超过 max_burst,多余生产的token会被抛弃local filled_tokens = math.min(capacity, last_tokens+(delta*rate))local allowed = filled_tokens >= requestedlocal new_tokens = filled_tokensif allowed then    new_tokens = filled_tokens - requestedend-- 更新新的token数,以及更新工夫redis.call("setex", KEYS[1], ttl, new_tokens)redis.call("setex", KEYS[2], ttl, now)return allowed

上述能够看出 lua script :只波及对 token 操作,保障 token 生产正当和读取正当。

函数剖析

从上述流程中看出:

  1. 有多重保障机制,保障限流肯定会实现。
  2. 如果redis limiter生效,至多在过程内rate limiter兜底。
  3. 重试 redis limiter 机制保障尽可能地失常运行。

总结

go-zero 中的 tokenlimit 限流计划实用于刹时流量冲击,事实申请场景并不以恒定的速率。令牌桶相当预申请,当实在的申请达到不至于霎时被打垮。当流量冲击到肯定水平,则才会依照预约速率进行生产。

然而生产token上,不能依照过后的流量状况作出动静调整,不够灵便,还能够进行进一步优化。此外能够参考Token bucket WIKI中提到分层令牌桶,依据不同的流量带宽,分至不同排队中。

参考

  • go-zero tokenlimit
  • Go-Redis 提供的分布式限流库

如果感觉文章不错,欢送 github 点个star ????

同时欢送大家应用 go-zero ,https://github.com/tal-tech/g...

我的项目地址:
https://github.com/tal-tech/go-zero