关于限流:图解代码常见限流算法以及限流在单机分布式场景下的思考

34次阅读

共计 4992 个字符,预计需要花费 13 分钟才能阅读完成。

大家好,我是 yes。

明天来说说限流的相干内容,包含常见的限流算法、单机限流场景、分布式限流场景以及一些常见限流组件。

当然在介绍限流算法和具体场景之前咱们先得明确 什么是限流,为什么要限流?

任何技术都要搞清它的起源,技术的产生来自痛点,明确痛点咱们能力抓住要害隔靴搔痒

限流是什么?

首先来解释下什么是限流?

在日常生活中限流很常见,例如去有些景区玩,每天售卖的门票数是无限的,例如 2000 张,即每天最多只有 2000 集体能进去玩耍。

题外话:我之前看到个新闻,最不想卖门票的景区“卢旺达火山公园”,每天就卖 32 张,并且每张门票须要 1 万元!

再回到主题,那在咱们工程下限流是什么呢?限度的是「流」,在不同场景下「流」的定义不同,能够是每秒申请数、每秒事务处理数、网络流量等等。

而通常咱们说的限流指代的是 限度达到零碎的并发申请数 ,使得零碎可能失常的解决 局部 用户的申请,来保证系统的稳定性。

限流不可避免的会造成用户的申请变慢或者被拒的状况,从而会影响用户体验。因而限流是须要在用户体验和零碎稳定性之间做均衡的,即咱们常说的 trade off

对了,限流也称流控(流量管制)。

为什么要限流?

后面咱们有提到限流是为了保证系统的稳定性。

日常的业务上有相似 秒杀流动、双十一大促或者突发新闻 等场景,用户的流量突增,后端服务的解决能力是无限的,如果不能解决好突发流量,后端服务很容易就被打垮。

亦或是爬虫等 不失常流量 ,咱们对外裸露的服务都要以 最大歹意去防范 咱们的调用者。咱们不分明调用者会如何调用咱们的服务。假如某个调用者开几十个线程一天二十四小时疯狂调用你的服务,不做啥解决咱服务也算完了。更胜的还有 DDos 攻打。

还有对于很多第三方开发平台来说,不仅仅是为了防范不失常流量,也是为了资源的偏心利用,有些接口都收费给你用了,资源都不可能始终都被你占着吧,他人也得调的。

当然加钱的话好磋商

在之前公司还做过一个零碎,过后 SaaS 版本还没进去。因而零碎须要部署到客户方。

过后老板的要求是,咱们须要给他个限流降级版本,岂但零碎出的计划是降级后的计划,外围接口每天最多只能调用 20 次,还须要限度零碎所在服务器的配置和数量,即限度部署的服务器的 CPU 核数等,还限度所有部署的服务器数量,避免客户集群部署,进步零碎的性能。

当然这所有须要能动静配置,因为加钱的话好磋商。客户始终都不晓得。

预计老板在等客户说感觉零碎有点慢吧。而后就搞个 2.0 版本?我让咱们研发部加班加点给你搞进去。

小结一下,限流的实质是因为后端解决能力无限,须要截掉超过解决能力之外的申请,亦或是为了平衡客户端对服务端资源的偏心调用,避免一些客户端饿死。

常见的限流算法

无关限流算法我给出了对应的图解和相干伪代码,有些人喜爱看图,有些人更喜爱看代码。

计数限流

最简略的限流算法就是计数限流了,例如零碎能同时解决 100 个申请,保留一个计数器,解决了一个申请,计数器加一,一个申请处理完毕之后计数器减一。

每次申请来的时候看看计数器的值,如果超过阈值要么回绝。

十分的简略粗犷,计数器的值要是存内存中就算单机限流算法。存核心存储里,例如 Redis 中,集群机器拜访就算分布式限流算法。

长处就是:简略粗犷,单机在 Java 中可用 Atomic 等原子类、分布式就 Redis incr。

毛病就是:假如咱们容许的阈值是 1 万,此时计数器的值为 0,当 1 万个申请在前 1 秒内一股脑儿的都涌进来,这突发的流量可是顶不住的。缓缓的减少解决和一下子涌入对于程序来说是不一样的。

而且个别的限流都是为了限度在指定工夫距离内的访问量,因而还有个算法叫固定窗口。

固定窗口限流

它相比于计数限流次要是多了个工夫窗口的概念。计数器每过一个工夫窗口就重置。
规定如下:

  • 申请次数小于阈值,容许拜访并且计数器 +1;
  • 申请次数大于阈值,回绝拜访;
  • 这个工夫窗口过了之后,计数器清零;

看起来如同很完满,实际上还是有缺点的。

固定窗口临界问题

假如零碎每秒容许 100 个申请,假如第一个工夫窗口是 0-1s,在第 0.55s 处一下次涌入 100 个申请,过了 1 秒的工夫窗口后计数清零,此时在 1.05 s 的时候又一下次涌入 100 个申请。

尽管窗口内的计数没超过阈值,然而全局来看在 0.55s-1.05s 这 0.1 秒内涌入了 200 个申请,这其实对于阈值是 100/s 的零碎来说是无奈承受的。

为了解决这个问题引入了滑动窗口限流。

滑动窗口限流

滑动窗口限流解决固定窗口临界值的问题,能够保障在任意工夫窗口内都不会超过阈值。

绝对于固定窗口,滑动窗口除了须要引入计数器之外还须要记录时间窗口内每个申请达到的工夫点,因而 对内存的占用会比拟多

规定如下,假如工夫窗口为 1 秒:

  • 记录每次申请的工夫
  • 统计每次申请的工夫 至 往前推 1 秒这个工夫窗口内申请数,并且 1 秒前的数据能够删除。
  • 统计的申请数小于阈值就记录这个申请的工夫,并容许通过,反之回绝。

然而滑动窗口和固定窗口都 无奈解决短时间之内集中流量的突击

咱们所想的限流场景,例如每秒限度 100 个申请。心愿申请每 10ms 来一个,这样咱们的流量解决就很平滑,然而实在场景很难管制申请的频率。因而可能存在 5ms 内就打满了阈值的状况。

当然对于这种状况还是有变型解决的,例如设置多条限流规定。不仅限度每秒 100 个申请,再设置每 10ms 不超过 2 个。

再多说一句,这个 滑动窗口可与 TCP 的滑动窗口不一样。TCP 的滑动窗口是接管方告知发送方本人能接多少“货”,而后发送方管制发送的速率。

接下来再说说漏桶,它能够解决工夫窗口类的痛点,使得流量更加的平滑。

漏桶算法

如下图所示,水滴继续滴入漏桶中,底部定速流出。如果水滴滴入的速率大于流出的速率,当存水超过桶的大小的时候就会溢出。

规定如下:

  • 申请来了放入桶中
  • 桶内申请量满了拒绝请求
  • 服务定速从桶内拿申请解决

能够看到水滴对应的就是申请。它的特点就是 宽进严出,无论申请多少,申请的速率有多大,都依照固定的速率流出,对应的就是服务依照固定的速率解决申请。“他强任他强,老子尼克杨”。

看到这想到啥,是不是和音讯队列思维有点像,削峰填谷。一般而言漏桶也是由队列来实现的,解决不过去的申请就排队,队列满了就开始拒绝请求。看到这又想到啥,线程池 不就是这样实现的嘛?

通过破绽这么一过滤,申请就能平滑的流出,看起来很像很挺完满的?实际上它的长处也即毛病。

面对突发申请,服务的处理速度和平时是一样的,这其实不是咱们想要的,在面对突发流量咱们心愿在零碎安稳的同时,晋升用户体验即能更快的解决申请,而不是和失常流量一样,安分守己的解决(看看,之前滑动窗口说流量不够平滑,当初太平滑了又不行,难搞啊)。

而令牌桶在应答突击流量的时候,能够更加的“激进”。

令牌桶算法

令牌桶其实和漏桶的原理相似,只不过漏桶是 定速地流出 ,而令牌桶是 定速地往桶里塞入令牌,而后申请只有拿到了令牌能力通过,之后再被服务器解决。

当然令牌桶的大小也是有限度的,假如桶里的令牌满了之后,定速生成的令牌会抛弃。

规定:

  • 定速的往桶内放入令牌
  • 令牌数量超过桶的限度,抛弃
  • 申请来了先向桶内索要令牌,索要胜利则通过被解决,反之回绝

看到这又想到啥?Semaphore 信号量啊,信号量可管制某个资源被同时拜访的个数,其实和咱们拿令牌思维一样,一个是拿信号量,一个是拿令牌。只不过信号量用完了返还,而咱们令牌用了不偿还,因为令牌会定时再填充。

再来看看令牌桶的伪代码实现,能够看出和漏桶的区别就在于一个是加法,一个是减法。

能够看出令牌桶在应答突发流量的时候,桶内如果有 100 个令牌,那么这 100 个令牌能够马上被取走,而不像漏桶那样匀速的生产。所以在 应答突发流量的时候令牌桶体现的更佳

限流算法小结

下面所述的算法其实只是这些算法最粗略的实现和最实质的思维,在工程上其实还是有很多变型的。

从下面看来如同漏桶和令牌桶比工夫窗口算法好多了,那工夫窗口算法有啥子用,扔了扔了?

并不是的,尽管漏桶和令牌桶比照工夫窗口对流量的 整形成果更佳,流量更加得平滑,然而也有各自的毛病(下面曾经提到了一部分)。

拿令牌桶来说,假如你没预热,那是不是上线时候桶里没令牌?没令牌申请过去不就间接拒了么?这就误杀了,明明零碎没啥负载当初。

再比如说申请的拜访其实是随机的,假如令牌桶每 20ms 放入一个令牌,桶内初始没令牌,这申请就刚好在第一个 20ms 内有两个申请,再过 20ms 外面没申请,其实从 40ms 来看只有 2 个申请,应该都放行的,而有一个申请就间接被拒了。这就有可能造成很多申请的误杀,然而如果看监控曲线的话,如同流量很平滑,峰值也管制的很好。

再拿漏桶来说,漏桶中申请是临时存在桶内的。这其实不合乎互联网业务低提早的要求。

所以漏桶和令牌桶其实比拟适宜 阻塞式限流 场景,即没令牌我就等着,这就不会误杀了,而漏桶本就是等着。比拟适宜后台任务类的限流。而基于工夫窗口的限流比拟适宜 对工夫敏感 的场景,申请过不了您就快点儿通知我,等的花儿都谢了(给阿姨倒一杯卡布奇诺。为什么我会忽然打这句话??)。

单机限流和分布式限流

实质上单机限流和分布式限流的区别其实就在于“阈值”寄存的地位。

单机限流就下面所说的算法间接在单台服务器上实现就好了,而往往咱们的服务是集群部署的。因而须要多台机器协同提供限流性能。

像上述的计数器或者工夫窗口的算法,能够将计数器寄存至 Tair 或 Redis 等分布式 K-V 存储中。

例如滑动窗口的每个申请的工夫记录能够利用 Redis 的 zset 存储,利用 ZREMRANGEBYSCORE 删除工夫窗口之外的数据,再用 ZCARD 计数。

像令牌桶也能够将令牌数量放到 Redis 中。

不过这样的形式等于每一个申请咱们都须要去 Redis 判断一下能不能通过,在性能上有肯定的损耗,所以有个优化点就是「批量」。例如每次取令牌不是一个一取,而是取一批,不够了再去取一批。这样能够缩小对 Redis 的申请。

不过要留神一点,批量获取会导致肯定范畴内的限流误差。比方你取了 10 个此时不必,等下一秒再用,那同一时刻集群机器总处理量可能会超过阈值。

其实「批量」这个优化点太常见了,不论是 MySQL 的批量刷盘,还是 Kafka 音讯的批量发送还是分布式 ID 的高性能发号,都蕴含了「批量」的思维。

当然分布式限流还有一种思维是平分,假如之前单机限流 500,当初集群部署了 5 台,那就让每台持续限流 500 呗,即在总的入口做总的限流限度,而后每台机子再本人实现限流。

限流的难点

能够看到每个限流都有个阈值,这个阈值如何定是个难点。

定大了服务器可能顶不住,定小了就“误杀”了,没有资源利用最大化,对用户体验不好。

我能想到的就是限流上线之后先预估个大略的阈值,而后不执行真正的限流操作,而是采取日志记录形式,对日志进行剖析查看限流的成果,而后调整阈值,推算出集群总的解决能力,和每台机子的解决能力(不便扩缩容)。

而后将线上的流量进行重放,测试真正的限流成果,最终阈值确定,而后上线。

我之前还看过一篇耗子叔的文章,讲述了在自动化伸缩的状况下,咱们要动静地调整限流的阈值很难,于是基于 TCP 拥塞管制的思维,依据申请响应在一个时间段的响应工夫 P90 或者 P99 值来确定此时服务器的健康状况,来进行动静限流。在他的 Ease Gateway 产品中实现了这套算法,有趣味的同学能够自行搜寻。

其实实在的业务场景很简单,须要限流的条件和资源很多 ,每个资源限流要求还不一样。所以我下面就是 嘴强王者

限流组件

一般而言咱们不须要本人实现限流算法来达到限流的目标,不论是接入层限流还是细粒度的接口限流其实都有现成的轮子应用,其实现也是用了上述咱们所说的限流算法。

比方Google Guava 提供的限流工具类 RateLimiter,是基于令牌桶实现的,并且扩大了算法,反对预热性能。

阿里开源的限流框架 Sentinel 中的匀速排队限流策略,就采纳了漏桶算法。

Nginx 中的限流模块 limit_req_zone,采纳了漏桶算法,还有 OpenResty 中的 resty.limit.req库等等。

具体的应用还是很简略的,有趣味的同学能够自行搜寻,对外部实现感兴趣的同学能够下个源码看看,学习下生产级别的限流是如何实现的。

最初

明天只是粗略解说了限流相干的内容,具体利用到工程还是有很多点须要思考的,并且限流只是保证系统稳定性中的一个环节,还须要配合降级、熔断等相干内容。当前有机会再讲讲。


我是 yes,从一点点到亿点点,咱们下篇见

正文完
 0