揭示限流的力量:确保现代系统的健壮和效能
引言
在快速演变的现代软件系统领域中,保持最佳性能、稳定性和安全性已成为首要关注点。在这种复杂情境下,限流的概念已经成为确保应对各种挑战时应用程序的韧性和效率的关键工具。
数字世界的指数增长凸显了实施有效限流策略的必要性。无论是为数千个客户端提供服务的 API,还是管理着各种工作负载的微服务架构,抑或是适应流量激增的云原生应用,限流在塑造这些系统的行为和性能方面发挥着关键作用。
在本文中,我们深入探讨了限流的领域,探讨了其不同形式、优势、实施技术和高级策略。到最后,读者将全面了解限流如何赋予现代系统维持稳定性、公平分配资源和优雅处理意外挑战的能力。
限流的实质
限流的核心思想是控制传入请求的速率,以避免系统过载。通过设置允许的请求速率,系统可以平稳地处理流量,而不至于被突发的请求淹没。这一策略对于防止服务器过载、降低响应时间和保持系统可用性至关重要。
除了稳定性,限流还有助于保护资源免受滥用。通过设定每个用户或客户端的请求速率,限流可以防止单个用户垄断资源,从而确保所有用户都能获得公平的服务。这种均衡有助于提供一致的用户体验,而不受个别用户的行为影响。
限流的实现可以基于不同的算法,如令牌桶和漏桶算法。令牌桶算法基于令牌的分发来控制请求速率,而漏桶算法则通过固定速率地接收请求,然后以固定速率处理它们。根据不同的应用需求和流量特征,选择适合的算法可以最大限度地优化限流效果。
限流环节
客户端
在客户端请求发起的位置进行限流。
一般常见的,比如按钮变灰,到了指定时间才被点亮,或者是上次点击后间隔某个时间才再次点亮。
再比如早期的秒杀系统,比如要求放过 10%的前端请求。用户点了秒杀按钮后,前端生成随机数判断是否提交请求。
针对秒杀的前端随机数限流多说两句,大家都知道这是防君子不防小人,有的人认为无所谓,本来秒杀就是看运气。但我不这么认为,对于“小人”来说,他们的这个环节通过的概率是 100%,而“君子”的概率是 10%,“小人”秒杀成功的概率是“君子”的 10 倍,当然不公平。
接入端
在 Web 服务器,或者网关等在业务前端的位置做限流。
通过用户 ID、IP 控制访问频率,比如 Nginx、Tomcat 等。
根据业务服务处理能力,限制流量访问,比如 Spring Cloud Gateway,Zuul 等。
业务服务端
分析所有业务服务访问路径,优先级,做压测找出性能瓶颈,核心请求路径等,依据二八原则有针对性的做出限流控制,考虑是否配合降级等方案。
限流策略和算法
流量计数器/固定时间窗
最简单的方式,每个时间周期内统计请求数量,超出阈值则限流,下一个时间周期重新统计。参考下图,每秒请求限制为 100 个。
优点:在控制传入请求速率方面提供了简单性和可预测性。
实施简单:该算法的直接性使其相对容易实施。开发人员可以快速设置限流机制,无需进行复杂的计算。
稳定可预测:请求在固定的时间间隔内进行计数,为限流过程提供了稳定性。这种可预测性有助于在不同流量条件下理解和规划系统行为。
立即执行:固定窗口方法在时间窗口重置后立即执行限流。
劣势:流量分布不均匀。
突发性影响:该算法在时间窗口开始时不区分请求的突发性和整个窗口内均匀分布的请求。这可能导致请求的不均匀分布,在突发流量期间可能导致服务中断。比如在时间周期 1 秒的前 100 毫秒把 100 次请求用完了,后面的 900 毫秒不可用了。
临界点问题:极端情况下,比如前一个周期结束前 1 毫秒和下一个周期的第 1 个毫秒,会通过 200 个请求(2 倍阈值)。
资源利用率低:同样的,前一个周期的限流额度未完全用掉,下一个周期开始时执行重置,这部分额度被浪费了。
滑动时间窗
为解决固定时间窗的流量分布问题,滑动时间窗把每个时间周期划分成多个小周期区间,每次向后移动一个小周期区间,把最早的丢掉。参考下图,第一个周期窗口是 0:00~1:00,第二个周期窗口是 0:50~1:00,以此类推。
优点:相对于固定时间窗算法,在粒度和流量分布上具有优势。
更精确的限流控制:与固定窗口算法不同,滑动窗口算法允许以更精确的方式控制请求速率。请求不仅在时间窗口开始时计数,还会根据滑动的时间间隔持续计数,因此可以更准确地控制流量。
均衡的请求分配:由于滑动窗口计数持续更新,该算法在更大程度上促进了均衡的请求分配。它可以更好地处理客户端的不规则请求模式,确保公平的资源分配。
抵御突发流量:滑动窗口算法的连续计数特性使其能够更好地抵御突发流量的影响。即使在短时间内发生大量请求,滑动窗口算法可以更有效地平稳处理。
劣势:主要体现在实现复杂,及资源消耗上。
复杂的实现:相对于固定窗口算法,滑动窗口算法的实现可能更加复杂。需要管理滑动窗口内的请求计数和滑动窗口的时间跨度,这可能导致实现的复杂性增加。
资源消耗:由于滑动窗口需要持续更新计数,可能会导致更大的计算和内存资源消耗。在高负载情况下,这可能会影响系统性能。
漏桶
水(请求)先进入到桶里,然后均速流出(处理),桶满则水溢出。
作为类比可知,水可以是变速、间歇流入的,流出则是固定速率。进一步可知如果入水的平均速率超过出水的速率,或者如果一次倒入的水超过桶容量的水量,则会溢出。
漏桶算法有两种实现版本,一个是用计数器实现,另一个是用队列实现。计数器的实现版本与令牌桶类似,所以一般情况下漏桶算法采用的是队列的实现。
优点:实现简单,流量管制、流量整形。
平滑请求流量:漏桶算法以恒定的速率处理请求。这有助于平滑流量,避免流量的突发性增加对系统造成冲击。
稳定的系统性能:通过固定的漏出速率,漏桶算法维持了恒定的请求处理速度,从而确保了系统的稳定性和可预测性。
防止流量峰值:漏桶算法能够有效地抑制突发流量和流量峰值,防止它们影响系统的性能和可用性。
劣势:
响应时间影响:当发生流量突增时,漏桶算法可能会导致请求的排队等待,从而增加了响应时间。这在高流量时可能影响用户体验。
灵活性不足:漏桶算法的固定速率可能难以适应不同流量模式,特别是那些具有时变特性的流量。
资源浪费:在请求不断到达时,漏桶算法可能会导致部分请求被丢弃,从而浪费了这些请求所代表的潜在业务价值。
处理突发流量挑战:尽管漏桶算法可以防止流量峰值,但在短时间内发生的大量请求可能会超出漏桶的容量,导致一些请求被拒绝。
令牌桶
令牌桶算法是以固定的速率向桶里放入令牌,桶满则丢掉,当有请求要处理时需要先从桶里拿到令牌,桶里没有令牌则拒绝请求。
可以看到令牌桶和漏桶是有一定相似性的,漏桶(请求)是定速流出,流入可以间歇变速;令牌桶(令牌)则是定速流入,流出可以间歇变速。
相对于漏桶的均速流出,令牌桶可以快速把桶内的令牌拿走,即可以更及时的应对一定程度上的突发流量。
优点:
灵活的限流控制:令牌桶算法通过动态分配令牌来控制请求速率。这使得它可以适应不同的流量模式,包括突发性和稳定性流量。
优雅的处理突发流量:令牌桶算法的令牌桶存储了一定数量的令牌,这使得它能够优雅地处理突发流量。请求只有在有足够令牌时才会被处理,从而平稳地消耗令牌。
劣势:
复杂的实现:相对于其他简单的限流算法,令牌桶算法的实现可能较为复杂,需要管理令牌生成和使用的逻辑。
限流算法小结
基于时间窗口的限流算法能处理快速处理请求,更适合低延时的业务限流场景。比如互联网业务。
漏桶、令牌桶对比时间窗口类算法对流量的整形效果更好。
漏桶由于请求是暂存在桶中的,所以请求处理是有延时的。
令牌桶上线时需要有预热处理,以避免请求被误丢弃的可能性。
令牌桶相比漏桶可以更充分,尽可能多的处理请求。
限流的关键在于阈值的配置,要考虑如何在不误伤请求的前提下更好的利用资源。比如漏桶、令牌桶的核心配置是桶的大小,流出速率/流入速率。令牌桶的流入速率允许更加灵活的限流控制。漏桶的桶大小难以控制,大了会加大延时,小了会误丢请求。除了经验,压测也是基础和必要的。
单机限流 vs 分布式限流
单机限流用于单个节点。分布式限流则要达到全局性的效果,比如系统有 5 个用户微服务,要限制每秒一共最多处理 3000 个请求。
单机模式的限流非常简单,可以直接基于内存就可以实现,而集群模式的限流必须依赖于某个“中心化”的组件,比如网关、Redis、或中间件。
限流组件
Google Guava RateLimiter
单机限流,采用令牌桶算法,支持两种限流方式:
平滑突发限流,默认
平滑预热限流
参考下面代码,因为指定了 3 秒的预热周期,刚启动时并不会每 500 毫秒生成 1 个令牌,3 秒后达到平均速率。
Hystrix
Netflix 开源的一款具备熔断、限流、降级能力的容错系统。但在 18 年就已经停止更新,官方推荐使用 Resilience4j。这里只大致介绍一下。
Hystrix 提供了简单的限流功能,即资源隔离,是对并发量限流。两种隔离策略:线程池隔离(Bulkhead Pattern)和信号量隔离。
线程池隔离
Hystrix 的线程池隔离针对不同的资源分别创建不同的线程池,不同服务调用都发生在不同的线程池中,在线程池排队、超时等阻塞情况时可以快速失败,并可以提供 fallback 机制。
信号量隔离
控制了对该依赖服务的并发请求数。当信号量达到上限时,后续的请求将会被立即拒绝,不会进入依赖服务的执行流程。信号量隔离主要关注的是对依赖服务之间的隔离和保护。
线程池隔离的好处是隔离度比较高,信号量则更加轻量级。
Resilience4j
Resilience4j 一个非常轻量级的 Java 函数式库,使得 Resilience4j 非常适合函数式编程。限流相关有 Rate Limiter 和 Bulkhead 两个功能:
Rate Limiter 请求频率限流
Resilience4j 基于令牌桶算法,提供了两种限流实现:
SemaphoreBasedRateLimiter 使用信号量来实现,每次用户请求都会尝试获取一个信号量,并记录下请求时间。如果成功获取信号量,则允许处理该请求;如果获取失败,则触发限流。此外,算法还包括一个内部线程,定期扫描已过期的信号量并释放。
AtomicRateLimiter 与上述实现相似,但不需要额外的线程。在处理每次请求时,它会根据距离上次请求的时间和令牌生成速率自动填充令牌。
Bulkhead 并发量限流
Resilience4j 同样提供了两种实现,SemaphoreBulkhead 和 ThreadPoolBulkhead,通过信号量或线程池控制请求的并发数。原理上与 Hystrix 的相似。
Sentinel
阿里的面向云原生微服务的高可用流控防护组件。具有以下特征:
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 提供了轻量级的信号量隔离的隔离策略,基于 QPS/基于调用关系的限流策略。
并发线程数控制,Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。
QPS 流量控制,当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:直接拒绝(滑动时间窗算法)、Warm Up(令牌桶算法)、匀速排队(漏桶算法结合虚拟队列等待机制)。
Sentinel 针对集群场景提供了集群流量控制功能
假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。即上面提到的分布式限流功能。
另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。
Redis
Redis-Cell
Redis 4.0 提供的限流模块,该模块使用了漏桶算法,并提供了原子的限流指令:cl.throttle。
RedisRateLimiter
Spring Cloud Gateway 自带了一个限流实现,就是 RedisRateLimiter,可以用于分布式限流。它的实现原理依然是基于令牌桶算法的,不过实现逻辑是放在一段 lua 脚本中的,可以在 src/main/resources/META-INF/scripts 目录下找到该脚本文件。
可以通过配置文件或代码直接配置,限于篇幅这里仅给出代码示例
reids + lua
基本上就是直接调用执行 lua 的代码字符串实现某个算法,比如令牌桶。略。
其它
Nginx
依赖 Nginx 自带的限流功能,针对请求的来源 IP 或者自定义的一个关键参数来做限流,比如用户 ID。
示例:基于 IP 限流
另,Nginx 的 limit_conn_zone 和 limit_conn 两个指令控制可以并发连接的总数。
Tomcat
Tomcat 使用 maxThreads,即最大线程数来实现限流。
OpenResty
OpenResty 中的 resty.limit.req 库。
下面示例代码使用名为 my_limit_req_store
的共享字典来存放统计数据,并把每秒的速率设置为 200。这样,如果超过 200 但小于 300(这个值是 200 + 100 计算得到的) 的话,就需要排队等候;如果超过 300 的话,就会直接拒绝。
动态限流策略
动态限流策略是现代应用设计的重要方面,尤其是在动态和不可预测的环境中。与静态限流不同,静态限流对所有请求实施固定速率,动态限流会根据实时条件进行调整,确保资源的最佳利用,提高用户体验并保持系统的稳定性。相信功能更强大,效果更好的动态限流策略会是未来一段时间内的趋势目标。
自适应阈值: 动态限流的一种常见方法是使用自适应阈值。这涉及持续监控诸如请求成功率、响应时间和错误率等各种指标。基于这些指标,限流器会动态调整允许的请求速率。例如,在高流量期间,系统可能会暂时降低速率以防止过载。
基于 AI 的限流: 可以利用人工智能(AI)和机器学习(ML)来预测流量模式并检测异常。通过分析历史数据并监控当前流量,系统可以动态调整速率限制以适应预期的变化。基于 AI 的限流可以更好地区分合法和恶意的流量。
行为分析: 动态限流还可以涉及分析用户行为。通过考虑会话长度、点击模式或导航历史等因素,系统可以动态调整速率限制。这有助于提供更好的用户体验,同时阻止潜在的滥用行为。
预测性负载均衡: 动态限流可以与预测性负载均衡一起使用。通过预测流量激增或工作负载变化,系统可以主动调整不同服务器实例或微服务的速率限制。这可以防止单个组件被淹没,同时优化资源使用。
结论
在当今复杂的数字化环境中,限流在维护软件系统的稳定性和可用性方面发挥着关键作用。它可以防止出现过载、资源耗尽和潜在的安全漏洞等问题。
通过实施有效的限流策略,开发人员可以确保在用户之间公平分配资源,防止任何单个用户或组件垄断资源。这不仅提升了整体用户体验,还可以在流量激增或分布式拒绝服务(DDoS)攻击期间防止系统减速或崩溃。
量身定制的限流方法可以根据应用程序的特定特征和流量模式进行微调。这种适应性确保系统可以动态响应需求变化,同时防止出现瓶颈或低效现象。
此外,限流有助于优化资源利用。通过根据预定规则高效分配资源,系统可以充分发挥其潜力,而不会将宝贵的资源浪费在不必要的请求或滥用行为上。
总之,限流对于现代软件系统以可靠和高效的方式运行至关重要。开发人员和架构师需要制定量身定制的限流策略,增强系统的韧性,提升用户满意度,并优化资源分配。
参考文献
https://en.wikipedia.org/wiki/Leaky_bucket#
https://en.wikipedia.org/wiki/Token_bucket#
版权声明: 本文为 InfoQ 作者【Steven】的原创文章。
原文链接:【http://xie.infoq.cn/article/34e6753fa7d6e5b9967e9352b】。文章转载请联系作者。
评论