tokenlimit

本节将通过令牌桶限流(tokenlimit)来介绍其基本使用。

使用

  1. const (
  2. burst = 100
  3. rate = 100
  4. seconds = 5
  5. )
  6. store := redis.NewRedis("localhost:6379", "node", "")
  7. fmt.Println(store.Ping())
  8. // New tokenLimiter
  9. limiter := limit.NewTokenLimiter(rate, burst, store, "rate-test")
  10. timer := time.NewTimer(time.Second * seconds)
  11. quit := make(chan struct{})
  12. defer timer.Stop()
  13. go func() {
  14. <-timer.C
  15. close(quit)
  16. }()
  17. var allowed, denied int32
  18. var wait sync.WaitGroup
  19. for i := 0; i < runtime.NumCPU(); i++ {
  20. wait.Add(1)
  21. go func() {
  22. for {
  23. select {
  24. case <-quit:
  25. wait.Done()
  26. return
  27. default:
  28. if limiter.Allow() {
  29. atomic.AddInt32(&allowed, 1)
  30. } else {
  31. atomic.AddInt32(&denied, 1)
  32. }
  33. }
  34. }
  35. }()
  36. }
  37. wait.Wait()
  38. 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 控制的几个关键属性:

argument mean
ARGV[1] rate 「每秒生成几个令牌」
ARGV[2] burst 「令牌桶最大值」
ARGV[3] now_time「当前时间戳」
ARGV[4] get token nums 「开发者需要获取的token数」
KEYS[1] 表示资源的tokenkey
KEYS[2] 表示刷新时间的key
  1. -- 返回是否可以活获得预期的token
  2. local rate = tonumber(ARGV[1])
  3. local capacity = tonumber(ARGV[2])
  4. local now = tonumber(ARGV[3])
  5. local requested = tonumber(ARGV[4])
  6. -- fill_time:需要填满 token_bucket 需要多久
  7. local fill_time = capacity/rate
  8. -- 将填充时间向下取整
  9. local ttl = math.floor(fill_time*2)
  10. -- 获取目前 token_bucket 中剩余 token
  11. -- 如果是第一次进入,则设置 token_bucket 数量为 令牌桶最大值
  12. local last_tokens = tonumber(redis.call("get", KEYS[1]))
  13. if last_tokens == nil then
  14. last_tokens = capacity
  15. end
  16. -- 上一次更新 token_bucket 的时间
  17. local last_refreshed = tonumber(redis.call("get", KEYS[2]))
  18. if last_refreshed == nil then
  19. last_refreshed = 0
  20. end
  21. local delta = math.max(0, now-last_refreshed)
  22. -- 通过当前时间与上一次更新时间的跨度,以及生产token的速率,计算出新的token
  23. -- 如果超过 max_burst,多余生产的token会被丢弃
  24. local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
  25. local allowed = filled_tokens >= requested
  26. local new_tokens = filled_tokens
  27. if allowed then
  28. new_tokens = filled_tokens - requested
  29. end
  30. -- 更新新的token数,以及更新时间
  31. redis.call("setex", KEYS[1], ttl, new_tokens)
  32. redis.call("setex", KEYS[2], ttl, now)
  33. return allowed

上述可以看出 lua script :只涉及对 token 操作,保证 token 生产合理和读取合理。

函数分析

令牌桶限流 - 图1

从上述流程中看出:

  1. 有多重保障机制,保证限流一定会完成。
  2. 如果redis limiter失效,至少在进程内rate limiter兜底。
  3. 重试 redis limiter 机制保证尽可能地正常运行。

总结

go-zero 中的 tokenlimit 限流方案适用于瞬时流量冲击,现实请求场景并不以恒定的速率。令牌桶相当预请求,当真实的请求到达不至于瞬间被打垮。当流量冲击到一定程度,则才会按照预定速率进行消费。

但是生产token上,不能按照当时的流量情况作出动态调整,不够灵活,还可以进行进一步优化。此外可以参考Token bucket WIKI 中提到分层令牌桶,根据不同的流量带宽,分至不同排队中。

参考