写点什么

热点参数限流功能的实现与流量效果控制

  • 2023-06-14
    湖南
  • 本文字数:3928 字

    阅读完需:约 13 分钟

热点参数限流

参数限流是指根据方法调用传递的参数实现限流,或者根据接口的请求参数限流,而热点参数限流是指对访问频繁的参数限流。


在电商场景中,每位顾客购买的商品不同,有主播带货的商品下单流量较大,而没有主播带货的商品相对来说下单流量较少。因为商品数量有限,每个下单请求都能成功是不太可能的,所以如果能够根据客户端请求传递的商品 ID 实现限流,将流量控制在商品的库存总量左右,并且使用 QPS 限流等进行兜底,即可将接口通过的有效流量最大化。

热点参数限流功能的实现

与围绕资源实现限流不同,热点参数限流是围绕资源的参数的不同取值来限流的,它不需要统计资源指标数据,而需要统计不同参数取值的指标数据。


因此,热点参数限流功能不在 Sentinel 的核心模块中,而是被当作一个扩展功能放在 Sentinel 的扩展功能模块——sentinelextension 中,其子模块为 sentinel-parameter-flow-control。

热点参数指标数据统计

热点参数限流不使用 Node 统计指标数据,而是使用 ParameterMetric 与 ParameterMetricStorage 统计指标数据。

  • ParameterMetric:用于实现类似于 ClusterNode 的统计功能。

  • ParameterMetricStorage:用于实现类似于 EntranceNode 的功能,管理和存储每个资源对应的 ParameterMetric。


ParameterMetric 有 3 个静态字段,源码如下:

  • ruleTimeCounters:用于实现匀速流量控制效果,key 为参数限流规则(ParamFlowRule),value 为参数不同取值对应的上次生产令牌的时间。

  • ruleTokenCounter:用于实现匀速流量控制效果,key 为参数限流规则(ParamFlowRule),value 为参数不同取值对应的当前令牌桶中的令牌数。

  • threadCountMap:key 为参数索引,value 为参数不同取值对应的当前并行占用的线程总数。


ParameterMetricStorage 使用 ConcurrentHashMap 缓存每个资源对应的 ParameterMetric,且只会为配置了参数限流规则的资源创建一个 ParameterMetric 实例,其部分源码如下:

ParameterMetricStorage 的 initParamMetricsFor 方法用于为资源创建 ParameterMetric 实例并初始化。该方法在资源被访问时由 ParamFlowSlot 调用,并且该方法只在为资源配置了参数限流规则的情况下被调用。

热点参数限流的实现原理

既然是参数限流,那么肯定需要获取参数,而 ProcessorSlot#entry 方法的最后一个参数就是请求传递过来的参数,该参数通过 SphU#entry 方法一层层向下传递。


回顾 Sentinel 的使用案例,代码如下:

在此案例中,如果导入了热点参数限流模块,并且为资源 GET:/hello 配置了热点参数规则,则调用 SphU#entry 方法时传入的 name 就是热点参数,且 name 最终会被传递给热点参数限流模块的 ProcessorSlot。


热点参数限流模块通过 JavaSPI 注册自定义的 SlotChainBuilder,即注册 HotParamSlotChainBuilder,将 ParamFlowSlot 放置在 StatisticSlot 的后面,这个 ParamFlowSlot 就是实现热点参数限流功能的处理器插槽,其部分源码如下:

如源码所示,在 ParamFlowSlot#entry 方法中,首先调用 ParamFlowRuleManager#hasRules 方法判断当前资源是否存在参数限流规则,如果存在,则调用 ParamFlowSlot#checkFlow 方法判断当前请求是否允许通过。ParamFlowSlot#checkFlow 方法的源码如下:


① checkFlow 方法的最后一个参数是请求参数,也就是调用 SphU#entry 方法传递进来的参数,如果参数为 null,则没有必要走后续逻辑。

② 调用 ParamFlowRuleManager#getRulesOfResource 方法获取为当前资源配置的所有参数

限流规则。

③ 遍历参数限流规则,首先调用 ParameterMetricStorage#initParamMetricsFor 方法判断是否需要为当前资源初始化创建 ParameterMetric 实例,然后调用 ParamFlowChecker#passCheck 方法判断当前请求是否可以被放行,如果需要拒绝请求,则抛出 ParamFlowException。

在阅读 ParamFlowChecker#passCheck 方法的源码之前,我们需要先了解参数限流规则的配置,了解每个配置项的作用。

设置参数限流规则的类名为 ParamFlowRule,其源码如下。

  • grade:限流规则的阈值类型,支持的类型与 FlowRule 相同。

  • count:阈值,支持的类型与 FlowRule 相同。

  • paramIdx:参数索引,ParamFlowChecker 根据限流规则的参数索引获取参数的值,下标从 0 开始。例如,apiHello(String name)方法只有一个参数,索引为 0 对应 name 参数。

  • ontrolBehavior:流量控制效果,支持的类型与 FlowRule 相同,但只支持快速失败和匀速排队。

  • maxQueueingTimeMs:实现匀速排队流量控制效果的虚拟队列最大等待时间,超过该值的请求被抛弃,支持的类型与 FlowRule 相同。

  • durationInSec:统计指标数据的时间窗口大小,单位为秒。

  • burstCount:支持的突发流量总数。


假设需要对资源 GET:/hello 的 name 参数限流,当 name 取值为 jackson 时,QPS 限流阈值为 5,则代码如下:

以此为例,我们继续分析 ParamFlowChecker#passCheck 方法的源码。passCheck 方法返回 true 表示放行,返回 false 表示拒绝。passCheck 方法的源码如下:

  1. 若参数为空,或者规则配置的参数索引越界,或者参数索引对应的参数值为空,则放行请求。

  2. 若是集群限流模式,则调用 passClusterCheck 方法,否则调用 passLocalCheck 方法。


我们先不讨论集群限流情况,仅看单机本地限流情况。passLocalCheck 方法的源码如下:

由于参数可能是基本数据类型,也可能是数组类型或引用类型,因此 passLocalCheck 方法分 3 种情况进行处理。这里只讨论其中一种情况,其他情况的处理方式与其类似。


以资源 GET:/hello 为例,其 apiHello 方法的 name 参数为 String 类型,因此会调用 passSingleValueCheck 方法。该方法的源码如下:

当规则配置的阈值类型为 QPS 时,根据限流效果调用 passThrottleLocalCheck 方法或 passDefaultLocalCheck 方法。

  1. 当规则配置的阈值类型为 Threads 时,获取当前资源的 ParameterMetric 实例,从而获取当前资源和参数取值对应的并行占用线程总数。如果并行占用线程总数加 1 大于限流阈值,则拒绝请求,否则放行请求。

  2. 并行占用线程总数是在哪里自增和自减的呢?


这是由 ParamFlowStatisticEntryCallback 与 ParamFlowStatisticExitCallback 这两个 Callback 实现的,这两个 Callback 分别在 StatisticSlot 的 entry 方法和 exit 方法中被回调执行。

流量效果控制

由于热点参数限流是围绕资源的参数取值并参照同参数不同取值的实时指标数据实现限流的,因此 Sentinel 核心模块中为实现限流功能而提供的几种流量效果控制器并不适合热点参数限流使用。


Sentinel 只为热点参数限流提供了两种流量效果控制策略,分别是快速失败(直接拒绝)和匀速排队。

快速失败

快速失败基于令牌桶算法实现,其操作如下:由 ParamFlowChecker 控制每个时间窗口只生产一次令牌,将令牌放入令牌桶中,每个请求都从令牌桶中取走令牌,当令牌足够时放行请求,当令牌不足时拒绝请求。

ParameterMetric 类的 tokenCounters 字段被用作令牌桶;timeCounters 字段用于存储最近一次生产令牌的时间。


ParamFlowChecker#passDefaultLocalCheck 方法的源码如下:



  1. 根据资源 ID 获取 ParameterMetric 实例,从 ParameterMetric 实例中获取当前限流规则的令牌桶和最近一次生产令牌的时间,并将时间精确到毫秒。

  2. 计算限流阈值,即令牌桶能够存放的最大令牌总数(tokenCount)。

  3. 重新计算限流阈值,将当前限流阈值加上允许突增流量的数量。

  4. 获取当前时间,如果当前参数值首次出现“?”,则初始化生产令牌,并立即使用。

  5. 获取当前时间与上次生产令牌的时间间隔,如果时间间隔大于一个时间窗口,则见第 6 条,否则见 7 条。

  6. 计算需要生产的令牌总数,并与当前令牌桶中剩余的令牌数相加得到新的令牌总数,如果新的令牌总数大于限流阈值,则使用限流阈值作为新的令牌总数,并且在令牌生产完成后立即使用,最后更新最近一次生产令牌的时间。

  7. 从令牌桶中获取令牌,如果获取成功,则放行当前请求,否则拒绝当前请求。

匀速排队

与 RateLimiterController 的实现原理一样,ParamFlowChecker 让请求在虚拟队列中排队,控制请求通过的时间间隔,且该时间间隔可以通过阈值与时间窗口大小计算出来。如果当前请求计算出来的排队等待时间大于限流规则配置的最大排队等待时间(maxQueueingTimeMs),则拒绝当前请求。


ParamFlowChecker#passThrottleLocalCheck 方法的源码如下:


  1. 当流量控制效果为匀速排队时,ParameterMetric 实例的 ruleTimeCounters 方法记录的是最后一个请求的期望通过时间。

  2. 计算限流阈值。

  3. 计算请求通过的时间间隔。例如,当 acquireCount 等于 1、限流阈值配置为 200QPS 且时间窗口大小为 1 秒时,计算出来的 costTime 等于 5 毫秒,即每 5 毫秒只允许通过一个请求。

  4. 计算当前请求的期望通过时间,值为最近一次请求的期望通过时间与请求通过的时间间隔之和,而最近一次请求的期望通过时间就是虚拟队列中队列尾部的那个请求的期望通过时间。

  5. 排队等待时间等于期望通过时间与当前时间的间隔,如果排队等待时间大于限流规则配置的最大等待时间,则拒绝当前请求,否则将当前请求“放入”虚拟队列中等待,并计算出当前请求需要等待的时间,让当前线程休眠指定时长之后再放行该请求。

总结

本篇主要分析了热点参数限流的实现原理,并介绍了同参数不同取值的实时指标数据统计的实现原理,以及热点参数限流提供的两种流量效果控制策略的实现原理。


虽然 Sentinel 为热点参数限流提供了一个滑动窗口用于收集指标数据,但是该滑动窗口并未被使用,而是使用了 ParameterMetric 与 ParameterMetricStorage,这应该是出于性能的考虑。


热点参数限流对性能的影响和对内存的占用与参数的取值有多少种可能成正比,限流参数的取值可能性就越多,占用的内存就越大,对性能的影响也就越大。在使用热点参数限流功能时,一定要考虑参数的取值。


如果限流阈值类型为 Threads,则不会存在占用内存过大的问题,这是因为 ParamFlowStatisticExitCallback 会调用资源的 ParameterMetric 实例的 decreaseThreadCount 方法扣减参数值占用的线程数,当线程数为零时,会将当前参数值对应的 key-value 从 CacheMap 中移除。

用户头像

加VX:bjmsb02 凭截图即可获取 2020-06-14 加入

公众号:程序员高级码农

评论

发布
暂无评论
热点参数限流功能的实现与流量效果控制_互联网架构师小马_InfoQ写作社区