开源推荐 go-zero 如何扛住流量冲击(二)

hxl · 2020年11月23日 · 最后由 hxl 回复于 2020年11月26日 · 935 次阅读
本帖已被设为精华帖!

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

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

使用

const (
    burst   = 100
    rate    = 100
    seconds = 5
)

store := redis.NewRedis("localhost:6379", "node", "")
fmt.Println(store.Ping())
// New tokenLimiter
limiter := 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 int32
var wait sync.WaitGroup
for 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 控制的几个关键属性:

argument mean
ARGV[1] rate「每秒生成几个令牌」
ARGV[2] burst「令牌桶最大值」
ARGV[3] now_time「当前时间戳」
ARGV[4] get token nums「开发者需要获取的 token 数」
KEYS[1] 表示资源的 tokenkey
KEYS[2] 表示刷新时间的 key
-- 返回是否可以活获得预期的token

local 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 = capacity
end

-- 上一次更新 token_bucket 的时间
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
    last_refreshed = 0
end

local 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 >= requested
local new_tokens = filled_tokens
if allowed then
    new_tokens = filled_tokens - requested
end

-- 更新新的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中提到分层令牌桶,根据不同的流量带宽,分至不同排队中。

参考

如果觉得文章不错,欢迎 github 点个 star 🤝 。同时欢迎大家使用 go-zerohttps://github.com/tal-tech/go-zero

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 11月23日 07:59

目测了下 lua 的部分,由于 now 参数是 redis 之外的程序传入的,好像存在一些工程风险(服务所在机器存在时差),建议改成 lua 里面取时间值,如图

只是目测了下,没有实际跑代码测试,不一定正确,可以讨论一下

帖子没有提及这个 lua 是用预加载 sha 调用还是每次传入,性能差别较大,而且预加载 sha 的话,重连时会失效要考虑 reload

lua 部分也可以考虑优化下,比如两个 key value 改成一个,因为 ttl 是一样的,改成一个,取出来 split 一下,少一个 key value 以及 lua 跟 redis 部分的交互能好些

还有就是对于高并发的系统,限流的时间单位通常是秒级的,对应的频率通常也较高,单个令牌生成的时间间隔如果是 go 或者其他语言服务的单进程内误差较小,但是 go 或者其他进程如果调用 redis、这个通信 +cpu 之类的消耗也是误差较高的、限流效果会导致请求量被勒紧了,还不如简单粗暴 1 秒内判断次数、不判断 delta,那样的话实现起来也更简单

pastor 回复

对,我看 go-redis 的里面的设计是在 lua 里面 now()

pastor 回复

可以可以,最近也在看其他人的设计,lua script 上很多写法差别。很感谢提的建议,我测试一下

不客气,多多交流。 刚看了下你们 github 上的仓库,是每次 Eval 的,这样每次都要传输完整脚本、redis 解析执行 lua,传输和 lua 解释的性能损失比较大,可以参考下 redis_rate 这种 script = redis.NewScript("..."),用的时候 script.Run 的方式,Run 里面是先 EvalSha 失败再 Eval,Eval 后 redis 里有存储、后续 EvalSha 就可以成功了,减少不必要的网络传输和 lua 的重复解释执行:

https://github.com/go-redis/redis_rate/blob/v9/lua.go#L7

https://github.com/go-redis/redis_rate/blob/v9/rate.go#L99

pastor 回复

这个之前还没注意,确实两个 limiter 在使用 lua 上都存在这个问题,包括 lua script 这个设计的比较简单,可能没考虑更加严苛的情况。好的,我去看看 redis_rate 实现,好好学习一下 十分感谢您。

pastor [该话题已被删除] 中提及了此贴 11月26日 08:55
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册