写点什么

Sentinel 支持异步调用链的实现原理

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

    阅读完需:约 6 分钟

RPC 框架和 Web 框架都将朝着异步化的方向发展,许多主流的框架或组件也都适配了主流的异步框架,如 WebFlux。


Sentinel 也不例外,早在 Sentinel 0.2.0 就引入了异步调用链的支持。

Sentinel 异步调用链的支持

Sentinel 支持异步实现资源指标数据统计,例如,在资源方法执行之前,也就是在执行 Entry#entry 方法时,统计请求总数、请求被拒绝总数等指标数据,自增并行占用线程数;在执行 Entry#exit 方法时,统计总耗时、请求成功总数、请求异常总数等指标数据,自减并行占用线程数,这些都不受异步的影响。


同步与异步最大的区别在于 Context 实例在调用链上的传递方式。同步可以使用 ThreadLocal 传递 Context 实例,而异步无法使用 ThreadLocal 传递 Context 实例。因此,只要解决异步环境下的 Context 实例在调用链上的传递问题,Sentinel 就能支持异步场景。


Sentinel 是在同步的基础上支持异步的,两者在使用上并没有太大差别,在异步场景下使用 Sentinel 保护资源只需要将调用的 SphU#entry 方法替换为 SphU#asyncEntry 方法即可。


SphU#asyncEntry 方法返回的 Entry 实例类型为 AsyncEntry,需要在异步资源方法执行完成后调用 AsyncEntry 实例的 exit 方法。


异步案例代码如下:

Sentinel 以最小的改动实现同一条调用链上支持同步与异步同时存在的场景。其中,最核心的两处改动分别是,新增区别同步环境下的 CtSph#entryWithPriority 方法的 CtSph#asyncEntryWithPriorityInternal 方法,以及新增区别同步环境下的 CtEntry 类的 AsyncEntry 实例。


CtSph#asyncEntryWithPriorityInternal 方法的源码如下:

其中,

①是为异步资源创建 AsyncEntry 实例,②和④与同步环境下的实现逻辑并无差异,因为异步是在同步的基础上实现的,两者的主要差异在于③和⑤。


③调用的是 AsyncEntry 实例的 initAsyncContext 方法,初始化用于异步环境下的 Context 实例;⑤调用的是 AsyncEntry 实例的 cleanCurrentEntryInLocal 方法,用于将当前异步资源的 Entry 实例从 Entry 双向链表中移除,因为异步方法被调用后就立即返回了,要确保前一个资源的后续资源能够正确获取前一个资源的 Entry 实例,而不是这个异步资源的 Entry 实例。


AsyncEntry 类是 Sentinel 为支持异步而新增的一个 Entry 实现类,继承了 CtEntry 类,其源码如下:


  • initAsyncContext 方法:创建用于异步环境下的 Context 实例。

  • cleanCurrentEntryInLocal 方法:将当前异步资源的 Entry 实例从 Entry 双向链表中移除。

  • trueExit 方法:在异步资源方法执行完成后、在调用 Entry 实例的 exit 方法时被调用,使用 initAsyncContext 方法创建的异步环境下的 Context 实例可完成与同步环境下 exit 方法相同的操作。


AsyncEntry 类新增的 asyncContext 字段实际上是父类 Context 字段的副本,用于在异步调用链上向下传递上下文信息,同时避免多线程非线程安全地读/写上下文信息。一条调用链上可能出现同步和异步都存在的场景,使用副本机制能够在同步场景下支持异步场景。


如下图所示,假设资源 A、C 是同步资源,资源 B 是异步资源,资源 A 是资源 B、C 的前驱节点,资源 A 与资源 B 在同一条调用链上,资源 A 与资源 C 也在同一条调用链上。如果资源 C 是在资源 A 调用资源 B 之后才被调用的,资源 B 就不能在方法执行完成后,回退同步环境下的 Context 实例的 curEntry 字段,使其指向前一个资源(资源 A)的 Entry 实例,否则会导致调用树上资源 A、B、C 在同一条调用链上,这是 AsyncEntry 新增 asyncContext 字段的意义。

Sentinel 维护 Entry 双向链表,用于构造正确的资源调用树。在 NodeSelectorSlot#entry 方法中,需要将当前资源的 DefaultNode 实例添加到调用链上前一个资源的 DefaultNode 实例的 childList 集合中,而 NodeSelectorSlot 需要从 Context 实例中获取当前资源的 Entry 实例指向的前一个资源的 Entry 实例,从而获取前一个资源的 DefaultNode 实例。


因为异步资源可能会在方法执行完成后调用其他资源方法,所以 asyncContext 能够将当前异步资源的 Entry 实例向下传递,构造出正确的 Entry 双向链表及资源调用树。


如下图所示,调用链(资源 A→资源 B→资源 D)上分别出现同步资源调用异步资源、异步资源调用同步资源的场景,如果资源 B 不是在异步完成后才更新 asyncContext 的 curEntry 指向,且资源 B 向下传递的 Context 实例不是资源 B 的 asyncContext,则资源 D 的 Entry 实例的父 Entry 指针会直接指向资源 A 的 Entry 实例,从而形成一条新的调用链(资源 A→资源 D)。


用户头像

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

公众号:程序员高级码农

评论

发布
暂无评论
Sentinel支持异步调用链的实现原理_Java_互联网架构师小马_InfoQ写作社区