-->

左耳听风_050_49_弹力设计篇之限流设计

你好,我是陈浩网名做耳朵house.今天这节课呢我们来讲限流设计保护系统不会在过载的情况下出现问题,我们呢就需要限流。

我们在一些系统中呢都可以看到这样的设计。

比如我们的数据库访问的连接池,还有我们的线程池,还有engines下面的用于限制瞬时并发连接数的limit connect模块,限制每秒平均速率的limit request模块,还有限制MQ的生产速度等等。

那首先呢我们来谈一谈限流的策略。

那限流的目的呢是通过对并发访问进行限速。

那相关的策略呢啊一般都是一旦达到限制的速率,那么我们呢就会触发相应的限流行为。

那一般来说呢触发的限流行为啊有这么几个。

那第一个呢是拒绝服务,就是把多出来的请求啊给拒绝掉。

那一般来说啊,好的限流系统在受到流量暴增的时候,会统计当前哪个客户端来的请求,最多啊直接拒掉这个客户端。

那这种行为啊可以把一些不正常的或者是带有恶意的高病发粉啊挡在门外。

那第二个呢是服务降级啊,就是关闭,或者把后端服务啊做降级处理。

那这样呢可以让服务有足够的资源来处理更多的请求。

那降级呢有很多方式。

那一种呢是把一些不重要的服务啊给停掉,把CPU内存或者数据的资源让给更重要的功能。

那另一种呢是不再返回全量数据,只返回部分数据。

因为全量数据啊需要做circle join操作,那部分的数据呢则不需要,那所以呢就可以让circle执行的更快。

那还有的最快的一种啊是直接返回预设的缓存,以牺牲一致性的方式来获得更大的性能吞吐。

那第三个呢是特权请求。

那所谓特权请求呢,意思就是说资源不够了,我只能把有限的资源分给重要的用户,比如分给权利更高的VIP用户。

在多租户的系统下限流的时候啊,应该保大客户的。

所以大客户呢有特权可以优先处理,而其他的非特权用户啊就得让路了。

那第五个呢是延时处理,在这种情况下呢,一般都会有一个队列来缓冲大量的请求。

那这个队列如果满了,那么就只能拒绝用户了。

那如果这个队列中的任务超时了啊,也要返回系统繁忙的错误了。

那使用缓冲队列呢只是为了减缓压力,一般呢用于应对短暂的封刺请求。

那最后呢第六个行为是弹性伸缩,动用自动化运维的方式,对相应的服务啊做自动化的伸缩。

那这个呢就需要一个应用性能的监控系统,能够感知到目前最繁忙的top五的服务啊是哪几个?然后呢去伸缩,他们还需要一个自动化的发布部署和服务注册的运维系统。

而且呢还要快啊,越快越好,否则呢系统会被压死掉了。

那当然呢如果是数据库的压力过大,弹性伸缩应用啊是没什么用的那这个时候呢还是应该限流好了,那接下来呢我们就来讲一下限流的实现方式。

那第一个实现方式呢是计数器方式。

最简单的限流算法啊,就是维护一个计数器counter.当一个请求过来的时候去做加一操作。

当一个请求处理完之后呢,就做减一操作。

那如果这个counter大于某个数了,比如我们设定的限流阈值,那么呢就开始拒绝请求啊,以保护系统负载了。

那这个算法足够的简单粗暴。

那第二个实践方式呢是队列算法。

在这个算法下面啊,请求的速度是可以波动的,而处理的速度呢则是非常匀速的那这个算法呢其实有点像一个先进先出的算法啊,也就是FIFO.我在文中呢给你展示了这个FIFO的队列图,我们呢还可以扩展出一些别的玩法。

那一个呢是有优先级的队列,处理的时候呢,先处理高优先级的队列,然后呢再处理低优先级的队列。

比如文中的这张图,它表示的就是只有高优先级的队列被处理完成之后啊,才会处理低优先级的队列。

有优先级的队列啊,可能会导致低优先级的队列长时间得不到处理。

那为了避免低优先级的队列被饿死,那一般来说啊是分配不同比例的处理时间到不同的队列上。

于是呢我们就有了带权重的队列。

我们再来看文中的第三张图,有三个队列的权重分布是三比二比一。

那这个呢就意味着我们需要在权重为三的这个队列上处理三个请求之后,再去权重为二的队列上处理两个请求。

最后呢再去权重为一的队列上处理一个请求。

如此反复,那队列流控呢是以队列的方式啊来处理请求。

那如果处理过慢啊,就会导致队列满而开始触发限流,但这样的算法需要用队列长度来控制流量,在配置上比较难以操作。

那如果队列过长导致后端符啊,在队列没有满的时候啊就挂掉了。

那一般来说啊这样的模型不能做push,而是方式啊会好一些。

那限流的第三个实现方式呢是漏斗算法,漏斗算法可以参看维基百科的相关词条。

在这里呢,我在文中给你放了一张漏斗算法的示意图,我们可以看到就像一个漏斗一样进来的水量。

就好像访问流量一样,而出去的水量呢,就像是我们的系统处理请求一样。

那当访问流量过大的时候呢,这个漏斗中啊就会积水,如果水太多的啊就会溢出。

那一般来说啊这个漏斗是用一个队列来实现的当请求,过多的时候队列呢就会开始积压请求。

那如果队列满了,就会开始拒绝请求。

嗯,很多系统啊都有这样的设计,比如TCP.当请求的数量过多的时候呢,就会有一个sink backlock队列啊来缓冲请求,或者是TCP的滑动窗口啊,也是用于流控的队列。

从文中这个算法流程图中呢,我们可以看到漏斗算法其实就是在队列请求中啊,加上一个限流器,来让processor以一个均匀的速度去处理请求。

那限流的第四个实现方式呢是令排桶算法。

那关于令牌桶算法呢啊主要是有一个中间人,他在一个桶内啊按照一定的速率放入一些token,然后呢处理程序要处理请求的时候,需要拿到头肯才能处理。

呃,如果拿不到呢就不处理。

我同样用了一张图啊,清楚的描述了这个算法。

从理论上来说呢,并排统的算法和漏斗算法不一样的地方在于漏堵。

算法中呢处理请求是一个常量和恒定的速度去处理的。

而另排同算法则是在流量小的时候啊,攒钱流量大的时候呢就可以快速处理。

但是呢我们可能会问processor的处理速度,因为有队列的存在啊,所以他总是能以最大处理能力来处理请求。

这呢也是我们所希望的方式。

所以令排桶和漏斗呢都是受制于processor的最大处理能力的。

无论令牌桶里面有多少令牌啊,也不论队列中还有多少请求。

总之呢processor在大流量来临的时候,总是按照自己最大的处理能力来处理的。

但是呢试想一下,如果我们的processor只是一个非常简单的任务分配器,比如像n这x这样的啊,基本没有什么业务逻辑的网关,那么它的处理速度啊一定会很快啊,不会有什么瓶颈,而把它用来把请求转发给后端服务。

那么在这种情况下呢,这两个算法就有不一样的情况了。

漏堵算法呢会以一个稳定的速度去转发。

而令牌桶算法呢平时流量不大时啊,在攒钱在流量大的时候呢,可一次发出队列里面的请求,而后呢就受到令牌桶的流控限制了。

另外呢令牌桶还可能做成第三方的一个服务。

那这样呢可以在分布式的系统中啊对全节进行流扣,那这个呢也是一种很好的方式。

那限流的第五个实现方式呢是基于响应时间的动态限流。

那前面提到的算法呢有一个不好的地方,就是需要设置一个确定的限流值。

那这个呢就要求我们在每次发布的时候啊,都要做相应的性能测试,找到系统最大的性能值。

那当然呢性能测试并不是很容易做的,有关性能测试的方法呢,请看看我在酷shall的一篇文章,叫做性能测试,应该怎么做啊,虽然性能测试比较不容易,但是呢还是应该要做的。

但是呢在很多时候啊,我们却并不知道这个限流值,或者说很难给出一个合适的值。

那基本呢会有这样一些因素。

首先呢在实际情况下,很多服务呢都会依赖于数据库。

所以不同的用户请求啊会对不同的数据集进行操作,就算是相同的请求,可能数据集啊也不一样。

比如现在很多应用啊都会有一个时间线,非得流不同的用户关心的主题仍然不一样。

但数据呢也不一样,而且数据库的数据呢是在不断的变化的。

可能前两天性能还行,但是因为数据量增加导致性能变差。

那在这种情况下,我们就很难给出一个确定的一成不变的值。

因为惯形数据库对于同条色后语句的执行时间啊,其实是不可预测的那另外呢不同的API有不同的性能,我们要在线上为每一个API配置,不同的限流制。

那这一点呢太难配置啊也很难管理。

而且现在的服务啊都是能够自动化伸缩的,不同大小的集群性能啊也不一样。

所以呢在自动化伸缩的情况下,我们就要动态的去调整限流的预制。

那这一点啊太难做到了。

那基于上述的这些原因啊,我们现有的值啊是很难被静态的设成一个恒定的一个值的那所以呢我们想使用一种动态限流的方式。

那这种方式呢不再设定一个特定的流控制,而是能够动态的感知系统的压力来自动化的限流。

那这方面涉及的典范呢是TCP协议的拥塞控制的算法。

那TCP呢使用RTT来探测网络的延时和性能,从而呢设定相应的滑动窗口的大小,让发送的速率和网络的性能相匹配。

那这个算法呢是非常精妙的。

我们完全可以借鉴在我们的流控技术中,我们呢记录下每次调用后端请求的响应时间。

然后在一个时间区间内的请求呢去计算一个响应时间的p九零或者p九九值啊,也就是把过去十秒之内的请求的响应时间啊排个序,然后呢看百分之九十或者百分之九十九的位置啊是多少。

那这样呢我们就知道了有多少显求大于某个响应时间。

那如果这个p九零或者p九九啊超过我们设定的阈值了,那么呢我们就自动限流。

那这个设计中啊有几个要点。

首先呢你需要计算一定时间内的p九零或者p九九。

在有大量请求的情况下,这个啊非常的耗内存,也非常的耗CPU.因为需要对大量的数据啊进行排序。

那解决方案呢有两种,一种呢是不记录所有的请求啊采样就好了。

那另一种呢是使用一个叫蓄水池的进入算法。

那关于这个算法呢,我在这里啊就不多说了啊,编程主机里讲过这个算法啊,你也可以自行google,英文呢叫resther var amplay.那其次呢,这种动态流况需要像TCP那样,你需要记录一个当前的QPS.那如果发现后端的p九零或者p九九响应太慢了,那么呢就可以把这个QPS啊减半,然后像TCP一样走慢启动的方式啊,直到又开始变慢。

然后呢减去四分之一的QPS啊,再慢启动,然后呢再减去八分之一的QPS啊,以此类推。

那这个过程呢有点像一个阻尼运行的过程,然后呢整个限流的流量啊会在一个值上下做小幅震动。

那这么做的目的呢在于,如果后端扩容伸缩后性能变好,那么系统啊会自动适应后端的最大性能。

那另外呢这种动态性能的方式啊实现起来并不容易。

大家呢可以看一下TCP的算法。

那TCP相关的一些算法呢,我写在了酷shl上面的TCP的那些事儿啊,这篇文章中你可以用来做参考来实现。

我在现在创业中呢easy gateway的产品中呢也实现了这个算法。

那前面说的这些呢就是限流的几种实现方式。

最后呢我们来谈一谈限流的设计要点。

腺流呢主要是有四个目的。

那第一呢是为了向用户承诺SLA,我们需要保证我们的系统啊在某个速度下的响应时间和可用性。

那第二呢也可以用来阻止在多租户的情况下,某一个用户把资源耗尽,而让所有的用户都无法访问的问题。

那第三个目的呢是为了应对突发的流量。

而第四个目的呢是节约成本。

我们啊不会为了一个不常见的尖峰啊,把我们的系统扩容到最大的尺寸。

我们要在有限的资源下能够承受比较高的流量。

另外呢在设计上我们还要有以下的考量。

那第一呢限流啊应该是在架构的早期来考虑。

当架构形成之后呢,限流啊就不是很容易的加入。

那第二呢限流模块性能必须要好,而且对流量的变化也是非常灵敏的,否则太过迟钝的限流系统早因为过载而挂掉了。

那第三呢,限流啊应该有一个手动的开关,那这样在应急的时候呢就可以手动操作。

那第四呢,当限流发生的时候啊,应该有一个监控事件通知,让我们知道有限流事件发生了。

那这样呢运维人员啊就可以及时跟进,而且呢还可以自动化触发扩容或者降级啊,以缓解系统压力。

那第五呢,当限流发生的时候,对于聚焦的请求啊,我们应该返回一个特定的限流错误码。

那这样呢可以和其他错误区分开。

而客户端看到限流呢可以调整发送速度啊,或者走重试机制。

那第六呢限流啊,应该让后端的服务感知到。

当限流发生的时候啊,我们应该在协议头中啊塞进一个标志啊,比如HTP header中放入一个限流的级别,告诉后端符啊,目前正在限流中。

那这样呢后端符啊就可以根据这个标志来决定是否做降级。

好了,我们来总结一下今天分享的主要内容。

首先呢限流的目的是为了保护系统,不在过载的情况下导致问题。

那接着呢我们讲了几种限流的策略,然后呢想到限流的算法啊,包括计数器、队列、漏斗,还有令排桶。

那然后呢我们又讨论了如何基于响应时间来限流。

最后呢我总结了限流设计的要点。

在下节课中呢,我们会讲述降级设计啊,希望能对你有帮助。

也欢迎你分享一下你实现过怎样的限流机制呢?我在文末呢给出了分布式系统模式系列文章的目录啊,希望你能在这个列表里啊找到自己感兴趣的内容。