源码分析RateLimiter SmoothWarmingUp 实现原理(文末附流程图)

简介: 源码分析RateLimiter SmoothWarmingUp 实现原理(文末附流程图)

篇详细介绍SmoothBursty 的实现原理,本文将介绍带有预热机制的限速器实现原理。


本篇最大的亮点并不是简单对SmoothWarmingUp上的注释进行翻译,而是进行总结与提炼。

1、类图


611c76799b25a84a3875c0ed8bd1cbfb.png

从上文也详细介绍了 RateLimiter 相关的类图,本文就不详细介绍。


2、SmoothWarmingUp 创建流程


创建 SmoothWarmingUp 限速器的入口为 RateLimiter 的 create 方法,其代码如下:

RateLimiter#create

public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {  // @1
    checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod);
    return create(
        SleepingStopwatch.createFromSystemTimer(), permitsPerSecond, warmupPeriod, unit, 3.0);
}

代码@1:首先先来看一下参数列表:


  • double permitsPerSecond
    每秒发放许可数量,即所谓的QPS。
  • long warmupPeriod
    设置预热时间。
  • TimeUnit unit
    warmupPeriod 的时间单位。


代码@2:调用内部的重载方法创建 SmoothWarmingUp 。


RateLimiter#create

static RateLimiter create( SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit, double coldFactor) {
    RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);  // @1
    rateLimiter.setRate(permitsPerSecond); // @2
    return rateLimiter;
}

创建 SmoothWarmingUp 两个主要步骤分别是调用其构造方法首先创建 SmoothWarmingUp 实例,然后调用其 setRate 方法进行初始化速率。这里先突出 coldFactor,默认为 3.0,该属性的作用将在下文详细介绍。


我们先来重点探讨一下 setRate 方法的实现。最终会调用其父类 SmoothRateLimiter 的doSetRate 方法。


SmoothRateLimiter#doSetRate

final void doSetRate(double permitsPerSecond, long nowMicros) {
    resync(nowMicros);   // @1 
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;   
    this.stableIntervalMicros = stableIntervalMicros;   // @2
    doSetRate(permitsPerSecond, stableIntervalMicros);  // @3
}

代码@1:基于当前时间重置 SmoothRateLimiter 内部的 storedPermits(已存储的许可数量) 与 nextFreeTicketMicros(下一次可以免费获取许可的时间) 值,所谓的免费指的是无需等待就可以获取设定速率的许可,该方法对理解限流许可的产生非常关键,稍后详细介绍。


代码@2:根据 QPS 算出一个稳定的获取1个许可的时间。以一秒发放5个许可,即限速为5QPS,那发放一个许可的世界间隔为 200ms,stableIntervalMicros 变量是以微秒为单位。


代码@4:调用 SmoothRateLimiter 的抽象方法 doSetRate 设置速率,这里会调用 SmoothWarmingUp 的 doSetRate 方法。


在介绍 SmoothWarmingUp 的 doSetRate 方法之前,我们先来看一下 resync 方法的实现。


SmoothRateLimiter#resync

void resync(long nowMicros) {
    if (nowMicros > nextFreeTicketMicros) {  // @1 
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();  // @2
      storedPermits = min(maxPermits, storedPermits + newPermits);    // @3
      nextFreeTicketMicros = nowMicros;   // @4
    }
}

代码@1:如果当前已启动时间大于 nextFreeTicketMicros(下一次可以免费获取许可的时间),则需要重新计算许可,即又可以向许可池中添加许可。


代码@2:根据当前时间可增加的许可数量,由于 SmoothWarmingUp 实现了预热机制,平均生成一个许可的时间并不是固定不变的。具体由 coolDownIntervalMicros 方法实现,稍候详细介绍。


代码@3:计算当前可用的许可,将新增的这些许可添加到许可池,但不会超过其最大值。


代码@4:更新下一次可增加计算许可的时间。


SmoothWarmingUp#coolDownIntervalMicros

double coolDownIntervalMicros() {
    return warmupPeriodMicros / maxPermits;
}

这个方法的实现其实简单,用生成这些许可的总时间除以现在已经生成的许可数,即可得到当前时间点平均一个许可的生成时间。


接下来重点探讨 SmoothWarmingUp 的 doSetRate 方法。


为了方便理解 SmoothWarmingUp doSetRate 方法,我根据 SmoothWarmingUp 类的注释,结合代码,给出如下示例图:

6f1d3e83c769e5b805f710087ddffcbb.png

首先我们先来根据 SmoothWarmingUp 的相关注释来理解一下上述这张图的几个要点。


  • 图中有两个阴影面积,一个用 stable,另外一个warm up period。在预热算法中,这两个阴影面积的关系与冷却因子相关。
  • 冷却因子 coldFactor 表示的含义为 coldIntervalMicros 与  stableIntervalMicros 的比值。
  • warm up period 阴影面积 与 stable 阴影面积的比值等于 (coldIntervalMicros -  stableIntervalMicros ) / stableIntervalMicros ,例如 SmoothWarmingUp 固定的冷却因子为3,那么 coldIntervalMicros 与 stableIntervalMicros 的比值为 3,那  (coldIntervalMicros -  stableIntervalMicros ) / stableIntervalMicros 则为 2。
  • 在预热算法中与数学中的积分相关(笔者对这方面的数学知识一窍不通),故这里只展示结论,而不做推导,阴影 WARM UP PERIOD 的面积等于 warmupPeriod,那阴影stable的面积等于 warmupPeriod/2。
  • 存在如下等式 warmupPeriod/2 = thresholdPermits * stableIntervalMicros (长方形的面积)
  • 同样存在如下等式 warmupPeriod = 0.5 * (stableInterval + coldInterval) * (maxPermits - thresholdPermits) (梯形面积,(上底 + 下底 * 高 / 2) )


有了上述基本知识,我们再来看一下代码。


SmoothWarmingUp#doSetRate

void doSetRate(double permitsPerSecond, double stableIntervalMicros) { 
    double oldMaxPermits = maxPermits;
    double coldIntervalMicros = stableIntervalMicros * coldFactor;                // @1
    thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;    // @2
    maxPermits =
          thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);   // @3
    slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);  // @4
    if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        storedPermits = 0.0;
    } else {
        storedPermits =
            (oldMaxPermits == 0.0)
                ? maxPermits // initial state is cold
                : storedPermits * maxPermits / oldMaxPermits;    // @5
    }
}

代码@1:根据冷却因子(coldFactor)来计算冷却间隔(单位为微秒),等于冷却因子与 stableIntervalMicros 的乘积。从这里我们可以得出如下几个基本的概念。冷却因子 coldFactor 为 冷却间隔与稳定间隔的比例。


代码@2:通过  warmupPeriod/2 = thresholdPermits * stableIntervalMicros 等式,求出 thresholdPermits 的值。


代码@3:根据  warmupPeriod = 0.5 * (stableInterval + coldInterval) * (maxPermits - thresholdPermits)  表示可求出 maxPermits 的数量。


代码@4:斜率,表示的是从 stableIntervalMicros 到 coldIntervalMicros 这段时间,许可数量从 thresholdPermits 变为 maxPermits 的增长速率。


代码@5:根据 maxPermits 更新当前存储的许可,即当前剩余可消耗的许可数量。


3、SmoothWarmingUp acquire 流程


首先 acquire 的定义在其父类,这里是典型的模板模式,由其父类定义基本流程,由具体的子类实现其特定功能。RateLimiter 中的 acquire 方法如下:

public double acquire(int permits) {
    long microsToWait = reserve(permits);    // @1
    stopwatch.sleepMicrosUninterruptibly(microsToWait);   // @2
    return 1.0 * microsToWait / SECONDS.toMicros(1L);   // @3
}

代码@1:根据当前剩余的许可与本次申请的许可来判断本次申请需要等待的时长,如果返回0则表示无需等待。


代码@2:如果需要等待的时间不为0,表示触发限速,睡眠指定时间后唤醒。


代码@3:返回本次申请等待的时长。


接下来重点介绍 reserve 方法的实现原理。


RateLimiter#reserve

inal long reserve(int permits) {
    checkPermits(permits);
    synchronized (mutex()) {  // @1
      return reserveAndGetWaitLength(permits, stopwatch.readMicros()); // @2
    }
}

代码@1:限速器主要维护的重要数据字段(storedPermits),对其进行维护时都需要先获取锁。


代码@2:调用内部方法 reserveAndGetWaitLength 来计算需要等待时间。


继续跟踪 reserveAndGetWaitLength 方法。

final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);   // @1
    return max(momentAvailable - nowMicros, 0);  // @2
}

代码@1:根据当前拥有的许可数量、当前时间判断待申请许可最早能得到满足的最早时间,用momentAvailable 表示。


代码@2:然后计算 momentAvailable 与 nowMicros 的差值与0做比较,得出需要等待的时间。


继续跟踪 reserveEarliestAvailable方法,该方法在 RateLimiter 中一个抽象方法,具体实现在其子类 SmoothRateLimiter 中。


SmoothRateLimiter#reserveEarliestAvailable

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);   // @1
    long returnValue = nextFreeTicketMicros;
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // @2
    double freshPermits = requiredPermits - storedPermitsToSpend; // @3
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);  // @4
    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);  // @5
    this.storedPermits -= storedPermitsToSpend;    // @6
    return returnValue;
}

代码@1:在尝试申请许可之前,先根据当前时间即发放许可速率更新 storedPermits 与 nextFreeTicketMicros(下一次可以免费获取许可的时间)。


代码@2:计算本次能从 storedPermits 中消耗的许可数量,取需要申请的许可数量与当前可用的许可数量的最小值,用 storedPermitsToSpend 表示。


代码@3:如果需要申请的许可数量(requiredPermits)大于当前剩余许可数量(storedPermits),则还需要等待新的许可生成,用freshPermits 表示,即如果该值大于0,则表示本次申请需要阻塞一定时间。


代码@4:计算本次申请需要等待的时间,等待的时间由两部分组成,一部分是由 storedPermitsToWaitTime 方法返回的,另外一部分以稳定速率生成需要的许可,其需要时间为 freshPermits * stableIntervalMicros,稍后我们详细分析一下 storedPermitsToWaitTime 方法的实现。


代码@5:更新 nextFreeTicketMicros 为当前时间加上需要等待的时间。


代码@6:更新 storedPermits 的值,即减少本次已消耗的许可数量。


代码@7:请注意这里返回的 returnValue 的值,并没有包含由于剩余许可需要等待创建新许可的时间,即允许一定的突发流量,故本次计算需要的等待时间将对下一次请求生效。


接下来重点探讨一下 SmoothWarmingUp 的 storedPermitsToWaitTime 方法。


SmoothWarmingUp#SmoothWarmingUp

long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {  // @1
    double availablePermitsAboveThreshold = storedPermits - thresholdPermits;   // @2
    long micros = 0;
    if (availablePermitsAboveThreshold > 0.0) {  // @3
        double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);  // @31 
                // TODO(cpovirk): Figure out a good name for this variable.
                double length = permitsToTime(availablePermitsAboveThreshold)
                     + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);             // @32
                micros = (long) (permitsAboveThresholdToTake * length / 2.0);                                                      // @33
                permitsToTake -= permitsAboveThresholdToTake;                                                                          // @34
         }
        // measuring the integral on the left part of the function (the horizontal line)
        micros += (stableIntervalMicros * permitsToTake);   // @4
        return micros;
}

代码@1:首先介绍其两个参数的含义:


  • double storedPermits
    当前存储的许可数量。
  • double permitsToTake
    本次申请需要的许可数量。


代码@2:availablePermitsAboveThreshold ,当前超出 thresholdPermits 的许可个数,如果超过 thresholdPermits ,申请许可将来源于超过的部分,只有其不足后,才会从 thresholdPermits 中申请,这部分的详细逻辑见代码@3。


代码@3:如果当前存储的许可数量超过了稳定许可 thresholdPermits,即存在预热的许可数量的申请逻辑,其实现关键点如下:


  • 获取本次从预热区间申请的许可数量。
  • 从预热区间获取一个许可的时间其算法有点晦涩难懂,具体实现为@32~@34。


代码@4:从稳定区间获取一个许可的时间,就容易理解,为固定的 stableIntervalMicros 。


温馨提示:从预热区间计算获取多个许可的算法,与 slope 有关,笔者并未完成感悟(走过路过的朋友如果对这块比较熟悉,欢迎留言探讨),但至少我们需要明白的是,从 剩余许可(storedPermits)中申请许可时,优先消耗(大于thresholdPermits 的许可,即消耗 (thresholdPermits ~ maxPermit ) 之间的许可)。


SmoothWarmingUp 的 acquire 流程就介绍到这里了。


4、总结


SmoothWarmingUp 的 acquire 的流程与 SmoothBursty 类似,故其流程图与下图通用,主要的区别生成一个许可的时间有变化,主要是提供了预热机制。


55895934031e9811bc6d9411cc8177da.png


相关文章
|
NoSQL 网络协议 数据库
为什么 Lettuce 会带来更长的故障时间
本文详述了阿里云数据库 Tair/Redis 将使用长连接客户端在非预期故障宕机切换场景下的恢复时间从最初的 900s 降到 120s 再到 30s的优化过程,涉及产品优化,开源产品问题修复等诸多方面。
69367 11
为什么 Lettuce 会带来更长的故障时间
|
缓存 Java 索引
Elasticsearch的TermsQuery慢查询分析和优化
前言 本篇文章主要记录业务上的一个TermsQuery优化和分析的过程和一些思考。 在使用ES的时候,经常会遇到慢查询,这时候可以利用profile进行分析,当利用profile也查看不出什么端倪时候,可以尝试通过阅读代码查看查询为什么这么慢。如下是一个我们内部业务的一个慢查询,经常出现4s左右的延时,一模一样的查询,但是延时不一样,且很难复现。 { "from": 0,
3883 0
Elasticsearch的TermsQuery慢查询分析和优化
|
5月前
|
缓存 监控 算法
高并发系统下,如何用限流算法优雅地保护你的服务?
在微服务架构中,面对突发流量,限流成为保障系统稳定的关键手段。本文深入解析基于 Uber/Limit 的限流实现,重点讲解漏桶算法原理及其在实际场景中的应用。通过限流,我们不仅能控制请求流量,还能保护后端服务资源,与熔断机制协同工作,提升系统容错能力。文中还介绍了限流的最佳实践,包括分层限流、差异化策略、动态调整和优雅降级,帮助开发者构建更具弹性的服务。
246 0
高并发系统下,如何用限流算法优雅地保护你的服务?
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
2931 65
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
人工智能 自然语言处理 Java
《Java 与 OpenAI 协同:开启智能编程新范式》
本文探讨了如何通过Java API调用OpenAI模型,结合两者优势开拓智能化应用。Java具备跨平台性、稳定性和丰富类库,而OpenAI的GPT等模型拥有强大的语言处理能力。文章详细介绍了准备工作、请求构建与响应解析、优化调用及错误处理,并展示了智能客服、内容生成和数据分析等领域的实际应用案例,展望了未来更多拓展方向,如智能家居和金融科技。这一结合为开发者带来无限创新可能。
309 12
|
JSON 缓存 Java
优雅至极!Spring Boot 3.3 中 ObjectMapper 的最佳实践
【10月更文挑战第5天】在Spring Boot的开发中,ObjectMapper作为Jackson框架的核心组件,扮演着处理JSON格式数据的核心角色。它不仅能够将Java对象与JSON字符串进行相互转换,还支持复杂的Java类型,如泛型、嵌套对象、集合等。在Spring Boot 3.3中,通过优雅地配置和使用ObjectMapper,我们可以更加高效地处理JSON数据,提升开发效率和代码质量。本文将从ObjectMapper的基本功能、配置方法、最佳实践以及性能优化等方面进行详细探讨。
1109 2
|
缓存 NoSQL 程序员
高并发下的生存之道:如何巧妙化解热Key危机?
本文详细探讨了互联网高并发场景下的热Key问题及其解决方案。热Key即因频繁访问导致缓存压力激增,影响系统稳定性。作者小米介绍了多种应对策略,包括Redis集群、主从复制、本地缓存、限流及Key加随机值等技术手段,旨在帮助读者有效分散负载,确保服务稳定。此外,还提供了兜底逻辑如降级处理和预热机制,以应对突发流量。希望本文能帮助大家更好地理解和解决热Key问题。
349 1
高并发下的生存之道:如何巧妙化解热Key危机?
|
存储 监控 NoSQL
解锁Redis Stream新境界:高级用法大揭秘【二】
解锁Redis Stream新境界:高级用法大揭秘【二】
941 0
|
关系型数据库 数据处理 对象存储
实时计算 Flink版产品使用问题之定时器执行存在延迟好几个小时,该如何处理
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
自然语言处理 Java
ElasticSearch 实现分词全文检索 - match、match_all、multimatch查询
ElasticSearch 实现分词全文检索 - match、match_all、multimatch查询
1341 0