1、限流的策略
2、限流的算法:计数器、队列、漏斗和令牌桶。
3、如何基于响应时间来限流。
4、限流设计的要点
限流:数据库访问的连接池,还有我们的线程池,还有 nginx 下的用于限制瞬时并发连接数的 limit_conn 模块,限制每秒平均速率的 limit_req 模块,还有限制 MQ 的生产速,等等。
一、限流的策略
拒绝服务。流量暴增时,直接拒掉请求最多的客户端,把不正常或是恶意的高并发访问抵挡掉。
服务降级。(1)不重要的服务停掉,把 CPU、内存或是数据的资源让给更重要的功能;(2)不再返回全量数据,只返回部分数据。
全量数据需要做 SQL Join 操作,部分则不需要,所以可以让 SQL 执行更快,或直接返回预设的缓存,牺牲一致性获得更大的性能吞吐。
特权请求。保大客户的,优先处理,其它让路。
延时处理。一个队列来缓冲大量的请求,队列如果满了,只能拒绝用户,如果这个队列中的任务超时了,也要返回系统繁忙的错误了。使用缓冲队列只是为了减缓压力,一般用于应对短暂的峰刺请求。
弹性伸缩。监控系统,感知繁忙的 TOP 5 的服务是哪几个。
然后去伸缩它们,还需要一个自动化的发布、部署和服务注册的运维系统,而且还要快,越快越好。否则,系统会被压死掉了。当然,如果是数据库的压力过大,弹性伸缩应用是没什么用的,这个时候还是应该限流。
二、限流的实现方式
1、计数器方式
一个请求来加一,请求处理完减一Counter 大于限流阈值,开始拒绝请求以保护系统的负载了。
2、队列算法
请求的速度可以是波动的,处理速度均速。有点像 FIFO算法。
![](https://img.haomeiwen.com/i9930763/33fea66f09c74f2a.png)
在上面这个 FIFO 的队列上,我们可以扩展出一些别的玩法。
优先级的队列,高优先级,再处理低优先级。 如下图:
![](https://img.haomeiwen.com/i9930763/373aa8771b9ec807.png)
低优先级的队列被饿死,于是我们有了带权重的队列。下图:三个队列的权重分布是 3:2:1,权重 3 的这个队列上处理 3 个请求后,再去权重 2 的队列上处理 2 个请求,最后去 1 的队列上处理 1 个请求,如此返复。
![](https://img.haomeiwen.com/i9930763/d537d83fa23d9ce6.png)
队列流控是以队列的的方式来处理请求。如果处理过慢,那么就会导致队列满,而开始触发限流。
但是,这样的算法需要用队列长度来控制流量,在配置上比较难以操作。如果队列过长,导致后端服务在队列没有满时就挂掉了。一般来说,这样的模型不能做 push 的,而是要做 pull 方式的会好一些。
3、漏斗算法 Leaky Bucket
漏斗算法可以参看 Wikipedia 的相关词条Leaky Bucket。下图是一个漏斗算法的示意图。
![](https://img.haomeiwen.com/i9930763/9ee65d77da946e4a.png)
像一个漏斗一样,进来的水量就好像访问流量一样,而出去的水量就像是我们的系统处理请求一样。当访问流量过大时这个漏斗中就会积水,如果水太多了就会溢出。
“漏斗”是用一个队列来实现的,如果队列满了,就会开拒绝请求。很多系统都有这样的设计,比如 TCP。请求的数量过多时,就有一个 sync backlog 的队列来缓冲请求,或是 TCP 的滑动窗口也是用于流控的队列。
![](https://img.haomeiwen.com/i9930763/ab509242fa46a030.png)
漏斗算法其实就是在队列请求中加上一个限流器,来让 Processor 以一个均匀的速度处理请求。
4、令牌桶算法 Token Bucket
主要是有一个中间人。在一个桶内按照一定的速率放入一些 token,处理程序要处理请求时,需要拿到 token,才能处理;如果拿不到,则不处理。
下面这个图很清楚地说明了这个算法。
![](https://img.haomeiwen.com/i9930763/46b1fed49002e992.png)
漏斗算法中,处理请求是以一个常量和恒定的速度处理的,而令牌桶算法则是在流量小的时候“攒钱”,流量大的时候,快速处理。
如果 Processor 只是非常简单的任务分配器,比如像 Nginx 基本没有什么业务逻辑的网关,那么它的处理速度一定很快,不会有什么瓶颈,而其用来把请求转发给后端服务,这种情况下,这两个算法就有不一样的情况了:
漏斗算法:稳定的速度转发
令牌桶:流量大时,一次发出队列里请求,受到令牌桶的流控限制。还可能做成在分布式的系统中,对全局进行流控的第三方的一个服务
三、基于响应时间的动态限流
上面算法有个不好的地方,就是要设置确定的限流值。每次发布都要性能测试,找到最大的性能值。很难给出一个合适的值,因为:
1、服务会依赖于数据库。不同的用户请求,会对不同的数据集进行操作。相同的请求,可能数据集也不一样(比如,很多应用都会有一个时间线 Feed 流,不同的用户关心的主题不一样,数据也不一样)。
数据是在不断地变化的,可能前两天性能还行,因为数据量增加导致性能变差。很难给出一个确定的一成不变的值,关系型数据库对于同一条 SQL 语句的执行时间其实是不可预测的(NoSQL 的就比 RDBMS 的可预测性要好)。
2、不同的 API 有不同的性能。我们要在线上为每一个 API 配置不同的限流值,这点太难配置,也很难管理。
3、自动化伸缩的情况下,不同大小的集群的性能也不一样,我们要动态地调整限流的阈值,太难做到了。
想使用一种动态限流的方式。不再设定一个特定的流控值,而是能够动态地感知系统的压力来自动化地限流。
这方面设计的典范是 TCP 协议的拥塞控制的算法。TCP 使用 RTT - Round Trip Time 来探测网络的延时和性能,从而设定相应的“滑动窗口”的大小,以让发送的速率和网络的性能相匹配。
记录每次调用后端请求的响应时间,然后在一个时间区间内(比如,过去 10 秒)的请求计算一个响应时间的 P90 或 P99 值,把过去 10 秒内的请求的响应时间排个序,然后看 90% 或 99% 的位置是多少。
这样就知道有多少请求大于某个响应时间。如果这个 P90 或 P99 超过我们设定的阈值,那么我们就自动限流。
设计中要点:
计算的一定时间内的 P90 或 P99。大量请求下,非常地耗内存和 CPU,因为要对大量数据排序。
解决方案有两种,一种是不记录所有的请求,采样就好了,另一种是使用一个叫蓄水池的近似算法。关于这个算法这里我不就多说了,《编程珠玑》里讲过这个算法,你也可以自行 Google,英文叫Reservoir Sampling。
这种动态流控需要像 TCP 那样,你需要记录一个当前的 QPS. 如果发现后端的 P90/P99 响应太慢,那么就可以把这个 QPS 减半,然后像 TCP 一样走慢启动的方式,直接到又开始变慢,然后减去 1/4 的 QPS,再慢启动,然后再减去 1/8 的 QPS……
这个过程有点像个阻尼运行的过程,然后整个限流的流量会在一个值上下做小幅振动。这么做的目的是,后端扩容伸缩后性能变好,自动适应后端的最大性能。
四、限流的设计要点
限流目的:
1、为了向用户承诺 SLA。保证某个速度下的响应时间以及可用性。
2、阻止多租户的情况下,某一用户把资源耗尽,而让所有的用户都无法访问的问题。
3、应对突发的流量。
4、节约成本。不为不常见的尖峰扩容到最大的尺寸。有限的资源下能够承受比较高的流量。
设计上的考量:
1、早期考虑。当架构形成后,限流不是很容易加入。
2、限流模块必需是非常好的性能,而且对流量的变化也是非常灵敏的,否则太过迟钝的限流,系统早因为过载而挂掉了。
3、手动的开关,应急。
4、监控事件通知。运维人员可以及时跟进。还可以自动化触发扩容或降级。
5、返回限流错误码。和其它错误区分开来。客户端看到限流,可以调整发送速度,或走重试机制。
6、让后端的服务感知到。比如 HTTP Header 中,放入一个限流的级别,告诉后端服务目前正在限流中。这样,后端服务可以根据标识决定是否做降级。
也欢迎你分享一下你实现过怎样的限流机制?
网友评论