为什么要限流
按照服务的调用方,服务类型分为对内和对外两类
对外服务
比如对外开放的 API。这类服务导致机器挂掉的可能情况有
- 用户增长过快
- 热点事件发生(微博)
- 恶意刷接口
这些情况在开发阶段是无法预知的,不知道什么时候会发生大量请求的涌入。
对内服务
比如对内的 RPC 服务。若一个 RPC 服务 A 被多个服务 BCDE 调用,当服务 B 涌入大量流量导致 A 挂掉,那么 CDE 也无法正常提供服务。
当然,这些都和我无关 。因为我的 web 服务既没有大量用户 ,也没有人会来刷接口 ,但我就是想做一个限流中间件来丰富我的 lib 包。
常见的限流算法
计数器算法
计数器算法简单粗暴,设置 1s 内的请求数阈值 qps。比如 qps = 100,从第一个请求进来开始计时,并将计数值置 0,在接下来的1s 内,每来一个请求,就把计数值加 1,如果计数值达到100,那后面的请求就全部拒绝。等到 1s 结束,把计数恢复为 0,重新开始计数。
缺点
如果在某个 1s 内的前 10ms,已经通过了100个请求,那后面的 990ms,就只能眼巴巴的把请求拒绝。这种现象被称为突刺现象。
漏桶算法
为了消除突刺现象,可以设置一个漏桶(漏斗)。请求进来,相当于水倒入漏桶,然后从下面小口缓慢匀速的流出,不管上面的流量多大,下面流量始终不变。
所以我们需要设置两个参数:桶最大容量 cap 和漏出速率 r。若当前进来的请求数达到 cap,即漏桶满了,那后面进来的请求就拒绝掉;若 r = 50,则表示每秒处理 50 个请求,即 20ms 处理一个请求。
实现的话,可以用队列作为漏桶,然后拿另外一个线程以速率 r 从队列中拿请求来处理。一次可以拿多个请求实现并发。
缺点
无法应对短时间的突发流量。
令牌桶算法
令牌桶算法中想象了一种令牌和令牌桶。令牌桶容量为 b,令牌以速率 r 产生,比如 r = 2 表示每秒产生 2 个令牌,即每 500ms 产生一个。当大量请求进来时,每个请求会消耗一个令牌,当令牌被消耗时,让后面的请求处于等待状态或直接将其拒绝掉。是不是很简单?
实现的话,不需要实现令牌,也不需要实现令牌桶,只需要有一个变量记录令牌数,当请求进来时,判断令牌数是否为零,来决定处理还是拒绝,若处理的话要让令牌数 -1。另外需要一个线程来只要令牌数小于 b 就以 r 的速率让令牌数 +1。
缺点
过于优秀。
全局令牌桶
我们可以通过在一个 Web 服务接口前加一个令牌桶来限制该接口的所有的请求。甚至在路由根部添加一个令牌桶来限制对该路由下所有服务的请求。
即用一个桶来控制一处流量。
用户令牌桶
现在我们想为每个用户设置一个令牌桶来限制每个用户的请求。当一个用户的请求进来时,只会使得他自己的令牌数减一。这样就需要为每个桶设置唯一识别的名称,可以是用户 IP 或者是用户的 UID。对每个用户设置一个令牌桶的好处是可以检测该用户是否在短时间内发起了大量的请求,若在一段时间内该用户被拒绝服务的次数达到某个阈值,就将其 ban 掉,让他一个月或者一年内的请求直接被拒绝。但为每个用户创建一个令牌桶也带来一个问题,随着用户的增长,令牌桶个数越来越多,占用过多机器的资源,使得主要服务性能下降甚至挂掉。
为解决令牌桶过多的问题,尝试采用 LRU 机制。即将所有令牌桶放入一个固定长度的链表中。
当链表未满时,新用户请求进来后,为该用户创建新的令牌桶,再将该桶插入链表头部。后面该用户每发起一次请求,就只需把它的桶移到链表头部。
当链表满了,新用户的请求进来,还是先为该用户创建新的令牌桶然后再将该桶插入链表头部,再移除链表尾部的桶。后面该用户每发起一次请求,就只需把它的桶移到链表头部。
这样就让令牌桶的个数固定不变了。
但有几点是需要注意的
- 判断是否是新用户就是通过看链表中是否有他 IP 标示的桶。
- 为了使整个过程的时间复杂度为 O(1),必须用一个Hash表和一个循环双向链表组合来实现令牌桶的存取。
- 由于每个请求属于不同的线程(Go 中属于不同的协程),所以需要对令牌桶的存取加锁
路由令牌桶
将用户令牌桶的 IP 标识换成请求路径就行了。
xxx 令牌桶
将用户令牌桶的 IP 标识换成 xxx 就行了。
最后我们需要将我们的令牌桶写成接口中间件放到合适的位置就行了。
实现
利用 Golang 的实现源码
测试
用 ab 压测工具测试了使用了上面实现的限流中间件的接口。
测试参数
- 总请求个数:100
- 单位并发数:10
- 令牌桶容量:20
- 令牌产生率:1
测试结果
可以看到每秒能处理 5k 多个请求。
接口日志
因为测试参数中令牌桶的容量为 20 且每秒产生一个令牌,所以我们期待一秒内最多会有 20 个请求被处理。
可以看到在 1 秒内前 21 个请求被处理,剩下的请求被实现的限流中间件拒绝,并返回的 429 状态码。多了一个请求被处理是因为新的用户发起的请求是先被处理再创建该用户的令牌桶的。
参考
接口中的几种限流实现-云栖社区-阿里云yq.aliyun.com我的博客地址
XJJ's Labxjj.pub记录一些平时的想法,有兴趣的可以逛逛。