产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

架构师成长之路之限流

  • 2019-11-06
  • 本文字数:4394 字

    阅读完需:约 14 分钟

架构师成长之路之限流

我们为什么需要限流

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


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


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


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

我们常见的限流手段

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

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.1 机器时钟不一致或者时钟回退问题

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

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

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


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


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

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

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

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

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


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

3.5 准确性和实时性的权衡

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

总结

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


本文转载自公众号云加社区(ID:QcloudCommunity)。


原文链接:


https://mp.weixin.qq.com/s/sON7E7w1yPj0Mxa0whjA2A


2019-11-06 17:421270

评论

发布
暂无评论
发现更多内容

Ubuntu 18.04系统编译安装MySQL 5.7教程。

百度搜索:蓝易云

MySQL 云计算 Linux ubuntu 运维

结合线上线下环境,提供产品的服务...

Bonaparte

产品 产品经理 产品设计 产品思维 产品服务

大模型,开源干不掉闭源

脑极体

开源 大模型

Docker学习路线10:容器安全

小万哥

Java c++ Python Go Docker

絮语2023

IT民工大叔

如何理解 Next.js中的 SSR、CSR、SSG 、ISR以及DPR技术

汽车之家客户端前端团队

前端 SSR React服务端渲染原理

Amazon Redshift Serverless – 现已正式推出新功能

亚马逊云科技 (Amazon Web Services)

Amazon

倪光南院士在 PingCAP 用户峰会的现场致辞

PingCAP

数据库 TiDB pingCAP

HashMap | 利用白话文讲解其底层知识点

派大星

java 编程

IPQ4019 VS IPQ4018 as example-how to choose a suitable WiFi solution?

wifi6-yiyi

IPQ4019 ipq4029 wifi5

Ubuntu 18.04系统编译安装Pure-Ftpd教程。

百度搜索:蓝易云

云计算 Linux ubuntu 运维 Pure-FTPd

Nautilus Chain 主网上线,Zepoch 持有者将获第三轮 POSE 空投

鳄鱼视界

k8s+containerd安装

tiandizhiguai

k8s

C++使用VLD检测内存泄漏

梦笔生花

文心一言 VS 讯飞星火 VS chatgpt (65)-- 算法导论6.5 4题

福大大架构师每日一题

ChatGPT

PingCAP 唐刘:携手中国用户,打造世界级产品

PingCAP

数据库 TiDB pingCAP

Java 命令行参数解析方式探索(一):原始实现

冰心的小屋

Java 命令行 console command

一文熟知存储 – 从磁盘到文件,到数据库,到分布式环境集中式存储,再到分布式数据库

邹志全

数据库 分布式事务 分布式系统

学习SAFe大规模敏捷开发培训有什么好处?

顿顿顿

Scrum 敏捷开发 SAFe框架

百度商业AI技术创新大赛区域赛落幕,各大高校科技新秀角逐复赛

百度Geek说

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

Notion 的用户经济 :爱好者们传播 “第二大脑” 的理念

B Impact

LLM Data Pipelines: 解析大语言模型训练数据集处理的复杂流程

Baihai IDP

人工智能 LLM 白海科技 数据集处理流程

什么是供应链金融?定义集

zhengzai7

金融科技 供应链金融

Ubuntu 18.04系统编译安装Apache教程。

百度搜索:蓝易云

Apache 云计算 Linux ubuntu 运维

软件开发必读!华为云软件开发生产线CodeArts深度体验指南

华为云PaaS服务小智

华为 软件开发 华为云

Java的异常处理

java易二三

异常处理 java 编程 #编程

【SPS人物志】安艺:永不服输是电竞选手最大的魅力

Geek_2d6073

文心大模型3.5国际评测拿下7个满分,大二学生1分钟AI作画估值百万!

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨 文心大模型

几种验证端口开启的方法

林十二XII

刘奇:经典数据库亟需跃迁,TiDB 不是“平替”

PingCAP

数据库 TiDB pingCAP

Ubuntu 18.04系统编译安装PHP教程。

百度搜索:蓝易云

php 云计算 Linux ubuntu 运维

架构师成长之路之限流_文化 & 方法_吃橙子的狐狸_InfoQ精选文章