0 为什么需要限流
保护系统不会在过载的情况下出现问题,我们就需要限流。 一些限流应用的例子,线程池如果线程数超过了最大线程数,则进入饱和策略,这就是一种限流的设计。
1 限流的策略
限流的时候可以做的事情
- 拒绝服务。把多出来的请求拒绝掉。一般来说,好的限流系统在受到流量暴增时,会统计当前哪个客户端来的请求最多,直接拒掉这个客户端,这种行为可以把一些不正常的或者是带有恶意的高并发访问挡在门外。
- 服务降级。关闭或是把后端服务做降级处理。这样可以让服务有足够的资源来处理更多的请求。降级有很多方式,一种是把一些不重要的服务给停掉,把 CPU、内存或是数据的资源让给更重要的功能;一种是不再返回全量数据,只返回部分数据。因为全量数据需要做 SQL Join 操作,部分的数据则不需要,所以可以让 SQL 执行更快,还有最快的一种是直接返回预设的缓存,以牺牲一致性的方式来获得更大的性能吞吐。
- 特权请求。所谓特权请求的意思是,资源不够了,我只能把有限的资源分给重要的用户,比如:分给权利更高的 VIP 用户。在多租户系统下,限流的时候应该保大客户的,所以大客户有特权可以优先处理,而其它的非特权用户就得让路了。
- 延时处理。在这种情况下,一般会有一个队列来缓冲大量的请求,这个队列如果满了,那么就只能拒绝用户了,如果这个队列中的任务超时了,也要返回系统繁忙的错误了。使用缓冲队列只是为了减缓压力,一般用于应对短暂的峰刺请求。
- 弹性伸缩。动用自动化运维的方式对相应的服务做自动化的伸缩。这个需要一个应用性能的监控系统,能够感知到目前最繁忙的 TOP 5 的服务是哪几个。然后去伸缩它们,还需要一个自动化的发布、部署和服务注册的运维系统,而且还要快,越快越好。否则,系统会被压死掉了。当然,如果是数据库的压力过大,弹性伸缩应用是没什么用的,这个时候还是应该限流。
2 限流的实现
2.1 计数器
维护一个计数器,这个计数器在一定时间段内会进行累计。如果超过了设定的阈值,就进行限流操作。
2.2 队列
在这个算法下,请求的速度可以是波动的,而处理的速度则是非常均速的。
在以上算法基础上扩展出优先级队列。这个算法有个缺点是低优先级队列的请求可能被饿死。
以上算法的基础上,每个队列设置权重。 如下图所示。有三个队列的权重分布是 3:2:1,这意味着我们需要在权重为 3 的这个队列上处理 3 个请求后,再去权重为 2 的队列上处理 2 个请求,最后再去权重为 1 的队列上处理1个请求,如此反复。
队列流控是以队列的的方式来处理请求。如果处理过慢,那么就会导致队列满,而开始触发限流。但是,这样的算法需要用队列长度来控制流量,在配置上比较难操作。如果队列过长,导致后端服务在队列没有满时就挂掉了。
2.3 漏斗 Leaky Bucket
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
漏斗算法的实现是在队列的基础上加上一个限流器,关键在于使队列的数据能够匀速地消费。
2.4 令牌桶 Token Bucket
关于令牌桶算法,主要是有一个中间人。在一个桶内按照一定的速率放入一些 token,然后,处理程序要处理请求时,需要拿到 token,才能处理;如果拿不到,则不处理。下面这个图很清楚地说明了这个算法。
从理论上来说,令牌桶的算法和漏斗算法不一样的是,漏斗算法中,处理请求是以一个常量和恒定的速度处理的,而令牌桶算法则是在流量小的时候“攒钱”,流量大的时候,可以快速处理。
2.5 基于响应时间的动态限流
以上限流算法都需要设定一个静止恒定的限流阈值。这个阈值怎么产生,就需要通过性能测试的方式产生,但是服务的性能数据是一个动态变化的值,可能一个月之前做的压测,一个月后这个数据就不能用了。
我们想使用一种动态限流的方式。这种方式,不再设定一个特定的流控值,而是能够动态地感知系统的压力来自动化地限流。
我们记录下每次调用后端请求的响应时间,然后在一个时间区间内(比如,过去 10 秒)的请求计算一个响应时间的 P90 或 P99 值,也就是把过去 10 秒内的请求的响应时间排个序,然后看 90% 或 99% 的位置是多少。这样,我们就知道有多少请求大于某个响应时间。如果这个 P90 或 P99 超过我们设定的阈值,那么我们就自动限流。
用响应时间来衡量服务当前的性能相对于qps而言是比较可靠的。响应时间三九线,意思是 99.9 %的请求在当前时间段平均的响应时间,五九线就是 99.999 %的请求在当前时间段的响应时间。