Sentinel源码解析一(流程总览)
引言
Sentinel作为ali开源的一款轻量级流控框架,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。相比于Hystrix,Sentinel的设计更加简单,在 Sentinel中资源定义和规则配置是分离的,也就是说用户可以先通过Sentinel API给对应的业务逻辑定义资源(埋点),然后在需要的时候再配置规则,通过这种组合方式,极大的增加了Sentinel流控的灵活性。
引入Sentinel带来的性能损耗非常小。只有在业务单机量级超过25W QPS的时候才会有一些显著的影响(5% - 10% 左右),单机QPS不太大的时候损耗几乎可以忽略不计。
Sentinel提供两种埋点方式:
try-catch方式(通过SphU.entry(...)),用户在 catch 块中执行异常处理 / fallbackif-else方式(通过SphO.entry(...)),当返回 false 时执行异常处理 / fallback
写在前面
在此之前,需要先了解一下Sentinel的工作流程
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如默认情况下会创建一下7个插槽:
NodeSelectorSlot负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;ClusterBuilderSlot则用于存储资源的统计信息以及调用者信息,例如该资源的RT, QPS, thread count等等,这些信息将用作为多维度限流,降级的依据;StatisticSlot则用于记录、统计不同纬度的runtime指标监控信息;FlowSlot则用于根据预设的限流规则以及前面slot统计的状态,来进行流量控制;AuthoritySlot则根据配置的黑白名单和调用来源信息,来做黑白名单控制;DegradeSlot则通过统计信息以及预设的规则,来做熔断降级;SystemSlot则通过系统的状态,例如load1等,来控制总的入口流量
注意:这里的插槽链都是一一对应资源名称的
上面的所介绍的插槽(slot chain)是Sentinel非常重要的概念。同时还有一个非常重要的概念那就是Node,为了帮助理解,尽我所能画了下面这张图,可以看到整个结构非常的像一棵树:

简单解释下上图:
- 顶部蓝色的
node节点为根节点,全局唯一 - 下面黄色的节点为入口节点,每个
CentextName(上下文名称)一一对应一个- 可以有多个子节点(对应多种资源)
- 中间绿色框框中的节点都是属于同一个资源的(相同的
ResourceName) - 最底下紫色的节点是集群节点,可以理解成绿色框框中Node资源的整合
- 最右边的指的是不同的来源(origin)流量,同一个EntranceNode可以有多个来源
以上2个概念务必要理清楚,之后再一步一步看源码会比较清晰
下面我们将从入口源码开始一步一步分析整个调用过程:
源码分析
下面的是一个Sentinel使用的示例代码,我们就从这里切入开始分析
// 创建一个名称为entrance1,来源为appA 的上下文Context
ContextUtil.enter("entrance1", "appA");
// 创建一个资源名称nodeA的Entry
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
// 清除上下文
ContextUtil.exit();
ContextUtil.enter("entrance1", "appA")
public static Context enter(String name, String origin) {
// 判断上下文名称是否为默认的名称(sentinel_default_context) 是的话直接抛出异常
if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
throw new ContextNameDefineException(
"The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
}
return trueEnter(name, origin);
}
protected static Context trueEnter(String name, String origin) {
// 先从ThreadLocal中尝试获取,获取到则直接返回
Context context = contextHolder.get();
if (context == null) {
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
// 尝试从缓存中获取该上下文名称对应的 入口节点
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 判断缓存中入口节点数量是否大于2000
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
try {
// 加锁
LOCK.lock();
// 双重检查锁
node = contextNameNodeMap.get(name);
if (node == null) {
// 判断缓存中入口节点数量是否大于2000
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
// 根据上下文名称生成入口节点(entranceNode)
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 加入至全局根节点下
Constants.ROOT.addChild(node);
// 加入缓存中
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
// 初始化上下文对象
context = new Context(node, name);
context.setOrigin(origin);
// 设置到当前线程中
contextHolder.set(context);
}
return context;
}
主要做了2件事情
- 根据
ContextName生成entranceNode,并加入缓存,每个ContextName对应一个入口节点entranceNode - 根据
ContextName和entranceNode初始化上下文对象,并将上下文对象设置到当前线程中
这里有几点需要注意:
- 入口节点数量不能大于2000,大于会直接抛异常
- 每个
ContextName对应一个入口节点entranceNode - 每个
entranceNode都有共同的父节点。也就是根节点
Entry nodeA = SphU.entry("nodeA")
// SphU.class
public static Entry entry(String name) throws BlockException {
// 默认为 出口流量类型,单位统计数为1
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
// CtSph.class
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
// 生成资源对象
StringResourceWrapper resource = new StringResourceWrapper(name, type);
return entry(resource, count, args);
}
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
return entryWithPriority(resourceWrapper, count, false, args);
}
上面的代码比较简单,不指定EntryType的话,则默认为出口流量类型,最终会调用entryWithPriority方法,主要业务逻辑也都在这个方法中
- entryWithPriority方法
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
// 获取当前线程上下文对象
Context context = ContextUtil.getContext();
// 上下文名称对应的入口节点是否已经超过阈值2000,超过则会返回空 CtEntry
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
// 如果没有指定上下文名称,则使用默认名称,也就是默认入口节点
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// 全局开关
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
// 生成插槽链
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
/*
* 表示资源(插槽链)超过6000,因此不会进行规则检查。
*/
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
// 生成 Entry 对象
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 开始执行插槽链 调用逻辑
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
// 清除上下文
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// 除非Sentinel内部存在错误,否则不应发生这种情况。
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
这个方法可以说是涵盖了整个Sentinel的核心逻辑
- 获取上下文对象,如果上下文对象还未初始化,则使用默认名称初始化。初始化逻辑在上文已经分析过
- 判断全局开关
- 根据给定的资源生成插槽链,插槽链是跟资源相关的,Sentinel最关键的逻辑也都在各个插槽中。初始化的逻辑在
lookProcessChain(resourceWrapper);中,下文会分析 - 依顺序执行每个插槽逻辑
lookProcessChain(resourceWrapper)方法
lookProcessChain方法为指定资源生成插槽链,下面我们来看下它的初始化逻辑
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 根据资源尝试从全局缓存中获取
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 非常常见的双重检查锁
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 判断资源数是否大于6000
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// 初始化插槽链
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
- 根据资源尝试从全局缓存中获取插槽链。每个资源对应一个插槽链(资源嘴多只能定义6000个)
- 初始化插槽链上的插槽(
SlotChainProvider.newSlotChain()方法中)
下面我们看下初始化插槽链上的插槽的逻辑
SlotChainProvider.newSlotChain()
public static ProcessorSlotChain newSlotChain() {
// 判断是否已经初始化过
if (builder != null) {
return builder.build();
}
// 加载 SlotChain
resolveSlotChainBuilder();
// 加载失败则使用默认 插槽链
if (builder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
builder = new DefaultSlotChainBuilder();
}
// 构建完成
return builder.build();
}
/**
* java自带 SPI机制 加载 slotChain
*/
private static void resolveSlotChainBuilder() {
List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
boolean hasOther = false;
// 尝试获取自定义SlotChainBuilder,通过JAVA SPI机制扩展
for (SlotChainBuilder builder : LOADER) {
if (builder.getClass() != DefaultSlotChainBuilder.class) {
hasOther = true;
list.add(builder);
}
}
if (hasOther) {
builder = list.get(0);
} else {
// 未获取到自定义 SlotChainBuilder 则使用默认的
builder = new DefaultSlotChainBuilder();
}
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
+ builder.getClass().getCanonicalName());
}
- 首先会尝试获取自定义的
SlotChainBuilder来构建插槽链,自定义的SlotChainBuilder可以通过JAVA SPI机制来扩展 - 如果未配置自定义的
SlotChainBuilder,则会使用默认的DefaultSlotChainBuilder来构建插槽链,DefaultSlotChainBuilder所构建的插槽就是文章开头我们提到的7种Slot。每个插槽都有其对应的职责,各司其职,后面我们会详细分析这几个插槽的源码,及所承担的职责。
总结
文章开头的提到的两个点(插槽链和Node),这是Sentinel的重点,理解这两点对于阅读源码来说事半功倍
Sentinel系列
Sentinel源码解析一(流程总览)的更多相关文章
- Sentinel源码解析二(Slot总览)
写在前面 本文继续来分析Sentinel的源码,上篇文章对Sentinel的调用过程做了深入分析,主要涉及到了两个概念:插槽链和Node节点.那么接下来我们就根据插槽链的调用关系来依次分析每个插槽(s ...
- Sentinel源码解析四(流控策略和流控效果)
引言 在分析Sentinel的上一篇文章中,我们知道了它是基于滑动窗口做的流量统计,那么在当我们能够根据流量统计算法拿到流量的实时数据后,下一步要做的事情自然就是基于这些数据做流控.在介绍Sentin ...
- 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(下)
关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/12/es-code03/ 前提 上篇文章写完了 ES 流程启动的一部分,main 方法都入 ...
- 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(上)
关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/11/es-code02/ 前提 上篇文章写了 ElasticSearch 源码解析 -- ...
- Log4j源码解析--框架流程+核心解析
OK,现在我们来研究Log4j的源码: 这篇博客有参照上善若水的博客,原文出处:http://www.blogjava.net/DLevin/archive/2012/06/28/381667.htm ...
- Tomcat源码解析-整体流程介绍
一.架构 下面谈谈我对Tomcat架构的理解 总体架构: 1.面向组件架构 2.基于JMX 3.事件侦听 1)面向组件架构 tomcat代码看似很庞大,但从结构上看却很清晰和简单,它主要由一堆组件组成 ...
- Sentinel源码解析三(滑动窗口流量统计)
前言 Sentinel的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等.上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot),StatisticSlo ...
- Activiti-5.3工作流引擎-源码解析(流程文档解析)
前面我们通过BPMN20.xsd和Activiti自定义的XML Schema文件初步了解了业务流程模型的定义,那么现在我们来了解一下流程文档的解析过程,这个过程主要是通过代码解析来完成. 代码解析过 ...
- ReactiveCocoa v2.5 源码解析 之 架构总览
ReactiveCocoa 是一个 iOS 中的函数式响应式编程框架,它受 Functional Reactive Programming 的启发,是 Justin Spahr-Summers 和 J ...
随机推荐
- python爬虫实战之爬取智联职位信息和博客文章信息
1.python爬取招聘信息 简单爬取智联招聘职位信息 # !/usr/bin/env python # -*-coding:utf-8-*- """ @Author ...
- B站百大UP主党妹被黑客勒索!!!
4月27日,哔哩哔哩视频网站的UP主“机智的党妹”发布消息称,自己被黑客勒索了.她的视频表示:“事发突然,我被勒索了,你也有可能继续被诈骗!这种诈骗的页面是由病毒程序自动生成并留在那里的.”根据她的介 ...
- 前端开发--ajax
使用ajax,他是有两个模块的,一个是客户端,一个是服务端. 客户端负责发送数据,发送数据的方式有两种,一种是GET,另一种是POST. 服务端是用来接收,处理数据和发送请求的数据. 要想使用ajax ...
- Content-Type 四种常见的 POST 提交数据方式
参考于: https://blog.csdn.net/tycoon1988/article/details/40080691(了解) 和: https://www.gy0929.com/wz/1420 ...
- 2019-2020-1 20199303 《Linux内核原理与分析》 第十一周作业
缓冲区溢出漏洞实验 安装一些用于编译C程序的32位软件包 sudo apt-get install -y lib32z1 libc6-dev-i386 sudo apt-get install -y ...
- (第六篇)vim编辑器的使用
什么是 vim(window文本文档) Vim是从 vi 发展出来的一个文本编辑器.代码补完.编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用.简单的来说, vi 是老式的字处理器,不过功 ...
- vue2.x学习笔记(二十七)
接着前面的内容:https://www.cnblogs.com/yanggb/p/12682364.html. 单元测试 vue cli拥有开箱即用的通过jest或mocha进行单元测试的内置选项.官 ...
- Swoole 实战:MySQL 查询器的实现(协程连接池版)
目录 需求分析 使用示例 模块设计 UML 类图 入口 事务 连接池 连接 查询器的组装 总结 需求分析 本篇我们将通过 Swoole 实现一个自带连接池的 MySQL 查询器: 支持通过链式调用构造 ...
- 源码学习VUE之Observe
在文章 源码学习VUE之响应式原理我们大概描述了响应式的实现流程,主要写了observe,dep和wather的简易实现,以及推导思路.但相应代码逻辑并不完善,今天我们再来填之前的一些坑. Obser ...
- 洛谷 2016 战略游戏(树形DP)
题目描述 Bob喜欢玩电脑游戏,特别是战略游戏.但是他经常无法找到快速玩过游戏的办法.现在他有个问题. 他要建立一个古城堡,城堡中的路形成一棵树.他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能 ...