程序收藏不看系列:一文轻松搞定系统限流

1. 我们为什么需要限流

为了“反脆弱”,在微服务复杂拓扑的情况下,限流是保障服务弹性和拓扑健壮的重中之重。

想一想,如果业务推出了一个秒杀活动,而你没有任何的限流措施;当你搭建了一个账号平台,而完全没有对十几个业务方设定流量配额……这些很有可能在特定场合下给你的产品带来大量的业务损失和口碑影响。

我们通常重点关注产品业务层面正向和逆向功能的完成,而对于逆向技术保障,这一点则是企业发展过程中很容易忽视的,所以一旦业务快速增长,这将给你的产品带来很大的隐患。

当然,也不是所有的系统都需要限流,这取决于架构师对于当前业务发展的预判。

2. 我们常见的限流手段

我们来列举业内比较常见的一些限流手段。

2.1 信号量计数

信号量竞争是用来控制并发的一个常见手段。比如 C 和 Java 中都有 Semaphore 的实现可以让你方便地上手。鼎鼎大名的弹性框架 Hystrix 也默认选择了信号量来作为隔离和控制并发的办法。它的优点即在于简单可靠,但是只能在单机环境中使用。

2.2 线程池隔离

隔离舱技术中也大量使用了线程池隔离的方式来实现,通过限制使用的线程数来对流量进行限制,一般会用阻塞队列配合线程池来实现。如果线程池和队列都被打满,可以设计对应拒绝策略。需要谨慎调整其参数和线程池隔离的个数,以避免线程过多导致上下文切换带来的高昂成本。也是基于这个考虑,Hystrix 默认采用了信号量计数的方式来控制并发。同样,其也只能在单机环境中使用。

2.3 固定窗口计数

我们可以以第一次请求访问的时候开始进行计数,而不严格按照自然时间来计数。比如可以利用 Redis 的 INCR 和 EXPIRE 组合进行计数,如下伪代码所示:

count = redis.incrby(key)
if count == 1
    redis.expire(key,3600)
if count >= threshold
    println("exceed...")

这种实现方式简单粗暴,可以解决绝大部分分布式限流的问题。但是其存在的问题是:

  1. 该计数方式并不是准确计数,由于时间窗口一旦过期,则之前积累的数据就失效,这样可能导致比如本来希望限制“一分钟内访问不能超过 100 次”,但实际上做不到精准的限制,会存在误判放过本应拒绝的流量。
  2. 每次请求都将访问一次 Redis,可能存在大流量并发时候将缓存打崩最终拖垮业务应用的问题。这个在高并发场景中是非常严重的问题。当然,你可以选择按照业务进行适当的缓存集群切割来缓解这种问题,但是这仍然是治标不治本。当然,如果你选择单机限流的实现方式,则无需使用 Redis,进一步,单机限流情况下该问题不存在。

2.4 自然窗口计数

有的场景会需要以自然窗口为维度进行限制,实现方式即进行分桶计数。每个 slot 一般以时间戳为 key salt,以单 slot 时间长度内的计数值为 Value,我们可以根据实际的需求对单 slot 的时间长度进行限制,比如如果你需要限制一天发送短信数不超限,则以 1 个自然天为 1 个 slot,如果希望限制 QPS,则以 1s 为 1 个 slot。然后起定时任务获取 slot,进一步取出实际的分桶计算结果,进行判断是否达到阈值,如果超过阈值则执行对应的限制操作。

该策略如果应用在分布式限流环境下,则会碰到若干个问题。这个后面章节中会提到。另外,该策略本质上其实是也是一种特殊的固定窗口计数策略,那么固定窗口所存在的弊端,自然窗口计数也会存在。那么我们不禁会问,如果希望规避固定窗口的一大问题——“无法准确计数”的话,要怎么做呢?这时,“滑动窗口计数”方式应运而生。

2.5 滑动窗口计数

滑动窗口的出现,可以很好地解决精准计数的问题。随着时间窗口不断地滑动,动态地进行计数判断。可以规避自然窗口和固定窗口计数所存在的计数不准确的问题。以下有两种常见的滑动窗口计数的实现类别。

2.5.1 基于共享分布式内存

可以采用 Redis ZSet,存储结构如下图所示。Key 为功能 ID,Value 为 UUID,Score 也记为同一时间戳。整个过程简单概括为“添加记录、设置失效时间、计数、删除过期记录”四部分。使用 ZADD、EXPIRE、ZCOUNT 和 zremrangeScore 来实现,并同时注意开启 Pipeline 来尽可能提升性能。

伪代码如下:

// 开启pipe
pipeline = redis.pielined()
// 增加一条请求
pipeline.zadd(key, getUUID(), now)
// 重新设置失效时间
pipeline.expire(key, 3600)
// 统计在滑动窗口内,有多少次的请求
count = pipeline.zcount(key, expireTimeStamp, now)
// 删除过期记录
pipeline.zremrangeByScore(key, 0, expireTimeStamp - 1)
pipeline.sync()
if count >= threshold
    println("exceed")

但是该方法,有一个比较突出的问题。就是这是一个重操作,将引发高 QPS 下 Redis 的性能瓶颈,也将消耗较多的资源和时间。一般我们可以付出秒级的时延,对其做多阶段异步化的处理。比如将计数、删除过期数据和新增记录分为三部分去进行异步处理。此处就不进一步展开了。

2.5.2 基于本地内存

第一个方案中,分布式滑动窗口的难度在于,不得不进行内存共享来达到窗口计数准确的目的。如果考虑分发时进行 Key Based Routing 是不是能解决这个问题?在付出非幂等、复杂度抬升等一定代价的情况下,引入基于本地内存的分布式限流实现方式。

实现方式有如下两种:

  1. 如果可以接受准实时计算的话,可以采用 Storm,使用 filedsGroup,指定 Key 到对应的 Bolt 去处理;
  2. 如果需要实时计算的话,那么就采用 RPC 框架的 LB 策略为指定 Key 的一致性 Hash。然后路由到对应的服务实例去处理。

以上两个实现方式,当到达 Bolt 或者服务实例后,即可基于本地内存进行处理,处理方式也有三种。

  1. 采用 Esper,用 DSL 语句即可简单实现滑动窗口。
  2. Storm 1.0 之后提供了滑动窗口的实现。
  3. 如果希望自实现滑动窗口(不推荐),实现思路也比较简单即:循环队列+自然窗口滑动计数。

循环队列来解决无限后延的时间里,计数空间重复利用的问题。而此处,我们看到了一个熟悉的名词——“自然窗口计数”。没错,底层仍然采用自然窗口计数,但是区别在于,我们会对自然窗口切分更细的粒度,每次批量超前获取多个分桶,来进行加和计算。这样就可以实现滑动窗口的效果,你可以认为,当分桶被细化到 10s、5s 甚至越来越细的时候,计数将趋近于更加准确。

2.6 令牌桶和漏桶算法计数

令牌桶的示意图如下:

而漏桶的示意图如下:

这个在业内也是鼎鼎大名。基本谈起限流算法,这两个算法必然会被提起,令牌桶可以有流量应对突发流量,漏桶则强调对流量的整型。二者的模型是相反的。令牌桶和漏桶算法在单机限流中较为常见,而在分布式限流中罕见踪迹。

对于令牌桶来说,你可以采用定时任务去做投递令牌的动作,也可以采用算法的方式去进行简单的计算。Guava Ratelimiter 采用的是后者。

令牌桶的优势之一,在于可以有部分余量用以应对突发流量。但是在实际生产环境中,这不一定是安全的。如果我们的服务没有做好应对更高突发流量的准备,那么很有可能会引发服务雪崩。所以考虑到这一点,Guava 采用了令牌桶 + 漏桶结合的策略来进行限流。对于默认业务,采用标准令牌桶方式进行“可超支”限速,而对于无法突然应对高峰流量的业务,会采用缓慢提升投放令牌速率(即逐步缩短业务请求等待时间)的方式来进行热启动控制,具体见 Guava Ratelimiter 源码注释描述,此处不赘述,其效果如下图所示:

3.微服务限流几个考虑的点

以上的限流手段,有的能应用在单机环境,有的能应用在分布式环境。而在高并发的分布式环境中,我们需要考虑清楚如下几个问题如何解决。

3.1 机器时钟不一致或者时钟回退问题

一旦出现这种问题,则可能导致收集的数据相互污染而导致判断出错。所以一方面,在运维层面需要确保机器时钟能够按期同步。另一方面,需要有准实时检测的手段,及时发现时钟偏差太大或者时钟回退的机器,基于一定策略筛选出不合格的数据来源,将其刨除出计算范围并发出警告。

3.2 在 SDK 还是 Server 端做限流逻辑

你需要考虑你的限流策略迭代的频繁程度,推动业务方改造的成本,语言/技术栈异构情况,是否有需要进行立多系统联合限流的场景,以此来进行决策。如果采用 SDK 方式,你需要做好碰到这几个棘手问题的心理准备。

而如果采用 Server 方式,你则需要更多考虑高并发下数据堆积,机器资源消耗,以及对业务方性能的影响问题。一般业内采用的是富 SDK 的方式来做,但是对于上述的 SDK 会面临的几个问题没有很好的解决方案。而 ServiceMesh 领军人物 Istio 采用了 Mixer 来实现 Server 端限流的方式,但是碰到了很严重的性能问题。所以这是一个很困难的选择。

回顾下架构师成长之路之服务治理漫谈一篇中所讲到的服务治理发展路径,是不是有点惊人的相似?是不是也许限流的未来,不在 SDK 也不在 Server,而在于 ServiceMesh?我不确定,但我觉得这是一个很好的探索方向。

3.3 限流是不是会让你的系统变得不可控

这是一个很有意思的问题,限流本身是为了“反脆弱”而存在的,但是如果你的分布式复杂拓扑中遍布限流功能,那么以后你每个服务的扩容,新的功能上线,拓扑结构的变更,都有可能会导致局部服务流量的骤增,进一步引发限流导致业务有损问题。这就是“反脆弱”的本身也有可能会导致“脆弱”的出现。所以,当你进行大规模限流能力扩张覆盖的时候,需要谨慎审视你的限流能力和成熟度是否能够支撑起如此大规模的应用。

3.4 拓扑的关联性能给限流带来什么

我们置身于复杂服务拓扑和各种调用链路中,这一方面确实给限流带来了很大的麻烦,但另一方面,我们是不是可以思考一下,这些复杂度,本身是不是可以带给我们什么样的利好?比如:底层服务扛不住,那么是不是可以在更上层的调用方入口进行限流?如此是不是可以给予用户更友好提示的同时,也可避免链路上服务各自限流后带来的系统级联处理压力?微服务的本质是自治没错,但是我们是不是可以更好地对各个服务的限流自治能力进行编排,以达到效率、体验、资源利用的优化?

相信大家都会有自己的答案。这件事情本身的难度是在于决策的准确性,但如果能很好地进行落地实现,则意味着我们的限流从自动化已经逐步转向了智能化。这也将是更高一层次的挑战和机遇。

3.5 准确性和实时性的权衡

在高并发限流场景下,准确性和实时性理论上不可兼得。在特定的场景中,你需要作出你的选择,比如前文介绍的基于 Redis ZSet 实现的滑动窗口实时计算方式可以满足实时性和准确性,但其会带来很明显的性能问题。所以我们需要作出我们的权衡,比如牺牲准确性将滑动窗口退化为固定窗口来保障性能;或者牺牲实时性,对滑动窗口多阶段去做异步化,分析和决策两阶段分离,来保障性能。这取决于你的判断。

4. 总结

限流是高可用治理中核心的一环,实现方式也五花八门,每种方式也都有各自的问题,本文只是做了一个简单的回顾。希望随着 ServiceMesh、AIOps 等理论的兴起,我们对于限流是什么,能做什么,怎么实现,能够释放出更大的空间去想象。

原文地址:https://www.cnblogs.com/Longdingtian/p/10981887.html

时间: 2024-10-06 16:02:16

程序收藏不看系列:一文轻松搞定系统限流的相关文章

程序员收藏必看系列:深度解析MySQL优化(二)

程序员收藏必看系列:深度解析MySQL优化(一) 性能优化建议 下面会从3个不同方面给出一些优化建议.但请等等,还有一句忠告要先送给你:不要听信你看到的关于优化的“绝对真理”,包括本文所讨论的内容,而应该是在实际的业务场景下通过测试来验证你关于执行计划以及响应时间的假设. scheme设计与数据型优化选择数据类型只要遵循小而简单的原则就好,越小的数据类型通常会更快,占用更少的磁盘.内存,处理时需要的CPU周期也更少.越简单的数据类型在计算时需要更少的CPU周期,比如,整型就比字符操作代价低,因而

从零开始,轻松搞定SpringCloud微服务系列

本系列博文目录 [微服务]之一:从零开始,轻松搞定SpringCloud微服务系列–开山篇(spring boot 小demo) [微服务]之二:从零开始,轻松搞定SpringCloud微服务系列–注册中心(一) [微服务]之三:从零开始,轻松搞定SpringCloud微服务-配置中心

分分钟轻松搞定IBM系列 RAID5搭建

分分钟轻松搞定IBM系列 RAID5搭建 按照 以下图片步骤一步步可轻松完成IBM服务器RAID1.5.10等的搭建. 此例是以RAID5为例,RAID1和10可举一反三. 原文地址:https://www.cnblogs.com/kezi/p/11621258.html

深入浅出 Python 装饰器:16 步轻松搞定 Python 装饰器

Python的装饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西.虽然好像,他们要干的事都很相似--都是想要对一个已有的模块做一些"修饰工作",所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去.但是OO的Decorator简直就是一场恶梦,不信你就去看看wikipedia上的词条

韩顺平_轻松搞定网页设计(html+css+javascript)_第34讲_js超级玛丽小游戏

韩顺平_轻松搞定网页设计(html+css+javascript)_第34讲_js超级玛丽小游戏_学习笔记_源代码图解_PPT文档整理 分类: PHP 2012-12-12 15:01 4256人阅读 评论(0) 收藏 举报 文西马龙:http://blog.csdn.net/wenximalong/ 采用面向对象思想设计超级马里奥游戏人物(示意图) 怎么用通过按键,来控制图片的位置 这个小游戏,用面向对象会很方便,不用面向对象会很麻烦很麻烦,比如以后要讲解的坦克大战的游戏,要是用纯的面向过程或

轻松搞定javascript预解析机制(搞定后,一切有关变态面试题都是浮云~~)

hey,guys!我们一起总结一下JS预解析吧! 首先,我们得搞清楚JS预解析和JS逐行执行的关系.其实它们两并不冲突,一个例子轻松理解它们的关系: 你去酒店吃饭,吃饭前你得看下菜谱,点下菜(JS预解析),但吃的时候还是一口一口的吃(JS逐行执行)! OK,解决下面五个问题,JS预解析就算过了~~(前提:对JS变量作用域有清晰理解) 一.JS预解析是什么? 其实就是对程序要用到的材料(变量,函数)给一个初始值,并存到一个表中(我自己虚构的),当程序运行到那一行时,就来这个表看有没有初始值,没有就

Webcast / 技术小视频制作方法——自己动手录制video轻松搞定

Webcast / 技术小视频制作方法——自己动手录制video轻松搞定 http://blog.sina.com.cn/s/blog_67d387490100wdnh.html 最近申请加入MSP的童鞋应该发现了一个新的要求——制作简短的视频!视频的内容要求是与微软技术相关~我们希望通过使用这种方法,简化申请流程,加强对创意.微软相关技术的考察~关于MSP项目以及申请流程的细则近期也会出台,请童鞋们耐心等待~ 首先呢,就跟广大的童鞋们介绍一款简单使用的录屏软件~而通过简单的安装,便可以轻松.便

【转】轻松搞定FTP之FlashFxp全攻略

转载网址:http://www.newhua.com/2008/0603/39163.shtml 轻松搞定FTP之FlashFxp全攻略 导读: FlashFXP是一款功能强大的FXP/FTP软件,融合了一些其他优秀FTP软件的优点.如像CuteFTP一样可以比较文件夹,支持彩色文字显示:像BpFTP支持多文件夹选择文件,能够缓存文件夹:像LeapFTP一样的外观界面,甚至设计思路也比较相似,支持文件夹 (带子文件夹)的文件传送.删除:支持上传.下载及第三方文件续传:可以跳过指定的文件类型,只传

centos下yum安装lamp和lnmp轻松搞定

centos下yum安装lamp和lnmp轻松搞定,到底多轻松你看就知道了,妈妈再也不担心不会装lamp了. 很辛苦整理的安装方法,会持续更新下去.凡无法安装的在评论里贴出问题来,会尽快解决.共同维护一个可用yum可用更新. 软件列表:php5.4 apache2.2 mysql5.5 nginx1.8 centos6.x rpm -Uvh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ng