写点什么

分布式系统关注点——限流该怎么做?

  • 2018-12-17
  • 本文字数:4939 字

    阅读完需:约 16 分钟

分布式系统关注点——限流该怎么做?

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


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



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



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


由此可见,光有熔断是远远不够的。


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



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


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

一、怎么做「限流」

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


  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 哥分享一些我个人的经验。

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

四种策略该如何选择?

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


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


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


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

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

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



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

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


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


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


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


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


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


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


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

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


三、总结


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


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


Question:


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


关于作者:张帆(Zachary),7 年电商行业经验,5 年开发团队管理经验,4 年互联网架构经验。专注大型系统架构、分布式系统。坚持用心打磨每一篇原创。本文首发于公众号:跨界架构师(ID:Zachary_ZF)。


2018-12-17 02:006932

评论 3 条评论

发布
用户头像
其次,滑动窗口。这个方案适用于对异常结果「高容忍」的场景,毕竟相比“两窗”少了一个缓冲区。
这句有错误, 应该是比“两桶”?
2019-03-05 16:12
回复
用户头像
写的不错,不过还是单机版的限流,哪个算法不重要,重要的是你的限流怎么实现分布式的
2019-03-05 10:59
回复
用户头像
学习了, 感谢。
2019-01-10 09:23
回复
没有更多了
发现更多内容

最佳实践:基于vite3的monorepo前端工程搭建 | 京东云技术团队

京东科技开发者

前端 vite Monorepo lodash vue3 vite 企业号 5 月 PK 榜

Python压缩JS文件,重点是 slimit

华为云开发者联盟

Python 开发 华为云 华为云开发者联盟 企业号 5 月 PK 榜

Tomcat处理http请求之源码分析 | 京东云技术团队

京东科技开发者

tomcat container HTTP 企业号 5 月 PK 榜

离线版Gerber查看器+PCB/PCBA检测神器新功能!

华秋PCB

工具 电路 元器件 PCB PCB设计

架构实战营 模块1作业

吴俊

架构实战营

Mac电脑上的系统监控工具:iStat Menus 中文激活版

真大的脸盆

Mac Mac 软件 系统监控

OpenAI如何让ChatGPT遵守了伦理道德的底线

陈磊@Criss

CST如何查看哪些 GPU 在线?

思茂信息

cst cst使用教程 cst操作 cst电磁仿真 cst仿真软件

NFTScan | 05.22~05.28 NFT 市场热点汇总

NFT Research

NFT 热点

【实践篇】手把手教你落地DDD | 京东云技术团队

京东科技开发者

DDD Archetype 企业号 5 月 PK 榜 三层架构

渲大师云主机按量付费功能上线!

Finovy Cloud

渲大师 按量付费

敏捷项目管理中缺陷bug的跟踪和管理

顿顿顿

Scrum 敏捷开发 缺陷管理 敏捷项目管理 敏捷开发管理工具

2023年互联网大厂Java面试八股文整理(1200+面试题附答案解析)

架构师之道

Java 程序员 面试

堪称一绝!阿里技术人都用的Nginx笔记手册,应用到架构齐全

做梦都在改BUG

nginx

盘点一款好用的运维团队协同软件,用过真香!

行云管家

运维 IT运维 协同合作

亿级 GPS 数据如何实现高效存储和查询?不同类型数据库选型分析

爱倒腾的程序员

涛思数据 时序数据库 ​TDengine taosdata

腾讯高工内产,Github都没的SpringBoot源码手册

做梦都在改BUG

Java spring Spring Boot 框架

5G和led显示屏有什么关系

Dylan

技术 5G LED显示屏

Flutter调优--深入探究MediaQuery引起界面Rebuild的原因及解决办法 | 京东云技术团队

京东科技开发者

flutter 企业号 5 月 PK 榜 MediaQuery rebuild

除了运行、休眠…进程居然还有僵尸、孤儿状态

华为云开发者联盟

Linux 开发 华为云 华为云开发者联盟 企业号 5 月 PK 榜

新手系列 I 如何使用 TDesign 轻松开发项目,秘诀都在这里

TDesign

#开源项目 项目搭建

如何设计一个自动化测试平台

老张

自动化测试 测试开发 测试平台

500代码行代码手写docker-设置网络命名空间

蓝胖子的编程梦

k8s 容器网络 ,docker 容器网络方案 容器网络平台

首届百度商业AI技术创新大赛启动 点燃AIGC革新“星火”

百度Geek说

人工智能 百度 AIGC 企业号 5 月 PK 榜

Java中synchronized锁的深入理解

做梦都在改BUG

Java synchronized

惊艳!京东T8纯手码的Redis核心原理手册,基础与源码齐下

做梦都在改BUG

Java 数据库 redis 缓存

我翻遍整个牛客网,整理出了全网最全的Java面试八股文大合集,整整6000多页

采菊东篱下

Java 程序员 面试

深度学习进阶篇-国内预训练模型[6]:ERNIE-Doc、THU-ERNIE、K-Encoder融合文本信息和KG知识;原理和模型结构详解。

汀丶人工智能

人工智能 自然语言处理 深度学习 预训练模型 Transformer

阿里技术大佬限产的Netty核心原理剖析手册,看完你不心动?

做梦都在改BUG

Netty

校园共享电动车发展现状及未来趋势

共享电单车厂家

共享电动车厂家 校园共享电单车 校内共享电动车

2023年天津等级测评机构有哪些?具体位置在哪里?

行云管家

等保 等保测评 等级 天津

分布式系统关注点——限流该怎么做?_架构_张帆_InfoQ精选文章