# periodlimit 不管是在单体服务中还是在微服务中,开发者为前端提供的API接口都是有访问上限的,当访问频率或者并发量超过其承受范围时候,我们就必须考虑限流来保证接口的可用性或者降级可用性。即接口也需要安装上保险丝,以防止非预期的请求对系统压力过大而引起的系统瘫痪。 `go-zero` 集成了开箱即用的 **限流器** 。其中内置了两种限流器,也对应两类使用场景: | **种类** | **原理** | **场景** | | ------------- | -------------------- | -------------------------------------------------- | | `periodlimit` | 单位时间限制访问次数 | 需要强行限制数据的传输速率 | | `tokenlimit` | 令牌桶限流 | 限制数据的平均传输速率,同时允许某种程度的突发传输 | 本文就来介绍一下 `periodlimit` 。 ## 使用 ```go const ( seconds = 1 total = 100 quota = 5 ) // New limiter l := NewPeriodLimit(seconds, quota, redis.NewRedis(s.Addr(), redis.NodeType), "periodlimit") // take source code, err := l.Take("first") if err != nil { logx.Error(err) return true } // switch val => process request switch code { case limit.OverQuota: logx.Errorf("OverQuota key: %v", key) return false case limit.Allowed: logx.Infof("AllowedQuota key: %v", key) return true case limit.HitQuota: logx.Errorf("HitQuota key: %v", key) // todo: maybe we need to let users know they hit the quota return false default: logx.Errorf("DefaultQuota key: %v", key) // unknown response, we just let the sms go return true } ``` ## periodlimit `go-zero` 采取 **滑动窗口** 计数的方式,计算一段时间内对同一个资源的访问次数,如果超过指定的 `limit` ,则拒绝访问。当然如果你是在一段时间内访问不同的资源,每一个资源访问量都不超过 `limit` ,此种情况是允许大量请求进来的。 而在一个分布式系统中,存在多个微服务提供服务。所以当瞬间的流量同时访问同一个资源,如何让计数器在分布式系统中正常计数? 同时在计算资源访问时,可能会涉及多个计算,如何保证计算的原子性? - `go-zero` 借助 `redis` 的 `incrby` 做资源访问计数 - 采用 `lua script` 做整个窗口计算,保证计算的原子性 下面来看看 `lua script` 控制的几个关键属性: | **argument** | **mean** | | ------------ | ----------------------------------------------- | | key[1] | 访问资源的标示 | | ARGV[1] | limit => 请求总数,超过则限速。可设置为 QPS | | ARGV[2] | window大小 => 滑动窗口,用 ttl 模拟出滑动的效果 | ```lua -- to be compatible with aliyun redis, -- we cannot use `local key = KEYS[1]` to reuse thekey local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) -- incrbt key 1 => key visis++ local current = redis.call("INCRBY", KEYS[1], 1) -- 如果是第一次访问,设置过期时间 => TTL = window size -- 因为是只限制一段时间的访问次数 if current == 1 then redis.call("expire", KEYS[1], window) return 1 elseif current < limit then return 1 elseif current == limit then return 2 else return 0 end ``` 至于上述的 `return code` ,返回给调用方。由调用方来决定请求后续的操作: | **return code** | **tag** | call code | **mean** | | --------------- | --------- | --------- | -------------- | | 0 | OverQuota | 3 | **over limit** | | 1 | Allowed | 1 | **in limit** | | 2 | HitQuota | 2 | **hit limit** | 下面这张图描述了请求进入的过程,以及请求触发 `limit` 时后续发生的情况: ![](https://static.gocn.vip/photo/2020/cefc03fe-988b-4192-853b-cbac53abd4e3.png?x-oss-process=image/resize,w_1920) ![](https://static.gocn.vip/photo/2020/98505b9a-7039-45e2-b020-2f6ba8284324.png?x-oss-process=image/resize,w_1920) ## 后续处理 如果在服务某个时间点,请求大批量打进来,`periodlimit` 短期时间内达到 `limit` 阈值,而且设置的时间范围还远远没有到达。后续请求的处理就成为问题。 `periodlimit` 中并没有处理,而是返回 `code` 。把后续请求的处理交给了开发者自己处理。 1. 如果不做处理,那就是简单的将请求拒绝 2. 如果需要处理这些请求,开发者可以借助 `mq` 将请求缓冲,减缓请求的压力 3. 采用 `tokenlimit`,允许暂时的流量冲击 所以下一篇我们就来聊聊 `tokenlimit` ## 总结 `go-zero` 中的 `periodlimit` 限流方案是基于 `redis` 计数器,通过调用 `redis lua script` ,保证计数过程的原子性,同时保证在分布式的情况下计数是正常的。 但是这种方案也存在缺点,因为它要记录时间窗口内的所有行为记录,如果这个量特别大的时候,内存消耗会变得非常严重。 ## 参考 - [go-zero periodlimit](https://github.com/tal-tech/go-zero/blob/master/core/limit/periodlimit.go) - [分布式服务限流实战,已经为你排好坑了](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673)