写点什么

Sentinel 是如何实现资源指标数据统计的

  • 2023-06-10
    湖南
  • 本文字数:9736 字

    阅读完需:约 32 分钟

基于滑动窗口实现资源指标数据统计

Sentinel 是基于滑动窗口实现资源的实时指标数据统计的。如果要深入理解 Sentinel 的限流实现原理,就要先了解如何实现其指标数据的统计,例如,如何统计 QPS。


为了学起来比较简单,我们不直接分析 Sentinel 的源码,而是分析笔者从 Sentinel 中摘抄的且经过改造后的 qps-helper 工具包的代码。这两者总体上是一样的,只是笔者去掉了一些指标数据统计,将 Sentinel 一些自定义的类替换成了 JDK 提供的类,并封装成了通用的 QPS 统计工具包。


当然,也可以直接查看 Sentinel 的源码,其源码在 sentinel-core 模块的 slots 包下。

Bucket

Sentinel 使用 Bucket 统计一段时间内的各项指标数据,这些指标数据包括请求总数、成功总数、异常总数、总耗时、最小耗时等。一个 Bucket 可以记录 1 秒内的数据,也可以记录 10 毫秒内的数据,这由采样周期决定。采样周期就是每个 Bucket 的时间窗口大小。


在 qps-helper 中,Bucket 的实现类为 MetricBucket,只统计请求成功总数、请求异常总数、总耗时和最小耗时,代码如下:

  • counters:存储各项指标的计数,包括请求异常总数、请求成功总数和总耗时等。

  • minRt:只记录最小耗时。


MetricBucket 使用 LongAdder 数组记录一段时间内的各项指标数据,LongAdder 数组的每个元素分别记录请求成功总数、请求异常总数和总耗时,如下图所示:

提示:LongAdder 数组保证了数据修改的原子性,并且性能比 AtomicInteger 更好。


Sentinel 使用枚举类型 MetricEvent 的 ordinal 属性作为数组的下标,因为 ordinal 的值从 0 开始,按枚举元素的顺序递增,正好可以用作数组的下标。


在 qps-helper 中,MetricBucket 的 LongAdder 数组已经替换成 j.u.c 包下的 LongAdder 类,并且 MetricEvent 只保留了 EXCEPTION、SUCCESS 和 RT,代码如下:

  • EXCEPTION:请求异常总数指标,对应的数组下标为 0。

  • SUCCESS:请求成功总数指标,对应的数组下标为 1。

  • RT:总耗时指标,对应的数组下标为 2。


当需要获取 time-window(时间窗口)内所有 Bucket 统计请求成功总数、请求异常总数或总耗时时,可以根据 MetricEvent 从每个 Bucket 的 LongAdder 数组中获取对应的 LongAdder 实例,并调用 sum 方法计算总数,代码如下:

当需要 Bucket 记录一个成功请求、一个异常请求或一个处理请求的耗时时,可以根据 MetricEvent 从 Bucket 的 LongAdder 数组中获取对应的 LongAdder 实例,并调用其 add 方法,代码如下:

滑动窗口

如果想知道某个接口的每秒请求成功总数(successQPS)、每秒请求失败总数(exception QPS),以及处理每个请求的平均耗时(avg RT),则只需要设置滑动窗口的大小为 1 秒即可。但如果想获取最近 1 分钟内的某 1 秒的 QPS 指标数据,就需要将滑动窗口的大小设置为 1 分钟,采样总数设置为 60,采样周期设置为 1 秒,这时,该滑动窗口拥有 60 个 Bucket,每个 Bucket 可以统计 1 秒的指标数据,若想要获取某 1 秒的 Bucket,则只需要根据时间戳定位 Bucket 即可。


假设滑动窗口的大小为 1 分钟,采样总数为 60,采样周期为 1 秒,给定一个时间戳如何定位到 Bucket 呢?

首先将当前时间戳去掉毫秒部分得到当前的秒数,然后将得到的秒数与数组长度做取余运算,即可得到当前时间窗口的 Bucket 在数组中的位置,如下图所示:

定位当前时间戳对应的 Bucket 在滑动窗口的 Bucket 数组中的位置,其算法实现如下:

  • windowLengthInMs:Bucket 的时间窗口大小。

  • timeMillis:当前时间戳。

  • array:当前滑动窗口的 Bucket 数组。


想要获取连续的 1 分钟内的 Bucket 数据,就不能简单地从头开始遍历数组,正确做法是指定一个开始时间戳和结束时间戳,从开始时间戳开始计算 Bucket 存放在数组中的下标,根据下标获取 Bucket,然后在每次循环时将开始时间戳加上 1 秒,直到开始时间戳等于结束时间戳。


由于滑动窗口会随着时间向前滑动,Bucket 数组会被循环使用,当前时间戳与 1 分钟之前的时间戳和 1 分钟之后的时间戳都会被映射到数组中的同一个 Bucket,因此必须能够判断指定的时间戳是否在取得的 Bucket 的时间窗口内,这就要求数组中的每个元素都要存储 Bucket 的时间窗口的开始时间戳和时间窗口大小。

例如,当前时间戳是 1577017699235,Bucket 的时间窗口大小是 1 秒,将时间戳的毫秒部分全部替换为 0,就能得到 Bucket 的时间窗口的开始时间戳为 1577017699000。


计算 Bucket 的时间窗口的开始时间戳的代码如下:

  • windowLengthInMs:Bucket 的时间窗口大小。

  • timeMillis:当前时间戳。

WindowWrap

因为 Bucket 自身并不保存时间窗口信息,所以 Sentinel 给 Bucket 添加了一个包装类 WindowWrap,用于记录 Bucket 的时间窗口信息。WindowWrap 类的源码如下:

  • windowLengthInMs:Bucket 的时间窗口大小。• windowStart:时间窗口的开始时间戳。

  • value:被包装的 Bucket。


假设 Bucket 的时间窗口大小为 1 秒,那么 Bucket 统计的就是 1 秒内的请求成功总数、请求异常总数、总耗时等指标数据。如果时间窗口为[1577017699000,1577017699999],那么

1577017699000 就是该 Bucket 的时间窗口的开始时间戳(windowStart),因为 1 秒等于 1000 毫秒,所以 1000 毫秒就是该 Bucket 的时间窗口大小(windowLengthInMs)。


windowStart+windowLengthInMs=时间窗口结束时间给定一个时间戳,判断该时间戳是否在 Bucket 的时间窗口内的算法实现如下:

通过时间戳定位 Bucket

Bucket 用于统计各项指标数据,WindowWrap 类用于记录 Bucket 的时间窗口信息(包括时间窗口的开始时间戳和大小),而 WindowWrap 数组就是一个滑动窗口。


当收到一个请求时,可以根据收到请求时的时间戳和滑动窗口大小计算出一个索引值,从滑动窗口(WindowWrap 数组)中获取一个 WindowWrap 类,从而获取 WindowWrap 类包装的 Bucket,并调用 Bucket 实例的 add 方法统计指标。


根据当前时间戳定位 Bucket 的算法实现如下:

上述代码实现的功能:通过当前时间戳计算出当前时间窗口的 Bucket(New Bucket)在数组中的索引(cidx),以及 Bucket 的时间窗口的开始时间戳,并通过索引从数组中获取 Bucket(Old Bucket)。

  1. 当 cidx 处不存在 Bucket 时,创建一个新的 Bucket,并且确保线程被安全写入数组中的 cidx 处,将此 Bucket 返回。

  2. 当 Old Bucket 不为空,且 Old Bucket 的时间窗口的开始时间戳与当前计算得到的 NewBucket 的时间窗口的开始时间戳相等时,该 Bucket 就是当前要找的 Bucket,将此 Bucket 直接返回。

  3. 当计算出 New Bucket 的时间窗口的开始时间戳大于当前数组中的 cidx 处存储的 OldBucket 的时间窗口的开始时间戳时,可以复用这个 OldBucket,确保线程能够安全地重置 Bucket,并返回 Bucket。

  4. 当计算出 New Bucket 的时间窗口的开始时间戳小于当前数组中的 cidx 处存储的 OldBucket 的时间窗口的开始时间戳时,直接返回一个空的 Bucket,因为时间不会倒退。

获取当前时间戳的前一个 Bucket

根据当前时间戳计算出当前 Bucket 的时间窗口的开始时间戳,使用当前 Bucket 的时间窗口的开始时间戳减去 Bucket 的时间窗口大小就能定位出前一个 Bucket。


由于滑动窗口在实现上使用的数据结构是数组,数组的每个元素都会被循环使用,因此当前 Bucket 与前一个 Bucket 可能会相差一个完整的滑动窗口周期,如下图所示:

如果当前时间戳对应的 Bucket 的时间窗口的开始时间戳为 1595974702000,那么前一个 Bucket 的时间窗口的开始时间戳可能是 1595974701000,也可能是一个滑动窗口周期之前的时间戳 1595974641000。


因此,在获取当前 Bucket 的前一个 Bucket 时,需要将 Bucket 的时间窗口的开始时间戳与当前时间戳比较,如果跨越了一个滑动窗口周期,就说明这个 Bucket 不是我们想要的。

资源指标数据统计全解析

本节将结合前面所学的内容,以及基于滑动窗口实现资源指标数据统计,分析 Sentinel 是如何实现资源指标数据统计的。

节点选择器插槽

节点选择器插槽(NodeSelectorSlot)负责为资源的首次访问创建 DefaultNode 实例,以及修改 Context 实例的 curNode 字段指向当前资源的 DefaultNode 实例,将 DefaultNode 实例绑定到调用树上。因为后续的 ProcessorSlot 在逻辑上都需要依赖这个 ProcessorSlot,所以它被放在 ProcessorSlot 链表的第一个位置。NodeSelectorSlot 的源码如下:

如源码所示,map 字段是一个非静态字段,意味着每个 NodeSelectorSlot 实例都有一个 Map 实例。而 Sentinel 只会为一个资源创建一个 ProcessorSlotChain,一个 ProcessorSlotChain 又只会创建一个 NodeSelectorSlot,并且 map 字段缓存 DefaultNode 使用的 key 并非资源 ID,而是调用链入口名称。所以,map 字段的作用是缓存同一资源、不同调用链入口创建的 DefaultNode 实例。


Sentinel 会为同一资源创建多少个 DefaultNode 实例取决于有多少个入口节点不同的调用链包含这个资源,这就是为什么说一个资源可能有多个 DefaultNode 实例的原因。


为什么这么设计呢?举个例子,对于同一支付接口,我们既可以使用 Spring MVC 暴露给前端访问,也可以使用 Dubbo 暴露给其他内部服务调用。由于入口节点不同,支付接口会被两条调用链包含。针对这种情况,我们可以通过设置来限制从 Spring MVC 进来的流量,也就是对前端请求限流。


NodeSelectorSlot 类的 entry 方法最难理解的就是,将当前资源的 DefaultNode 实例绑定到调用树的如下代码:

这行代码可以分为两种情况来分析,接下来以 Sentinel 提供的 demo 为例进行分析。

1. 一般情况

Sentinel 的 sentinel-demo 模块提供了多种使用场景的 demo,我们以 sentinel-demo-springwebmvc 这个 demo 为例进行讲解。该 demo 下有一个 hello 接口,其代码如下。

提示:这里不需要添加任何规则,只是为了调试 Sentinel 的源码。


在启动 demo 后,使用浏览器访问 hello 接口,在 NodeSelectorSlot 类的 entry 方法的绑定调用树这一行代码下断点,观察此时 Context 实例的字段信息。在正常情况下,我们可以看到如下图所示的结果:

从上图中可以看出,此时调用链入口节点(entranceNode)的子节点(childList)为空,并且当前 CtEntry 实例(curEntry)的父(parent)、子(child)节点都是 Null。当绑定调用树这一行代码执行完成后,Context 实例的字段信息如下图所示:

从上图中可以看出,NodeSelectorSlot 为当前资源创建的 DefaultNode 实例被添加到了调用链入口节点(entranceNode)的子节点(childList)中。


此时,ROOT 节点、调用链入口节点及当前资源的 DefaultNode 节点构造成的调用树如下:

如果现在访问 demo 的其他接口,如访问 err 接口,则将会生成如下所示的调用树:

提示:名称为 sentinel_spring_web_context 的调用链入口节点将会存储 Web 项目中所有资源的 DefaultNode 节点。

2. 存在多次调用 SphU#entry 方法的情况

如果在一个服务中既添加了 Sentinel 的 WebMvc 适配模块的依赖,也添加了 Sentinel 的 OpenFeign 适配模块的依赖,并且使用了 OpenFeign 调用内部其他服务的接口,就会存在一次调用链路上出现多次调用 SphU#entry 方法的情况。


WebMvc 适配器在接收客户端请求时会调用一次 SphU#entry 方法,在处理客户端请求时可能需要使用 OpenFeign 调用内部其他服务的接口,那么在发起接口调用时,Sentinel 的 OpenFeign 适配器也会调用一次 SphU#entry 方法。


现在将 demo 的 hello 接口修改一下,将 hello 接口调用的 doBusiness 方法也作为资源,并使用 Sentinel 保护起来,改造后的 hello 接口代码如下:

我们可以将 doBusiness 方法看作远程调用。例如,使用 POST 方式调用第三方的接口,接口名称为 hello2,那么我们可以使用 POST:/hello2 作为资源名称,并将流量类型设置为 OUT 类型,将上下文名称设置为 my_context。


在启动 demo 后,使用浏览器访问 hello 接口。当代码执行到 apiHello 方法时,在 NodeSelectorSlot#entry 方法的绑定调用树这一行代码下断点,当绑定调用树这一行代码执行完成后,Context 实例的字段信息如下图所示。

从上图中可以看出,Sentinel 并没有创建名称为 my_context 的 Context 实例,因为当前调用链上已经存在 Context 实例,Sentinel 只是在调用链入口处创建了 Context 实例。


在执行 NodeSelectorSlot#entry 方法之前,由于还没有为名称为 POST:/hello2 的资源创建 ProcessorSlotChain,因此 SphU#entry 方法会为该资源创建一个 ProcessorSlotChain,并为该 ProcessorSlotChain 创建一个 NodeSelectorSlot。


当执行到 NodeSelectorSlot#entry 方法时,该方法就会为该资源创建一个 DefaultNode 实例,而将该资源的 DefaultNode 实例绑定到节点树后,该资源的 DefaultNode 实例就会成为 GET:/hello 资源的 DefaultNode 实例的子节点,此时调用树如下:

此时,当前调用链路上已经存在两个 CtEntry 实例,这两个 CtEntry 实例构造了一个双向链表,如下图所示:

虽然存在两个 CtEntry 实例,但此时 Context 实例的 curEntry 字段指向的是第二个 CtEntry 实例,第二个 CtEntry 实例是在 apiHello 方法调用 SphU#entry 方法时创建的。在执行完 doBusiness 方法后,需要调用当前 CtEntry#exit 方法,由该 CtEntry 将 Context 实例的 curEntry 字段还原为指向该 CtEntry 的父 CtEntry。

接下来分析 NodeSelectorSlot#entry 方法中的另一行代码,代码如下:

这行代码的作用是将当前创建的 DefaultNode 实例赋值给当前 CtEntry 实例的 curNode 字段。结合图 4.7 来理解,就是将资源 GET:/hello 的 DefaultNode 实例赋值给第一个 CtEntry 实例的 curNode 字段,将资源 POST:/hello2 的 DefaultNode 实例赋值给第二个 CtEntry 实例的 curNode 字段。

ClusterNode 构造器插槽

在一个资源的 ProcessorSlotChain 中,NodeSelectorSlot 负责为资源创建 DefaultNode 实例,而这个 DefaultNode 实例仅限同一入口的调用链使用。所以,一个资源可能会存在多个 DefaultNode 实例,那么想要获取一个资源的总 QPS,就必须遍历这些 DefaultNode 实例。出于性能考虑,Sentinel 会为每个资源创建一个全局唯一的 ClusterNode 实例,用于统计资源的全局指标数据。


与 NodeSelectorSlot 的职责相似,ClusterBuilderSlot 的职责是为资源创建全局唯一的 ClusterNode 实例,且仅在资源第一次被访问时创建。


ClusterBuilderSlot 在创建 ClusterNode 实例时,需要将 ClusterNode 实例赋值给 DefaultNode 实例的 clusterNode 字段,由 DefaultNode 实例持有 ClusterNode 实例,并由 DefaultNode 负责代理 ClusterNode 完成资源指标数据统计。


必须先有 DefaultNode 实例,才能将 ClusterNode 实例委托给 DefaultNode 实例,这就是 ClusterBuilderSlot 在 ProcessorSlotChain 中必须被放在 NodeSelectorSlot 之后的原因。


ClusterBuilderSlot 声明的字段如下:

因为一个资源只会有一个 ProcessorSlotChain,这就意味着 ClusterBuilderSlot 只会被创建一个,那么让 ClusterBuilderSlot 持有资源的 ClusterNode 实例,就可以省去每次都从 clusterNodeMap 静态字段中获取资源的 ClusterNode 实例的步骤,这当然也是出于性能方面的考虑。


ClusterBuilderSlot 类的 entry 方法的源码如下:

  1. 如果 clusterNode 字段为空,说明当前资源首次被访问,那么需要为资源创建一个全局唯一的 ClusterNode 实例。

  2. 将 ClusterNode 实例委托给资源的 DefaultNode 实例来统计资源指标数据,node 参数为 NodeSelectorSlot 传递过来的 DefaultNode 实例。

  3. 如果调用来源不为空,那么为当前调用来源创建一个 StatisticNode 实例。


ClusterBuilderSlot 将 ClusterNode 实例赋值给 DefaultNode 实例的 clusterNode 字段,后续的 ProcessorSlot 就能从 entry 方法的 node 参数中获取 ClusterNode 实例。DefaultNode 与 ClusterNode 的关系如图所示:

每个 ClusterNode 实例都有一个 Map 类型的字段,用来缓存调用来源(origin)与 StatisticNode 实例的映射,代码如下:

  • originCountMap:如果上游服务调用当前服务的接口将 origin 字段传递过来,那么 ClusterBuilderSlot 就会为 ClusterNode 实例创建一个 StatisticNode 实例,用来统计当前资源被该远程服务调用的指标数据。


提示:例如,上游服务在发送 HTTP 请求时,在请求头添加 S-user 参数,或者上游服务在发送 Dubbo RPC 调用时,在请求参数列表添加 application 参数,就能获取来源应用名称。


当我们想要查看哪个来源应用访问这个接口最频繁时,可以从 ClusterNode 实例的 originCountMap 字段,根据来源应用名称获取 StatisticNode 实例,从而获取 QPS,并据此实现按调用来源限流。

ClusterNode#getOrCreateOriginNode 方法的源码如下:

为了便于使用,ClusterBuilderSlot 会将调用来源的 StatisticNode 实例赋值给 CtEntry 实例的 originNode 字段,后续的 ProcessorSlot 可先调用 Context 实例的 getCurEntry 方法获取 CtEntry 实例,再调用 CtEntry 实例的 getOriginNode 方法即可获取该 StatisticNode 实例。


这里我们可以得出一个结论,如果自定义的 ProcessorSlot 需要用到调用来源的 StatisticNode,那么在构建 ProcessorSlotChain 时,必须将这个自定义的 ProcessorSlot 放在 ClusterBuilderSlot 之后。

资源指标数据统计插槽

StatisticSlot 是实现资源各项指标数据统计的处理器插槽,它与 NodeSelectorSlot、ClusterBuilderSlot 共同组成了资源指标数据统计流水线。


NodeSelectorSlot 负责为资源创建 DefaultNode 实例,并将 DefaultNode 实例向下传递给 ClusterBuilderSlot;ClusterBuilderSlot 则负责加工资源的 DefaultNode 实例,添加 ClusterNode 实例,然后将 DefaultNode 实例向下传递给 StatisticSlot,如图所示:

StatisticSlot 在统计指标数据之前会先调用后续的 ProcessorSlot,再根据后续 ProcessorSlot 判断是否需要拒绝当前请求的结果并决定记录哪些指标数据。StatisticSlot 的源码框架如下:

  • entry:先通过 fireEntry 方法调用后续的 ProcessorSlot#entry 方法,再根据后续的 ProcessorSlot 是否抛出 BlockException 来决定统计哪些指标数据,并将资源并行占用的线程数加 1。

  • exit:若无任何异常,则统计请求成功、请求执行耗时指标,并将资源并行占用的线程数减 1。


从 StatisticSlot#entry 方法的源码中可以看出,为什么 Sentinel 设计的责任链需要由前一个 ProcessorSlot 在 entry 方法或 exit 方法中调用 fireEntry 方法或 fireExit 方法以调用下一个 ProcessorSlot 的 entry 方法或 exit 方法,而不是使用 for 循环遍历调用 ProcessorSlot。因为每个 ProcessorSlot 都有权决定先等后续的 ProcessorSlot 执行完成再做自己的事情,还是先完成自己的事情再让后续的 ProcessorSlot 执行,这与流水线有所区别。

1. entry 方法

第一种情况:当后续的 ProcessorSlot 未抛出任何异常时,表示不需要拒绝当前请求,当前请求会被放行。


如果当前请求被放行,则需要将当前资源并行占用的线程数加 1,将当前时间窗口被放行的请求总数加 1,代码如下:

如果调用来源不为空,也将调用来源对应的 StatisticNode 的当前并行占用线程数加 1,将当前时间窗口被放行的请求数加 1,代码如下:


如果流量类型为 IN,则让统计整个应用所有流入类型流量的 ENTRY_NODE 自增并行占用的线程数、当前时间窗口被放行的请求数加 1,代码如下:

回调所有的 ProcessorSlotEntryCallback 的 onPass 方法,代码如下:

调用 StatisticSlotCallbackRegistry#addEntryCallback 静态方法注册

ProcessorSlotEntryCallback。ProcessorSlotEntryCallback 接口的定义如下:

  • onPass:该方法在请求被放行时被回调执行。

  • onBlocked:该方法在请求被拒绝时被回调执行。


第二种情况:捕获到 PriorityWaitException。


这是特殊情况,在需要对请求限流时,只有使用默认流量效果控制器才可能会抛出 PriorityWaitException,这部分内容将在讲解 FlowSlot 的实现源码时再做分析。


当捕获到 PriorityWaitException 时,说明当前请求已经被休眠了一段时间了,但还是允许请求通过的,只是不需要让 DefaultNode 实例统计这个请求了,只自增当前资源并行占用的线程数,同时,DefaultNode 实例也会让 ClusterNode 实例自增并行占用的线程数,最后会回调所有 ProcessorSlotEntryCallback#onPass 方法。这部分的源码如下:

第三种情况:捕获到 BlockException。


BlockException 只在需要拒绝请求时被抛出。捕获到 BlockException 时执行的代码如下:

  1. 当捕获到 BlockException 时,将异常保存到调用链上下文的当前 CtEntry 实例中,StatisticSlot 的 exit 方法会识别是统计请求异常指标还是统计请求被拒绝指标。

  2. 调用 DefaultNode#increaseBlockQps 方法自增请求被拒绝总数,将当前时间窗口的 block qps 这项指标数据的值加 1。

  3. 如果调用来源不为空,则让调用来源对应的 StatisticNode 实例统计的请求被拒绝总数加 1。

  4. 如果流量类型为 IN,则让 ENTRY_NODE 统计的请求被拒绝总数加 1。

  5. 回调所有的 ProcessorSlotEntryCallback#onBlocked 方法。


StatisticSlot 捕获 BlockException 只是为了统计请求被拒绝的总数,而 BlockException 还是会被向上抛出。抛出异常的目的是拦住请求,执行服务降级处理。


第四种情况:捕获到其他异常。


其他异常并非指业务异常,因为此时业务代码还未被执行,而业务代码抛出的异常,会通过调用 Tracer#trace 方法统计请求异常总数。


当捕获到非 BlockException 时,除 PriorityWaitException 外,其他类型的异常都进行同样的处理:让资源的 DefaultNode 实例自增当前时间窗口的请求异常总数;让调用来源的 StatisticNode 实例、统计所有 IN 类型流量的 ENTRY_NODE 自增当前时间窗口的请求异常总数。这部分的源码如下:

  1. 将异常保存到调用链上下文的当前 Entry 实例中。

  2. 调用 DefaultNode#increaseExceptionQps 方法统计异常指标,将当前时间窗口的 exception qps 这项指标数据的值加 1。

  3. 如果调用来源不为空,则让调用来源的 StatisticNode 实例统计异常指标。

  4. 如果流量类型为 IN,则让 ENTRY_NODE 统计异常指标。

  5. 抛出异常。

2. exit 方法

当 exit 方法被调用时,要么请求被拒绝,要么请求被放行且已经被执行完成,所以 exit 方法需要知道当前请求是否被正常执行完成,这正是 StatisticSlot 在捕获异常时将异常保存到当前 CtEntry 实例的原因。


exit 方法通过 Context 实例可以获取当前 CtEntry 实例,从当前 CtEntry 实例中可以获取 entry 方法中保存的异常。exit 方法的源码如下(有删减):

  1. exit 方法通过 Context 实例可以获取当前资源的 DefaultNode 实例,如果 entry 方法中未出现异常,则说明请求是正常完成的。

  2. 当计算耗时时,可以将当前时间减去调用链上当前 CtEntry 实例的创建时间的值作为请求的执行耗时。

  3. 在请求被正常完成的情况下,需要统计总耗时指标,增加当前请求的执行耗时,统计成功请求总数,将成功请求总数加 1。

  4. 如果调用来源不为空,则让调用来源的 StatisticNode 实例统计总耗时指标,增加当前请求的执行耗时,统计成功请求总数,将成功请求总数加 1。

  5. 恢复当前资源占用的线程数。

  6. 如果调用来源不为空,则恢复当前调用来源占用的线程数。

  7. 如果流量类型为 IN,则让 ENTRY_NODE 统计总耗时指标,增加当前请求的执行耗时,统计成功请求总数,将成功请求总数加 1,恢复占用的线程数。

  8. 回调所有 ProcessorSlotExitCallback#onExit 方法。

资源指标数据的收集过程

ClusterNode 是一个资源全局的指标数据统计节点,但我们并未在 StatisticSlot 的 entry 方法与 exit 方法中看到其被使用。这里实际上使用了委托模式,ClusterNode 被 ClusterBuilderSlot 委托给 DefaultNode 统计指标数据,如下述代码所示:

当请求被成功处理后,StatisticSlot 会调用 DefaultNode 实例的 addRtAndSuccess 方法增加请求处理成功总数和总耗时;DefaultNode 会先调用父类的 addRtAndSuccess 方法,再调用 ClusterNode 实例的 addRtAndSuccess 方法。ClusterNode 类与 DefaultNode 类都是 StatisticNode 类的子类。StatisticNode 类的 addRtAndSuccess 方法的源码如下:

rollingCounterInSecond 是一个秒级滑动窗口,rollingCounterInMinute 是一个分钟级滑动窗口,类型都为 ArrayMetric。分钟级滑动窗口共有 60 个 MetricBucket,每个 MetricBucket 都会被 WindowWrap 数组包装,用于统计 1 秒内的各项指标数据,如图所示:

当调用 rollingCounterInMinute 的 addSuccess 方法时,先由滑动窗口根据当前时间戳获取当前时间窗口的 MetricBucket 实例,再调用 MetricBucket 实例的 addSuccess 方法,将 success 这项指标的值加上方法参数 successCount 的值(一般是 1)。


Sentinel 在 MetricEvent 枚举类中定义了 Sentinel 会收集的指标数据。MetricEvent 枚举类的源码如下:

  • PASS 指标:请求被放行的总数。

  • BLOCK 指标:请求被拒绝的总数。

  • EXCEPTION 指标:异常的请求总数。

  • SUCCESS 指标:被成功处理的请求总数。

  • RT 指标:被成功处理的请求的总耗时。

  • OCCUPIED_PASS 指标:预通过总数(前一个时间窗口使用了当前时间窗口的 passQps)。


其他一些指标数据都可以通过以上指标数据计算得出,例如,被成功处理的请求的平均耗时可以根据被成功处理的请求的总耗时除以被成功处理的请求总数计算得出。

小结

本篇主要介绍了 Sentinel 如何实现基于滑动窗口统计资源的实时指标数据、Sentinel 资源指标数据统计流程分析,以及调用树的结构,同时分析了 NodeSelectorSlot、ClusterBuilderSlot 和 StatisticSlot 这几个处理器插槽的用途及它们之间的联系。

用户头像

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

公众号:程序员高级码农

评论

发布
暂无评论
Sentinel 是如何实现资源指标数据统计的_Java_互联网架构师小马_InfoQ写作社区