在前面的文章中,我已经介绍了 Sentinel 中的概念,以及所提供的各类功能如何使用。从本篇文章开始,我们将深入到源码中,自顶向下地介绍 Sentinel 整体的实现原理以及各个核心模块的实现原理。本文作为这一部分介绍的开篇,我会先介绍一下 Sentinel 的整体设计思想,以及下层包含的各个模块,后续的文章中会详细地介绍各个核心模块的实现原理。更多相关文章和其他文章均收录于贝贝猫的文章目录。
接下来,我们会分别从数据和处理过程这两个角度介绍 Sentinel 的设计原理,首先,我们先介绍一下 Sentinel 中内部是如何组织数据的,通过对数据结构的观察,大家就能清楚地认识到 Sentinel 是如何实现链路流控、关联流控、直接流控、访问源流控这些功能的了。
这里我们以一次完整的使用流程出发,展开对 Sentinel 数据部分的介绍。在下面的例子中,我们先定义代码执行的 Context,然后调用 SphU.entry
入口函数,在执行完正常业务逻辑后,我们从 entry 中退出,如果期间出现了异常,则通过 Tracer.trace
统计异常。
// 处理 HTTP 请求
public String handleHttpRequest(String originIp) {
ContextUtil.enter("HTTPResource1Entrance", originIp);
return doSomeThing();
}
// 处理 RPC
public String handleRPC(String consumerName) {
ContextUtil.enter("RPCResource1Entrance", consumerName);
return doSomeThing();
}
public String doSomeThing(String param, String origin) {
Entry entry1 = null;
try {
entry1 = SphU.entry("Resource1");
// do some thing
doOtherThing();
return "Result";
} catch (BlockException e) {
return "System busy";
} catch (Exception ex) {
Tracer.trace(ex);
} finally {
if (entry1 != null) {
entry1.exit();
}
}
}
public void doOtherThing(String param) {
Entry entry2 = null;
try {
entry2 = SphU.entry("Resource2");
// do other thing
} catch (BlockException e) {
throw e;
} catch (Exception ex) {
Tracer.trace(ex);
} finally {
if (entry2 != null) {
entry2.exit();
}
}
}
在上面的例子中,主要涉及到了 Sentinel 中几个核心的概念,Resource, Entry,Context。
SphU.entry
接口将其定义为一个资源,SphU.entry
接口的第一个参数描述了该资源的名称。Sentinel 会创建一些和该资源相关的统计节点,它们肩负着保存各类统计量的责任(QPS, Exception数量等)。这里大家可能会有疑问:为什么一个资源需要多个统计节点呢?因为 Sentinel 所支持的限流维度很多,比如针对访问源的针对链路的等等,这每一个维度都需要一个独立的统计节点,就比如上例中,我将 doSomeThing
这个函数定义为一个名为 Resource1
的资源,那么当执行了 doSomeThing
后,Sentinel 内部就会有和 Resource1
相关的各类统计节点,这也就意味着后续只要我们添加了针对 Resource1
资源的流控规则,就能限制 doSomeThing
这个函数的调用。HTTPResource1Entrance
和 RPCResource1Entrance
就是两个不同的根节点,ContextUtil.enter
接口的第一个参数描述了该调用链路的根节点的名称,如果没有显式地调用 ContextUtil.enter
接口,那么 Sentinel 会以 sentinel_default_context
作为默认根节点,Sentinel 会保证每个名称的根节点实例是唯一的ContextUtil.enter
接口的第二个参数指定,一般会将 Consumer name 或者 consumer IP 地址设定为调用源在下面这张图中,大家可以看到上例中 Context, Entry, Resource 数据之间是如何组织的,其中 Context 主要用来保存当前调用树的 Origin,调用树根节点,以及当前执行到的调用点。而整个调用链路是通过 Entry 的父子指针来描述的,同时 Entry 中还会保存相同根节点下访问当前资源的统计节点(我称之为 DefaultNode),Sentinel 的链路模式流控功能就是通过这个统计节点实现的。为了统计某一资源的所有访问量,Sentinel 中还对上述统计量进行了聚合,聚合后的统计节点会包含某一资源在不同入口下的所有访问量(我称之为 ClusterNode),Sentinel 直接模式和关联模式流控功能就是通过这个统计节点实现的。最后,Sentinel 为了能够根据访问源进行流控还会针对每一个资源的每一类访问源分别进行流量统计(我称之为 OriginNode)。
上面的图中,旨在介绍 Context,Entry,和 Resource 各个统计节点之间的关系,而且各个统计节点(EntranceNode, DefaultNode, ClusterNode, OriginNode)之间的的关系并没有完整的展示出来。下面我将统计节点之间的关系单独抽了出来,通过下图大家应该能够更加清晰地理解各个统计节点之间的关系。
那么,上述的这些数据是通过什么方式组织在一起的呢?接下来我们就来介绍一下这个部分。
首先,在调用 ContextUtil.enter(xxx)
时,会创建对应的 EntranceNode 节点并保存在 ThreadLocal 的 Context 中。如果没有显式地调用 ContextUtil.enter(xxx)
的话,它会在 SphU.entry
接口中被自动调用,生成一个默认的 Context。
当执行到 SphU.entry
接口时,就到了 Sentinel 的核心骨架——处理链(ProcessorSlotChain)了,Sentinel 将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。Slot Chain 其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。其核心结构如下:
上图仅作为设计思想的展示,图中 Slot 的顺序已和最新版 Sentinel Slot Chain 顺序不一致,目前每一个 Resource 都会有一个对应的 ProcessorSlotChain,各个 Resource 的 ProcessorSlotChain 是隔离的。
上图中很明确的展示了各个 Slot 如何组织数据:
最后,总结一下各个统计节点的数据规模:
Sentinel 的整个项目结构都很清晰,下面我简单地介绍一下项目中各个模块的职责,在后续的文章中,我们会对主要的模块进行源码实现介绍。
[1] Sentinel GitHub 仓库
[2] Sentinel 官方 Wiki
[3] Sentinel 1.6.0 网关流控新特性介绍
[4] Sentinel 微服务流控降级实践
[5] Sentinel 1.7.0 新特性展望
[6] Sentinel 为 Dubbo 服务保驾护航
[7] 在生产环境中使用 Sentinel
[8] Sentinel 与 Hystrix 的对比
[9] 大流量下的服务质量治理 Dubbo Sentinel初涉
[10] Alibaba Sentinel RESTful 接口流控处理优化
[11] 阿里 Sentinel 源码解析
[12] Sentinel 教程 by 逅弈
[13] Sentinel 专题文章 by 一滴水的坚持