写点什么

go-zero 如何扛住流量冲击(一)

用户头像
Kevin Wan
关注
发布于: 2020 年 11 月 18 日

不管是在单体服务中还是在微服务中,开发者为前端提供的 API 接口都是有访问上限的,当访问频率或者并发量超过其承受范围时候,我们就必须考虑限流来保证接口的可用性或者降级可用性。即接口也需要安装上保险丝,以防止非预期的请求对系统压力过大而引起的系统瘫痪。


go-zero 集成了开箱即用的 限流器 。其中内置了两种限流器,也对应两类使用场景:


| 种类 | 原理 | 场景 |

| ------ | ---------- | -------------------------- |

| periodlimit | 单位时间限制访问次数 | 需要强行限制数据的传输速率 |

| tokenlimit | 令牌桶限流 | 限制数据的平均传输速率,同时允许某种程度的突发传输 |


本文就来介绍一下 periodlimit

使用


const (    seconds = 1    total   = 100    quota   = 5)// New limiterl := NewPeriodLimit(seconds, quota, redis.NewRedis(s.Addr(), redis.NodeType), "periodlimit")
// take sourcecode, err := l.Take("first")if err != nil { logx.Error(err) return true}
// switch val => process requestswitch 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 借助 redisincrby 做资源访问计数

  • 采用 lua script 做整个窗口计算,保证计算的原子性


下面来看看 lua script 控制的几个关键属性:


| argument | mean |

| ------------ | --------------------- |

| key[1] | 访问资源的标示 |

| ARGV[1] | limit => 请求总数,超过则限速。可设置为 QPS |

| ARGV[2] | window 大小 => 滑动窗口,用 ttl 模拟出滑动的效果 |


-- to be compatible with aliyun redis, -- we cannot use `local key = KEYS[1]` to reuse thekeylocal 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 1elseif current < limit then    return 1elseif current == limit then    return 2else    return 0end
复制代码


至于上述的 return code ,返回给调用方。由调用方来决定请求后续的操作:


| return code | tag | call code | mean |

| --------- | ------- | ------ | ------|

| 0 | OverQuota | 3 | over limit |

| 1 | Allowed | 1 | in limit |

| 2 | HitQuota | 2 | hit limit |


下面这张图描述了请求进入的过程,以及请求触发 limit 时后续发生的情况:




后续处理


如果在服务某个时间点,请求大批量打进来,periodlimit 短期时间内达到 limit 阈值,而且设置的时间范围还远远没有到达。后续请求的处理就成为问题。


periodlimit 中并没有处理,而是返回 code 。把后续请求的处理交给了开发者自己处理。


  1. 如果不做处理,那就是简单的将请求拒绝

  2. 如果需要处理这些请求,开发者可以借助 mq 将请求缓冲,减缓请求的压力

  3. 采用 tokenlimit,允许暂时的流量冲击


所以下一篇我们就来聊聊 tokenlimit


总结


go-zero 中的 periodlimit 限流方案是基于 redis 计数器,通过调用 redis lua script ,保证计数过程的原子性,同时保证在分布式的情况下计数是正常的。


但是这种方案也存在缺点,因为它要记录时间窗口内的所有行为记录,如果这个量特别大的时候,内存消耗会变得非常严重。


参考



同时欢迎大家使用 go-zero 并加入我们!


项目地址


https://github.com/tal-tech/go-zero


如果觉得文章不错,欢迎 **github** 点个 star 👏


发布于: 2020 年 11 月 18 日阅读数: 473
用户头像

Kevin Wan

关注

保持简单 2017.10.24 加入

go-zero作者

评论 (1 条评论)

发布
用户头像
这部分功能和Sentinel-golang是否可以对接呢
2021 年 03 月 12 日 16:43
回复
没有更多了
go-zero 如何扛住流量冲击(一)