限流问题 转

之前没有充分搞清楚「限流」和「熔断」的关系。我们先来思考一个问题,生活中也有限流,为什么国庆春节长假热门景点要限流?而不是一早先开几小时,如果人多了就关几小时,人少了就再开呢?其实这就是限流和熔断表象上的一个区别。有熔断机制的系统,它对可用性的作用至少保证了不会全盘崩溃。但是你可以想象一个稍微极端一点的场景,如果系统流量不是很稳定,导致频繁触发熔断的话,是不是意味着系统一直熔断的三种状态中不断切换。 从容断,半开,非熔 导致的结果是每次从开启熔断到关闭熔断的期间,必然会导致大量的用户无法正常使用。系统层面的可用性大致是这样的。 另外,从资源利用率上也会很容易发现,波谷的这段时期资源是未充分利用的。 由此可见,光有熔断是远远不够的。 在高压下,只要系统没宕机,如果能将接收的流量持续保持在高位,但又不超过系统所能承载的上限,会是更有效率的运作模式,因为会将这里的波谷填满。 在如今的互联网已经作为社会基础设施的大环境下,上面的这个场景其实离我们并不是那么远,同时也会显得没那么极端。例如,层出不穷的营销玩法,一个接着一个的社会热点,以及互联网冰山之下的黑产、刷子的蓬勃发展,更加使得这个场景变的那么的需要去考虑、去顾忌。因为随时都有可能会涌入超出你预期的流量,然后压垮你的系统。

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

一、怎么做「限流」

从前面聊到的内容中我们也知道,限流最好能“限”在一个系统处理能力的上限附近,所以: 通过「压力测试」等方式获得系统的能力上限在哪个水平是第一步。 其次,就是制定干预流量的策略。比如标准该怎么定、是否只注重结果还是也要注重过程的平滑性等。 最后,就是处理“被干预掉”的流量。能不能直接丢弃?不能的话该如何处理? 1、获得系统能力的上限 第一步不是我们这次内容的重点,说起来就是对系统做一轮压测。可以在一个独立的环境进行,也可以直接在生产环境的多个节点中选择一个节点作为样本来压测,当然需要做好与其他节点的隔离。 一般我们做压测为了获得2个结果,「速率」和「并发数」。前者表示在一个时间单位内能够处理的请求数量,比如xxx次请求/秒。后者表示系统在同一时刻能处理的最大请求数量,比如xxx次的并发。从指标上需要获得「最大值」、「平均值」或者「中位数」。后续限流策略需要设定的具体标准数值就是从这些指标中来的。 题外话:从精益求精的角度来说,其他的诸如CPU、网络带宽以及内存的耗用也可以作为参照因素。

2、制定干预流量的策略

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

2.1、固定窗口

固定窗口就是定义一个“固定”的统计周期,比如1分钟或者30秒、10秒这样。然后在每个周期统计当前周期中被接收到的请求数量,经过计数器累加后如果达到设定的阈值就触发「流量干预」。直到进入下一个周期后,计数器清零,流量接收恢复正常状态。 固定窗口有一点需要注意的是,假如请求的进入非常集中,那么所设定的「限流阈值」等同于你需要承受的最大并发数。所以,如果需要顾忌到并发问题,那么这里的「固定周期」设定的要尽可能的短。因为,这样的话「限流阈值」的数值就可以相应的减小。甚至,限流阈值就可以直接用并发数来指定。比如,假设固定周期是3秒,那么这里的阈值就可以设定为「平均并发数*3」。

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

2.2、滑动窗口

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

同时,我们还可以得出一个结论是:如果固定窗口的「固定周期」已经很小了,那么使用滑动窗口的意义也就没有了。举个例子,现在的固定窗口周期已经是1秒了,再切分到毫秒级别能反而得不偿失,会带来巨大的性能和资源损耗。 虽然说滑动窗口可以改善这个问题,但是本质上还是预先划定时间片的方式,属于一种“预测”,意味着几乎肯定无法做到100%的物尽其用。

但是,「桶」模式可以做的更好,因为「桶」模式中多了一个缓冲区(桶本身)。 全局数组 链表[] counterList = new 链表[切分的滑动窗口数量]; //有一个定时器,在每一次统计时间段起点需要变化的时候就将索引0位置的元素移除,并在末端追加一个新元素。 2.3、漏桶

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

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

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

2、缓冲的实现可以做一个短暂的休眠或者记录到一个容器中再做异步的重试。

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

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

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

你应该知道,一个程序所在的运行环境中,往往不单单只有这个程序本身,会存在一些系统进程甚至是其它的用户进程。也就是说,程序本身的处理能力是会被干扰的,是会变化的。所以,你可以预估某一个阶段内的平均值、中位数,但无法预估具体某一个时刻的程序处理能力。又因此,你必然会使用相对悲观的标准去作为阈值,防止程序超负荷。 全局变量 int unitSpeed; //出口当前的流出速率。每隔一个速率计算周期(比如1秒)会触发定时器将数值清零。 全局变量 int waterLevel; //当前缓冲区的水位线。 那么从资源利用率来说,有没有更优秀的方案呢?有,这就是「令牌桶」。

2.4、令牌桶

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

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

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

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

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

全局变量 int tokenCount = 令牌数阈值; //可用令牌数。有一个独立的线程用固定的频率增加这个数值,但不大于「令牌数阈值」。 所以,没有真正完美的策略,只有合适的策略。因此,根据不同的场景能够识别什么是最合适的策略是更需要锻炼的能力。下面分享一些我个人的经验。

二、做「限流」的最佳实践

1、四种策略该如何选择?

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

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

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

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

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

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

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

2.1、纵

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

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

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

那么应用间的限流到底是做到客户端还是服务端呢?

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

所以,最终建议你:如果考虑成本就服务端模式,考虑效果就客户端模式。当然也不是绝对,比如一个服务端的流量大部分都来源于某一个客户端,那么就可以直接在这个客户端做限流,这也不失为一个好方案。

数据库层面的话,一般连接字符串中本身就会包含「最大连接数」的概念,就可以起到限流的作用。如果想做更精细的控制就只能做到统一封装的数据库访问层框架中了。

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

2.2、横

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

三、总结

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

不过,触发限流后的措施除了直接丢弃请求之外,还有一个方式是「降级」

最后送上几短话共勉

菜鸟和高手几点区别

1.菜鸟程序员拿到新的需求就急忙忙的上阵打仗了,把自己搞的忙呼呼的,由于考虑不全面做的东西基本上经常被打回来重新写,经常的加班加点。高手拿到需求会在大脑之中,不停的寻找最佳的解决方案,可能在写代码之前已经有很多方案被否定了,所以写出来的代码成品率非常高,真正的高手写代码的时间很短,大部分时间都在思考梳理思维。

2.菜鸟程序员基本上写完代码之后,不太习惯对代码后续优化,甚至有些代码过了一段时间自己都不能识别出来,写代码的时候基本上没有指导思路,后续很容易忘掉。高手写的代码时间长了回来基本上瞅一眼就能明白,主要高手在代码上不断精益求精,不停更新自己代码思维。 3.抗压能力也是菜鸟程序员和高手一个很大的差异,菜鸟遇到大的需求会觉得暗无天日,还会怀疑是不是自己不适合做程序员,高手来再大的需求都会很沉稳,任何一个程序员都会遇到项目紧急状态,抗压能力没有很难在这个行业呆下去。

(adsbygoogle = window.adsbygoogle || []).push({});

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大数据和云计算技术

边缘计算(二)——边缘计算的类型与用途

目前,市场上存在的边缘计算相关概念包括雾计算、边缘计算、多接入边缘计算/移动边缘计算、移动云计算等概念。

46020
来自专栏微信公众号【程序员黄小斜】

Java后端学习路线图,你真的只需要这一张!

学习路线图往往是学习一样技术的入门指南。网上搜到的Java学习路线图也是一抓一大把。

21920
来自专栏原创

如何利用数据架构带动企业增长?

对于架构师而言,技术的发展是无尽的,在搭建和实践智能数据架构的过程中,架构师们都会或多或少地遇到一些疑惑和挑战,如何解决在架构建设中遇到的某些问题?架构建设的领...

8240
来自专栏大数据和云计算技术

分布式数据库助力民生、广发银行前台智慧化业务

随着银行业务的拓展以及网点业务的需求量加大,在新一轮技术浪潮驱动下,各大商业银行也在纷纷推进智能网点的建设。其中,商业银行的柜面无纸化就是最先推进的业务之一。 ...

17120
来自专栏机器之心

各种NLP操作难实现?谷歌开源序列建模框架Lingvo

Lingvo 是世界语(Esperanto)中的一个单词,它表示「语言」的意思。这一命名展示了 Lingvo 框架的根源:它是由 TensorFlow 开发的通...

11520
来自专栏皮振伟的专栏

[qemu][rbd]librbd连接overflow问题

前言: 后端存储使用Ceph卷,在虚拟机中执行mkfs的时候,遇到卡顿。 卡顿位置不确定,有时候是卡在Guest内部执行discard,有时候执行写superb...

12720
来自专栏数据饕餮

分布式数据仓库最佳实践(21)- 事实表设计

本文是《分布式数据仓库最佳实践》系列文章的第四部分第21篇《事实表设计》,针对事实表设计专题进行详细论述,内容包括事实表的类型划分,各种类型的事实表应用的场景、...

21330
来自专栏云霄雨霁

高并发中幂等的实现

在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。即不用担心重复执行幂等方法不会影响系统状态。比如setTrue()方法就具有幂等...

17940
来自专栏原创

实战训练营:传统分布式架构如何进行容器化升级 顶

前言:随着以Docker为典型代表的容器化理念逐渐兴起,众多的使用分布式架构的公司和企业,开始考虑对原有系统进行容器化升级。传统分布式架构为什么需要容器化?容器...

13430
来自专栏Java架构沉思录

分布式系统事务一致性

现今互联网界,分布式系统和微服务架构盛行。业界著名的CAP理论也告诉我们,在设计和实现一个分布式系统时,需要将数据一致性、系统可用性和分区容忍性放在一起考虑。

14630

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励