分布式系统关注点(9)——想通关「限流」?只要这一篇

若是这是第二次看到个人文章,欢迎点击文末连接订阅个人我的公众号(跨界架构师)哟~

每周五11:45 按时送达。固然了,也会时不时加个餐~nginx

本文长度为3319字,建议阅读9分钟。程序员


可能你在网上看过很多「限流」相关的文章,可是z哥的这篇多是最全面,最深刻浅出的一篇了(容我飘几秒~)。数据库

开个玩笑,但愿你能收获一些增量价值就好~。后端


以前有了解到z哥的一部分读者们没有充分搞清楚「限流」和「熔断」的关系。咱们先来思考一个问题,生活中也有限流,为何国庆春节长假热门景点要限流?而不是一早先开几小时,若是人多了就关几小时,人少了就再开呢?其实这就是限流和熔断表象上的一个区别。数组


在上一篇中咱们聊到了「熔断」(分布式系统关注点——99%的人都能看懂的「熔断」以及最佳实践),有熔断机制的系统,它对可用性的做用至少保证了不会全盘崩溃。缓存


可是你能够想象一个稍微极端一点的场景,若是系统流量不是很稳定,致使频繁触发熔断的话,是否是意味着系统一直熔断的三种状态中不断切换。微信

致使的结果是每次从开启熔断到关闭熔断的期间,必然会致使大量的用户没法正常使用。系统层面的可用性大体是这样的。网络

另外,从资源利用率上也会很容易发现,波谷的这段时期资源是未充分利用的。架构

因而可知,光有熔断是远远不够的。并发

在高压下,只要系统没宕机,若是能将接收的流量持续保持在高位,但又不超过系统所能承载的上限,会是更有效率的运做模式,由于会将这里的波谷填满。

在现在的互联网已经做为社会基础设施的大环境下,上面的这个场景其实离咱们并非那么远,同时也会显得没那么极端。例如,层出不穷的营销玩法,一个接着一个的社会热点,以及互联网冰山之下的黑产、刷子的蓬勃发展,更加使得这个场景变的那么的须要去考虑、去顾忌。由于随时都有可能会涌入超出你预期的流量,而后压垮你的系统。

那么限流的做用就很显而易见了:只要系统没宕机,系统只是由于资源不够,而没法应对大量的请求,为了保证有限的系统资源可以提供最大化的服务能力,于是对系统按照预设的规则进行流量(输出或输入)限制的一种方法,确保被接收的流量不会超过系统所能承载的上限。


1、怎么作「限流」

从前面聊到的内容中咱们也知道,限流最好能“限”在一个系统处理能力的上限附近,因此:

  1. 经过「压力测试」等方式得到系统的能力上限在哪一个水平是第一步。

  2. 其次,就是制定干预流量的策略。好比标准该怎么定、是否只注重结果仍是也要注重过程的平滑性等。

  3. 最后,就是处理“被干预掉”的流量。能不能直接丢弃?不能的话该如何处理?


得到系统能力的上限

第一步不是咱们此次内容的重点,提及来就是对系统作一轮压测。能够在一个独立的环境进行,也能够直接在生产环境的多个节点中选择一个节点做为样原本压测,固然须要作好与其余节点的隔离。

通常咱们作压测为了得到2个结果,「速率」和「并发数」。前者表示在一个时间单位内可以处理的请求数量,好比xxx次请求/秒。后者表示系统在同一时刻能处理的最大请求数量,好比xxx次的并发。从指标上须要得到「最大值」、「平均值」或者「中位数」。后续限流策略须要设定的具体标准数值就是从这些指标中来的。

题外话:从精益求精的角度来讲,其余的诸如cpu、网络带宽以及内存的耗用也能够做为参照因素。


制定干预流量的策略

经常使用的策略就4种,我给它起了一个简单的定义——「两窗两桶」。两窗就是:固定窗口、滑动窗口,两桶就是:漏桶、令牌桶。

固定窗口

固定窗口就是定义一个“固定”的统计周期,好比1分钟或者30秒、10秒这样。而后在每一个周期统计当前周期中被接收到的请求数量,通过计数器累加后若是达到设定的阈值就触发「流量干预」。直到进入下一个周期后,计数器清零,流量接收恢复正常状态。

这个策略最简单,写起代码来也没几行。

全局变量 int totalCount = 0; //有一个「固定周期」会触发的定时器将数值清零。

if(totalCount > 限流阈值) {

return; //不继续处理请求。

}

totalCount++;

// do something...

固定窗口有一点须要注意的是,假如请求的进入很是集中,那么所设定的「限流阈值」等同于你须要承受的最大并发数。因此,若是须要顾忌到并发问题,那么这里的「固定周期」设定的要尽量的短。由于,这样的话「限流阈值」的数值就能够相应的减少。甚至,限流阈值就能够直接用并发数来指定。好比,假设固定周期是3秒,那么这里的阈值就能够设定为「平均并发数*3」。

不过无论怎么设定,固定窗口永远存在的缺点是:因为流量的进入每每都不是一个恒定的值,因此一旦流量进入速度有所波动,要么计数器会被提早计满,致使这个周期内剩下时间段的请求被“限制”。要么就是计数器计不满,也就是「限流阈值」设定的过大,致使资源没法充分利用

「滑动窗口」能够改善这个问题。


滑动窗口

滑动窗口其实就是对固定窗口作了进一步的细分,将原先的粒度切的更细,好比1分钟的固定窗口切分为60个1秒的滑动窗口。而后统计的时间范围随着时间的推移同步后移。

同时,咱们还能够得出一个结论是:若是固定窗口的「固定周期」已经很小了,那么使用滑动窗口的意义也就没有了。举个例子,如今的固定窗口周期已是1秒了,再切分到毫秒级别能反而得不偿失,会带来巨大的性能和资源损耗。

滑动窗口大体的代码逻辑是这样:

全局数组 链表[] counterList = new 链表[切分的滑动窗口数量];

//有一个定时器,在每一次统计时间段起点须要变化的时候就将索引0位置的元素移除,并在末端追加一个新元素。

int sum = counterList.Sum();

if(sum > 限流阈值) {

return; //不继续处理请求。

}

int 当前索引 = 当前时间的秒数 % 切分的滑动窗口数量;

counterList[当前索引]++;

// do something...

虽说滑动窗口能够改善这个问题,可是本质上仍是预先划定时间片的方式,属于一种“预测”,意味着几乎确定没法作到100%的物尽其用。

可是,「桶」模式能够作的更好,由于「桶」模式中多了一个缓冲区(桶自己)。


漏桶

首先聊聊「漏桶」吧。漏桶模式的核心是固定“出口”的速率,无论进来多少许,出去的速率一直是这么多。若是涌入的量多到桶都装不下了,那么就进行「流量干预」。

整个实现过程咱们来分解一下。

  1. 控制流出的速率。这个其实可使用前面提到的两个“窗口”的思路来实现。若是当前速率小于阈值则直接处理请求,不然不直接处理请求,进入缓冲区,并增长当前水位。

  2. 缓冲的实现能够作一个短暂的休眠或者记录到一个容器中再作异步的重试。

  3. 最后控制桶中的水位不超过最大水位。这个很简单,就是一个全局计数器,进行加加减减。

这样一来,你会发现本质就是:经过一个缓冲区将不平滑的流量“整形”成平滑的(高于均值的流量暂存下来补足到低于均值的时期),以此最大化计算处理资源的利用率

实现代码的简化表示以下:

全局变量 int unitSpeed; //出口当前的流出速率。每隔一个速率计算周期(好比1秒)会触发定时器将数值清零。

全局变量 int waterLevel; //当前缓冲区的水位线。

if(unitSpeed < 速率阈值) {

unitSpeed++;

//do something...

}

else{

if(waterLevel > 水位阈值){

return; //不继续处理请求。

}

waterLevel++;

while(unitSpeed >= 速率阈值){

sleep(一小段时间)。

}

unitSpeed++;

waterLevel--;

//do something...

}

更优秀的「漏桶」策略已经能够在流量的总量充足的状况下发挥你所预期的100%处理能力,但这还不是极致。

你应该知道,一个程序所在的运行环境中,每每不仅仅只有这个程序自己,会存在一些系统进程甚至是其它的用户进程。也就是说,程序自己的处理能力是会被干扰的,是会变化的。因此,你能够预估某一个阶段内的平均值、中位数,但没法预估具体某一个时刻的程序处理能力。又所以,你必然会使用相对悲观的标准去做为阈值,防止程序超负荷。

那么从资源利用率来讲,有没有更优秀的方案呢?有,这就是「令牌桶」。


令牌桶

令牌桶模式的核心是固定“进口”速率。先拿到令牌,再处理请求,拿不到令牌就被「流量干预」。所以,当大量的流量进入时,只要令牌的生成速度大于等于请求被处理的速度,那么此刻的程序处理能力就是极限

也来分解一下它的实现过程。

  1. 控制令牌生成的速率,并放入桶中。这个其实就是单独一个线程在不断的生成令牌。

  2. 控制桶中待领取的令牌水位不超过最大水位。这个和「漏桶」同样,就是一个全局计数器,进行加加减减。

大体的代码简化表示以下(看上去像「固定窗口」的反向逻辑):

全局变量 int tokenCount = 令牌数阈值; //可用令牌数。有一个独立的线程用固定的频率增长这个数值,但不大于「令牌数阈值」。

if(tokenCount == 0){

return; //不继续处理请求。

}

tokenCount--;

//do something...

聪明的你可能也会想到,这样一来令牌桶的容量大小理论上就是程序须要支撑的最大并发数。的确如此,假设同一时刻进入的流量将令牌取完,可是程序来不及处理,将会致使事故发生。

因此,没有真正完美的策略,只有合适的策略。所以,根据不一样的场景可以识别什么是最合适的策略是更须要锻炼的能力。下面z哥分享一些我我的的经验。


2、作「限流」的最佳实践

四种策略该如何选择?

首先,固定窗口。通常来讲,如非时间紧迫,不建议选择这个方案,太过生硬。可是,为了能快速止损眼前的问题能够做为临时应急的方案。

其次,滑动窗口。这个方案适用于对异常结果「高容忍」的场景,毕竟相比“两窗”少了一个缓冲区。可是,胜在实现简单。

而后,漏桶。z哥以为这个方案最适合做为一个通用方案。虽然说资源的利用率上不是极致,可是「宽进严出」的思路在保护系统的同时还留有一些余地,使得它的适用场景更广。

最后,令牌桶。当你须要尽量的压榨程序的性能(此时桶的最大容量必然会大于等于程序的最大并发能力),而且所处的场景流量进入波动不是很大(不至于一瞬间取完令牌,压垮后端系统)。


分布式系统中带来的新挑战

一个成熟的分布式系统大体是这样的。

每个上游系统均可以理解为是其下游系统的客户端。而后咱们回想一下前面的内容,可能你发现了,前面聊的「限流」都没有提到究竟是在客户端作限流仍是服务端作,甚至看起来更倾向是创建在服务端的基础上作。可是你知道,在一个分布式系统中,一个服务端自己就可能存在多个副本,而且还会提供给多个客户端调用,甚至其自身也会做为客户端角色。那么,在如此交错复杂的一个环境中,该如何下手作限流呢?个人思路是经过「一纵一横」来考量。


都知道「限流」是一个保护措施,那么能够将它想象成一个盾牌。另外,一个请求在系统中的处理过程是链式的。那么,正如古时候军队打仗同样,盾牌兵除了有小部分在老大周围保护,剩下的全在最前线。由于盾的位置越前,能受益的范围越大


分布式系统中最前面的是什么?接入层。若是你的系统有接入层,好比用nginx作的反向代理。那么能够经过它的ngx_http_limit_conn_module以及ngx_http_limit_req_module来作限流,是很成熟的一个解决方案。

若是没有接入层,那么只能在应用层以AOP的思路去作了。可是,因为应用是分散的,出于成本考虑你须要针对性的去作限流。好比ToC的应用必然比ToB的应用更须要作,高频的缓存系统必然比低频的报表系统更须要作,Web应用因为存在Filter的机制作起来必然比Service应用更方便。


那么应用间的限流究竟是作到客户端仍是服务端呢?

z哥的观点是,从效果上客户端模式确定是优于服务端模式的,由于当处于被限流状态的时候,客户端模式连创建链接的动做都省了。另外一个潜在的好处是,与集中式的服务端模式相比,能够把少数的服务端程序的压力分散掉。可是在客户端作成本也更高,由于它是去中心化的,假如须要多个节点之间的数据共通的话,是一个很麻烦的事情。

因此,最终z哥建议你:若是考虑成本就服务端模式,考虑效果就客户端模式。固然也不是绝对,好比一个服务端的流量大部分都来源于某一个客户端,那么就能够直接在这个客户端作限流,这也不失为一个好方案。


数据库层面的话,通常链接字符串中自己就会包含「最大链接数」的概念,就能够起到限流的做用。若是想作更精细的控制就只能作到统一封装的数据库访问层框架中了。

聊完了「纵」,那么「横」是什么呢?


无论是多个客户端,仍是同一个服务端的多个副本。每一个节点的性能必然会存在差别,如何设立合适的阈值?以及如何让策略的变动尽量快的在集群中的多个节点生效?提及来很简单,引入一个性能监控平台和配置中心。但这些真真要作好不容易,后续咱们再展开这块内容。


3、总结

限流就比如保险丝,根据你制定的标准,达到了就拉闸。

不过,触发限流后的措施除了直接丢弃请求以外,还有一个方式是「降级」,那么降级有哪些方式呢?咱们下一篇再聊吧。



Question:
你在工做中有遇到过什么场景须要作「限流」吗?欢迎分享交流一下。



相关文章:



▶ 关于做者:张帆(Zachary,我的微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。本文首发于公众号:「 跨界架构师」(ID:Zachary_ZF)。  
若是你是初级程序员,想提高但不知道如何下手。又或者作程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注个人公众号「 跨界架构师」,回复「技术」,送你一份我长期收集和整理的思惟导图。 
若是你是运营,面对不断变化的市场一筹莫展。又或者想了解主流的运营策略,以丰富本身的“仓库”。欢迎关注个人公众号「 跨界架构师」,回复「运营」,送你一份我长期收集和整理的思惟导图。

微信公众号(首发):跨界架构师<-- 点击后阅读热门文章

按期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些深度思考

相关文章
相关标签/搜索