架构师成长之路之限流漫谈

阅读本文大概须要 7.3 分钟。

1. 咱们为何须要限流

在上一篇架构师成长之路之服务治理漫谈里面,咱们已经谈到了高可用治理的部分。为了“反脆弱”,在微服务复杂拓扑的状况下,限流是保障服务弹性和拓扑健壮的重中之重。redis

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

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

固然,也不是全部的系统都须要限流,这取决于架构师对于当前业务发展的预判。安全

2. 咱们常见的限流手段

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

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 来尽量提高性能。

伪代码以下:

// 开启pipepipeline = 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://cloud.tencent.com/developer/article/1408380


往期精彩回顾


相关文章
相关标签/搜索