图解常见限流算法以及限流在分布式场景下的思考


今天来讲说限流的相关内容,包括常见的限流算法、单机限流场景、分布式限流场景以及一些常见限流组件。程序员

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

任何技术都要搞清它的来源,技术的产生来自痛点,明确痛点咱们才能抓住关键对症下药面试

限流是什么?

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

在平常生活中限流很常见,例如去有些景区玩,天天售卖的门票数是有限的,例如 2000 张,即天天最多只有 2000 我的能进去游玩。后端

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

卢旺达火山公园

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

而一般咱们说的限流指代的是 限制到达系统的并发请求数,使得系统可以正常的处理 部分 用户的请求,来保证系统的稳定性。网络

限流不可避免的会形成用户的请求变慢或者被拒的状况,从而会影响用户体验。所以限流是须要在用户体验和系统稳定性之间作平衡的,即咱们常说的 trade off并发

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

为何要限流?

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

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

亦或是爬虫等不正常流量,咱们对外暴露的服务都要以最大恶意去防备咱们的调用者。咱们不清楚调用者会如何调用咱们的服务。假设某个调用者开几十个线程一天二十四小时疯狂调用你的服务,不作啥处理咱服务也算完了。更胜的还有 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 产品中实现了这套算法,有兴趣的同窗能够自行搜索。

其实真实的业务场景很复杂,须要限流的条件和资源不少,每一个资源限流要求还不同。因此我上面就是嘴强王者

罗老师V5

限流组件

通常而言咱们不须要本身实现限流算法来达到限流的目的,无论是接入层限流仍是细粒度的接口限流其实都有现成的轮子使用,其实现也是用了上述咱们所说的限流算法。

好比Google Guava 提供的限流工具类 RateLimiter,是基于令牌桶实现的,而且扩展了算法,支持预热功能。

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

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

具体的使用仍是很简单的,有兴趣的同窗能够自行搜索,对内部实现感兴趣的同窗能够下个源码看看,学习下生产级别的限流是如何实现的。

最后

今天只是粗略讲解了限流相关的内容,具体应用到工程上仍是有不少点须要考虑的,而且限流只是保证系统稳定性中的一个环节,还须要配合降级、熔断等相关内容。

能读到这里的朋友很少(有数听说话的),提醒下你们,若是还没关注到石头首次搞的送书活动的能够瞧一瞧,免费包邮到家,欢迎你们来抽奖(两种抽奖方式详情点这里),记得帮忙 review 下抽奖的代码是否有 bug


推 荐 阅 读

面试 Google, 我失败了 | Google 面经分享

提高开发效率N倍的20+命令行神器!(附 demo)

10 年 bloger 告诉你要不要写博客,又该如何优雅地写博客?

招人,咱们是认真的 ——“阿里云-ECS/神龙计算平台” 急招开发




程序猿石头 


程序猿石头(ID: tangleithu),现任阿里巴巴技术专家,清华学渣,前大疆后端 Leader。用不一样的视角分享高质量技术文章,以每篇文章都让人有收获为目的,欢迎关注,交流和指导!扫码回复关键字 “1024” 获取程序员大厂面试指南



本文分享自微信公众号 - 程序猿石头(tangleithu)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。