了解 Sentinel 的一些概念资源资源是 Sentinel 的关键概念。资源可以是一个方法、一段代码、由应用提供的接口或者由应用调用其它应用的接口。规则围绕资源的实时状态设定的规则包括流量控制规则、熔断降级规则以及系统保护规则、自定义规则。降级在流量剧增的情况下为保证系统能够正常运行根据资源的实时状态、访问流量以及系统负载有策略的拒绝掉一部分流量。Sentinel 资源指标数据统计相关的类Sentinel 中指标数据统计以资源为维度。资源使用 ResourceWrapper 对象表示我们把 ResourceWrapper 对象称为资源 ID。如果一个资源描述的是一个接口那么资源名称通常就是接口的 url例如“GET:/v1/demo”。ResourceWrapperpublic abstract class ResourceWrapper { protected final String name; protected final EntryType entryType; protected final int resourceType; public ResourceWrapper(String name, EntryType entryType, int resourceType) { this.name name; this.entryType entryType; this.resourceType resourceType; } }ResourceWrapper 有三个字段name 为资源名称例如“GET:/v1/demo”。entryType 为流量类型即流入流量还是流出流量通俗点说就是发起请求还是接收请求。resourceType 表示资源的类型例如 Dubbo RPC、Web MVC 或者 API Gateway 网关。EntryType 是一个枚举类型public enum EntryType { IN(IN), OUT(OUT); }可以把 IN 和 OUT 简单理解为接收处理请求与发送请求。当接收到别的服务或者前端发来的请求那么 entryType 为 IN当向其他服务发起请求时那么 entryType 就为 OUT。例如在消费端向服务提供者发送请求当请求失败率达到多少时触发熔断降级那么服务消费端为实现熔断降级就需要统计资源的 OUT 类型流量。Sentinel 目前支持的资源类型有以下几种public final class ResourceTypeConstants { public static final int COMMON 0; public static final int COMMON_WEB 1; public static final int COMMON_RPC 2; public static final int COMMON_API_GATEWAY 3; public static final int COMMON_DB_SQL 4; }COMMON默认COMMON_WEBWeb 应用的接口COMMON_RPCDubbo 框架的 RPC 接口COMMON_API_GATEWAY用于 API Gateway 网关COMMON_DB_SQL数据库 SQL 操作NodeNode 用于持有实时统计的指标数据Node 接口定义了一个 Node 类所需要提供的各项指标数据统计的相关功能为外部屏蔽滑动窗口的存在。提供记录请求被拒绝、请求被放行、请求处理异常、请求处理成功的方法以及获取当前时间窗口统计的请求总数、平均耗时等方法。Node 接口源码如下。public interface Node extends OccupySupport, DebugSupport { long totalRequest(); // 获取总的请求数 long totalPass(); // 获取通过的请求总数 long totalSuccess(); // 获取成功的请求总数 long blockRequest(); // 获取被 Sentinel 拒绝的请求总数 long totalException(); // 获取异常总数 double passQps(); // 通过 QPS double blockQps(); // 拒绝 QPS double totalQps(); // 总 qps double successQps(); // 成功 qps double maxSuccessQps(); // 最大成功总数 QPS例如秒级滑动窗口的数组大小默认配置为 2则取数组中最大 double exceptionQps(); // 异常 QPS double avgRt(); // 平均耗时 double minRt(); // 最小耗时 int curThreadNum(); // 当前并发占用的线程数 double previousBlockQps(); // 前一个时间窗口的被拒绝 qps double previousPassQps(); // 前一个时间窗口的通过 qps Map metrics(); List rawMetricsInMin(Predicate timePredicate); void addPassRequest(int count); // 添加通过请求数 void addRtAndSuccess(long rt, int success); // 添加成功请求数并且添加处理成功的耗时 void increaseBlockQps(int count); // 添加被拒绝的请求数 void increaseExceptionQps(int count); // 添加异常请求数 void increaseThreadNum(); // 自增占用线程 void decreaseThreadNum(); // 自减占用线程 void reset(); // 重置滑动窗口 }它的几个实现类DefaultNode、ClusterNode、EntranceNode、StatisticNode 的关系如下图所示。StatisticNodeStatistic 即统计的意思StatisticNode 是 Node 接口的实现类是实现实时指标数据统计 Node。public class StatisticNode implements Node { // 秒级滑动窗口2 个时间窗口大小为 500 毫秒的 Bucket private transient volatile Metric rollingCounterInSecond new ArrayMetric(2,1000); // 分钟级滑动窗口60 个 Bucket 数组每个 Bucket 统计的时间窗口大小为 1 秒 private transient Metric rollingCounterInMinute new ArrayMetric(60, 60 * 1000, false); // 统计并发使用的线程数 private LongAdder curThreadNum new LongAdder(); }如代码所示一个 StatisticNode 包含一个秒级和一个分钟级的滑动窗口以及并行线程数计数器。秒级滑动窗口用于统计实时的 QPS分钟级的滑动窗口用于保存最近一分钟内的历史指标数据并行线程计数器用于统计当前并行占用的线程数。StatisticNode 的分钟级和秒级滑动窗口统计的指标数据分别有不同的用处。例如StatisticNode 记录请求成功和请求执行耗时的方法中调用了两个滑动窗口的对应指标项的记录方法代码如下Override public void addRtAndSuccess(long rt, int successCount) { // 秒级滑动窗口 rollingCounterInSecond.addSuccess(successCount); rollingCounterInSecond.addRT(rt); // 分钟级滑动窗口 rollingCounterInMinute.addSuccess(successCount); rollingCounterInMinute.addRT(rt); }获取前一秒被 Sentinel 拒绝的请求总数从分钟级滑动窗口获取代码如下Override public double previousBlockQps() { return this.rollingCounterInMinute.previousWindowBlock(); }而获取当前一秒内已经被 Sentinel 拒绝的请求总数则从秒级滑动窗口获取代码如下Override public double blockQps() { return rollingCounterInSecond.block() / rollingCounterInSecond.getWindowIntervalInSec(); }获取最小耗时也是从秒级的滑动窗口取的代码如下Override public double minRt() { // 秒级滑动窗口 return rollingCounterInSecond.minRt(); }由于方法比较多这里就不详细介绍每个方法的实现了。StatisticNode 还负责统计并行占用的线程数用于实现信号量隔离按资源所能并发占用的最大线程数实现限流。当接收到一个请求就将 curThreadNum 自增 1当处理完请求时就将 curThreadNum 自减一如果同时处理 10 个请求那么 curThreadNum 的值就为 10。假设我们配置 tomcat 处理请求的线程池大小为 200通过控制并发线程数实现信号量隔离的好处就是不让一个接口同时使用完这 200 个线程避免因为一个接口响应慢将 200 个线程都阻塞导致应用无法处理其他请求的问题这也是实现信号量隔离的目的。DefaultNodeDefaultNode 是实现以资源为维度的指标数据统计的 Node是将资源 ID 和 StatisticNode 映射到一起的 Node。public class DefaultNode extends StatisticNode { private ResourceWrapper id; private volatile Set childList new HashSet(); private ClusterNode clusterNode; public DefaultNode(ResourceWrapper id, ClusterNode clusterNode) { this.id id; this.clusterNode clusterNode; } }如代码所示DefaultNode 是 StatisticNode 的子类构造方法要求传入资源 ID表示该 Node 用于统计哪个资源的实时指标数据指标数据统计则由父类 StatisticNode 完成。DefaultNode 字段说明id资源 IDResourceWrapper 对象。childListchildList 是一个 NodeDefaultNode集合用于存放子节点。clusterNodeclusterNode 字段是一个 ClusterNodeClusterNode 也是 StatisticNode 的子类。我们回顾下 Sentinel 的基本使用ContextUtil.enter(上下文名称例如sentinel_spring_web_context); Entry entry null; try { entry SphU.entry(资源名称例如/rpc/openfein/demo, EntryType.IN (或者 EntryType.OUT)); // 执行业务方法 return doBusiness(); } catch (Exception e) { if (!(e instanceof BlockException)) { Tracer.trace(e); } throw e; } finally { if (entry ! null) { entry.exit(1); } ContextUtil.exit(); }如上代码所示doBusiness 业务方法被 Sentinel 保护当 doBusiness 方法被多层保护时就可能对同一个资源创建多个 DefaultNode。一个资源理论上可能有多个 DefaultNode是否有多个 DefaultNode 取决于是否存在多个 Context即当前调用链路上是否多次调用 ContextUtil#enter 方法是否每次调用 ContextUtil#enter 方法都会创建一个 Context。特别加上“理论上”是因为在一个线程中调用多次 ContextUtil#enter 方法只有第一次调用会创建 ContextContextUtil 使用 ThreadLocal 存储 Context所以后续的调用都会使用之前创建的 Context而 DefaultNode 是在 NodeSelectorSlot 中创建的使用 Map 缓存key 为 Context#name所以在使用同一个 Context 的情况下只会为一个资源创建一个 DefaultNode。这部分内容在介绍 NodeSelectorSlot 时再作详细介绍。ClusterNodeSentinel 使用 ClusterNode 统计每个资源全局的指标数据以及统计该资源按调用来源区分的指标数据。全局数据指的是不区分调用链路一个资源 ID 只对应一个 ClusterNode。public class ClusterNode extends StatisticNode { // 资源名称 private final String name; // 资源类型 private final int resourceType; // 来源指标数据统计 private Map originCountMap new HashMap(); // 控制并发修改 originCountMap 用的锁 private final ReentrantLock lock new ReentrantLock(); public ClusterNode(String name, int resourceType) { this.name name; this.resourceType resourceType; } }ClusterNode 字段说明name资源名称。很奇怪这里没有使用 ResourceWrapper是版本历史问题还是因为 ClusterNode 不需要判断流量类型。resourceType资源类型。originCountMap维护每个调用来源的指标数据统计数据StatisticNode其用途是什么在使用到时再做分析。EntranceNodeEntranceNode 是一个特殊的 Node它继承 DefaultNode用于维护一颗树从根节点到每个叶子节点都是不同请求的调用链路所经过的每个节点都对应着调用链路上被 Sentinel 保护的资源一个请求调用链路上的节点顺序正是资源被访问的顺序。public class EntranceNode extends DefaultNode { public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) { super(id, clusterNode); } }在一个 Web MVC 应用中每个接口就是一个资源Sentinel 通过 Spring MVC 拦截器拦截每个接口的入口统一创建名为“sentinel_spring_web_context”的 Context名称相同的 Context 都使用同一个 EntranceNode。一个 Web 应用可能有多个接口而 childList 就用于存储每个接口对应的 DefaultNode。如果想统计一个应用的所有接口不一定是所有没有被调用过的接口不会创建对应的 DefaultNode总的 QPS只需要调用 EntranceNode 的 totalQps 就能获取到。EntranceNode 的 totalQps 方法代码如下Override public double totalQps() { double r 0; // 遍历 childList for (Node node : getChildList()) { r node.totalQps(); } return r; }EntranceNode、DefaultNode、ClusterNode 与滑动窗口的关系如下图所示Sentinel 中的 Context 与 Entry理解 Context 与 Entry 也是理解 Sentinel 整个工作流程的关键其中 Entry 还会涉及到“调用树”这一概念。ContextContext 代表调用链路上下文贯穿一次调用链路中的所有 Entry。Context 维持着入口节点entranceNode、本次调用链路的 curNode、调用来源origin等信息。Context 名称即为调用链路入口名称。Context 通过 ThreadLocal 传递只在调用链路的入口处创建。假设服务B提供一个查询天气预报的接口给服务A调用服务B实现查询天气预报的接口是调用第三方服务C实现的服务B是一个 MVC 应用同时服务B调用服务 C 接口使用 OpenFeign 实现 RPC 调用。那么服务 B 即使用了 Sentinel 的 MVC 适配模块也使用了 Sentinel 的 OpenFeign 适配模块。当服务 B 接收到服务 A 请求时会创建一个名为“sentinel_spring_web_context”的 Context服务 B 在向服务 C 发起接口调用时由于当前线程已经存在一个 Context所以还是用“sentinel_spring_web_context”这个 Context代表是同一个调用链路入口。举个不恰当的例子路径一A.a()- 调用 - B.b()- 调用 C.c()A.a() 为该调用链路的入口入口名称为“a_context”。路径二D.d()- 调用 - B.b()- 调用 C.c()D.d() 为该调用链路的入口入口名称为“d_context”。那么每次调用 A.a() 方法都会创建名为“a_context”的 Context每次调用 B.b() 方法都会创建名为“b_context”的 Context。如果 A.a() 同时有 20 个请求那么就会创建 20 个名为“a_context”的 ContextContext 代表了这 20 个请求每个请求的调用链路上下文而路径一就是这 20 个请求相同的调用链路。Context 的字段定义如下public class Context { private final String name; private DefaultNode entranceNode; private Entry curEntry; private String origin ; // 我们不讨论异步的情况 // private final boolean async; }nameContext 的名称。entranceNode当前调用树的入口节点类型为 EntranceNode。同一个入口的资源每个资源对应一个 DefaultNodeentranceNode#childList 用于存储这些资源的 DefaultNode。curEntry当前 EntryCtEntry。origin调用来源的名称即服务消费者的名称或者服务消费者的来源 IP取决于服务消费者是否使用 Sentinel由 Sentinel 适配层传递过来。例如服务提供者是 Spring MVC 应用且服务提供者使用 Sentinel 的 Web MVC 适配那么 Sentinel 会尝试从请求头获取”S-user”如果服务消费者有在请求头传递这个参数那么就能够获取到。EntryCtEntry在调用 Context#getCurNode 方法获取调用链路上当前访问到的资源的 DefaultNode 时实际是从 Context#curEntry 获取的Entry 维护了当前资源的 DefaultNode以及调用来源的 StatisticNode。Entry 抽象类字段的定义如下。public abstract class Entry implements AutoCloseable { private static final Object[] OBJECTS0 new Object[0]; private long createTime; // 当前节点DefaultNode private Node curNode; // 来源节点 private Node originNode; private Throwable error; // 资源 protected ResourceWrapper resourceWrapper; }CtEntry 是 Entry 的直接子类后面分析源码时我们所说 Entry 皆指 CtEntry。CtEntry 中声明的字段信息如下代码所示。class CtEntry extends Entry { // 当前 Entry 指向的父 Entry protected Entry parent null; // 父 Entry 指向当前 Entry protected Entry child null; // 当前资源的 ProcessorSlotChain protected ProcessorSlot chain; // 当前上下文 protected Context context; }CtEntry 用于维护父子 Entry每一次调用 SphU#entry 方法都会创建一个 CtEntry。如果服务 B 在处理一个请求的路径上会多次调用 SphU#entry那么这些 CtEntry 会构成一个双向链表。在每次创建 CtEntry都会将 Context.curEntry 设置为这个新的 CtEntry双向链表的作用就是在调用 CtEntry#exit 方法时能够将 Context.curEntry 还原为上一个资源的 CtEntry。例如在服务 B 接收到服务 A 的请求时会调用 SphU#entry 方法创建一个 CtEntry我们取个代号 ctEntry1此时的 ctEntry1 的父节点parent为空。当服务 B 向服务 C 发起调用时OpenFeign 适配器调用 SphU#entry 的方法会创建一个 CtEntry我们取个代号 ctEntry2此时 ctEntry2 的父节点parent就是 ctEntry1ctEntry1 的子节点child就是 ctEntry2如下图所示。ROOT 与调用树Constants 常量类用于声明全局静态常量Constants 有一个 ROOT 静态字段类型为 EntranceNode。在调用 ContextUtil#enter 方法时如果还没有为当前入口创建 EntranceNode则会为当前入口创建 EntranceNode将其赋值给 Context.entranceNode同时也会将这个 EntranceNode 添加到 Constants.ROOT 的子节点childList。资源对应的 DefaultNode 则是在 NodeSelectorSlot 中创建并赋值给 Context.curEntry.curNode。Constants.ROOT、Context.entranceNode 与 Entry.curNode 三者关系如下图所示。Sentinel 中的 ProcessorSlotProcessorSlot 直译就是处理器插槽是 Sentinel 实现限流降级、熔断降级、系统自适应降级等功能的切入点。Sentinel 提供的 ProcessorSlot 可以分为两类一类是辅助完成资源指标数据统计的切入点一类是实现降级功能的切入点。辅助资源指标数据统计的 ProcessorSlotNodeSelectorSlot为当前资源创建 DefaultNode并且将 DefaultNode 赋值给 Context.curEntry.curNode(见倒数第二张图)如果当前调用链路上只出现过一次 SphU#entry 的情况将该 DefaultNode 添加到的 Context.entranceNode 的子节点如倒数第一张图所示名为 sentinel_spring_web_context 的 EntranceNode否则添加到 Context.curEntry.parent 的子节点childList。有点抽象我们在分析 NodeSelectorSlot 源码时再详细介绍。ClusterBuilderSlot如果当前资源未创建 ClusterNode则为资源创建 ClusterNode将 ClusterNode 赋值给当前资源的 DefaultNode.clusterNode如果调用来源origin不为空则为调用来源创建 StatisticNode用于实现按调用来源统计资源的指标数据ClusterNode 持有每个调用来源的 StatisticNode。StatisticSlot这是 Sentinel 最为重要的类之一用于实现指标数据统计。先是调用后续的 ProcessorSlot#entry 判断是否放行请求再根据判断结果进行相应的指标数据统计操作。实现降级功能的 ProcessorSlotAuthoritySlot实现黑白名单降级SystemSlot实现系统自适应降级FlowSlot实现限流降级DegradeSlot实现熔断降级关于每个 ProcessorSlot 实现的功能将在后续文章详细分析。