Sentinel 是什么?github描述如下
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
本文建立在会使用Sentinel的基础上,详细的介绍和使用不会展开,具体介绍和使用看:Sentinel介绍
一个简单的Demo如下:
String resourceName = "资源名称";
Entry entry = null;
try {
entry = SphU.entry(resourceName);
run();
} catch (BlockException ex) {
throw ex;
} catch (Throwable ex) {
Tracer.trace(ex);
throw ex;
} finally {
if (entry != null) {
entry.exit();
}
}
这就是一个设置之后,run方法就会被Sentinel所监控起来,但是这时候,是没有任何效果的,因为没有告诉Sentinel需要去限制什么?在Sentinel中,这个叫做规则,即你需要设置好限制的规则,Sentinel会根据设置的规则去限制你的代码,即上面的run方法,那么下面来看下Sentinel的整个调用流程是如何。
以SphU.entry
方法为入口,一步步的跟进去
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);// 1
}
//CtSph.java
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
StringResourceWrapper resource = new StringResourceWrapper(name, type);//2
return entry(resource, count, args);//3
}
- 1:entry有很多重载的方法,如果不填,就会设置默认值,其他参数后续分析
- 2:对于Sentinel来说,限制的是资源,这里将名称和EntryType构造成一个资源对象
- 3:接着调用entry方法进行处理
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
return entryWithPriority(resourceWrapper, count, false, args);
}
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
Context context = ContextUtil.getContext();// 1
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());//2
}
if (!Constants.ON) {//3
return new CtEntry(resourceWrapper, null, context);
}
ProcessorSlot
- 1:从ThreadLocal中获取一个上下文对象,此时第一次调用,ThreadLocal为空。当然我们在demo中可以手动指定一个上下文,那么到这里就不会为空了
- 2:第一次为空,所以需要进行一个Context的初始化
- 3:一个全局开关,可供动态切换,如果关闭了,则后续就不会走规则校验
- 4:画个重点!!!!这
个ProcessorSlotChain
是Sentinel整个流程的核心,相当于一个拦截器链,所有请求会经过拦截器链进行处理,一会分析 - 5:chain为空,是由某种情况引起的,具体情况在
lookProcessChain
中 - 6:开始执行核心逻辑
获取上下文及初始化
private static ThreadLocal contextHolder = new ThreadLocal();
public static Context getContext() {
return contextHolder.get();
}
从代码中看到,contextHolder
在此之前没有做过初始化,那么会走到如下方法:
//com.alibaba.csp.sentinel.CtSph
MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
//com.alibaba.csp.sentinel.CtSph.MyContextUtil
private final static class MyContextUtil extends ContextUtil {
static Context myEnter(String name, String origin, EntryType type) {
return trueEnter(name, origin);
}
}
//com.alibaba.csp.sentinel.context.ContextUtil
protected static Context trueEnter(String name, String origin) {
Context context = contextHolder.get();//1
if (context == null) {
Map localCacheNameMap = contextNameNodeMap;//2
DefaultNode node = localCacheNameMap.get(name);//3
if (node == null) {
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {// 4
setNullContext();
return NULL_CONTEXT;
} else {
try {
LOCK.lock();
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {// 5
setNullContext();
return NULL_CONTEXT;
} else {// 6
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
Constants.ROOT.addChild(node);
Map newMap = new HashMap(
contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
//7
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
- 1:首先这里从ThreadLocal中获取,这时还是空的
- 2:contextNameNodeMap这里初始化的时候默认加了个name为
Constants.CONTEXT_DEFAULT_NAME
的节点进去,所以上述demo会走到7。这里 - 3:假设我们自己指定了上下文名称(是否要指定上下文需要看情况),那么第一次进行,这里为空,会进入下面的判断
- 4:
localCacheNameMap.size()
即上下文的数量(因为以contextName为key),Sentinel限制了上下文的数量是2000以下,如果大于2000会返回NULL_CONTEXT
,在之前的处理中可以看到NULL_CONTEXT
是直接返回的 - 5:由于4的判断是在无锁的情况下进行的,所以需要在加锁条件下再进行一次判断(类似单例
double check
的模式),假设没有问题则会走到6 - 6:这里创建了个
EntranceNode
节点,关于Sentinel中Node的问题会在后续文章分析 - 7:创建了个上下文对象并且放入了ThreadLocal中,下次同一线程可以直接获取
注意点:
- 由于这个Node节点内部有许多信息,为了限制内存占用,会限制上下文的数量
- 有些情况不关心上下文,那么就如demo中一样,直接调用
SphU.entry
,那么这时候会指定一个默认的供其使用,如果需要区分上下文,那么则需要在SphU.entry
之前调用ContextUtil.enter
方法指定上下文
调用链的创建与触发
ProcessorSlot
- 1:上面的Node节点是和上下文关联的,而这个的ProcessorSlotChain是和资源关联的,即一个资源会有一个ProcessorSlotChain对象
- 2~5:使用了double check,判断map中是否有该资源的ProcessorSlotChain对应
- 6:同上面上下文的创建,控制内存
- 7:初始化一个ProcessorSlotChain对象并放入map中
看下SlotChainProvider.newSlotChain方法
public static ProcessorSlotChain newSlotChain() {
if (builder != null) {// 第一次会为空,需要初始化
return builder.build();
}
// 初始化builder
resolveSlotChainBuilder();
if (builder == null) {// 初始化后仍然为空,则设置为默认的Builder
builder = new DefaultSlotChainBuilder();
}
// 通过builder创建ProcessorSlotChain
return builder.build();
}
// 通过java SPI的机制获取对应的信息
private static final ServiceLoader LOADER =
ServiceLoader.load(SlotChainBuilder.class);
private static void resolveSlotChainBuilder() {
List list = new ArrayList();
boolean hasOther = false;
// 获取SPI中配置的实现
for (SlotChainBuilder builder : LOADER) {
// 如果SPI的配置文件中自定义了实现
if (builder.getClass() != DefaultSlotChainBuilder.class) {
hasOther = true;
list.add(builder);
}
}
// 如果有多个自定义实现,则默认取第一个
if (hasOther) {
builder = list.get(0);
} else {
// 没有自定义实现那么取默认实现.
builder = new DefaultSlotChainBuilder();
}
}
Sentinel中大量使用了Java 的SPI机制去进行一个扩展,这里就用来扩展Builder,如果我们需要自己去自定义一个Buidler,去排列调用链中的元素节点,那么可以参考Java SPI机制去配置,那么Sentinel就选择自定义的Builder去创建ProcessorSlotChain
,而默认情况使用的是DefaultSlotChainBuilder
,那么看下其build方法
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
chain.addLast(new NodeSelectorSlot());
chain.addLast(new ClusterBuilderSlot());
chain.addLast(new LogSlot());
chain.addLast(new StatisticSlot());
chain.addLast(new SystemSlot());
chain.addLast(new AuthoritySlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
可以看到DefaultSlotChainBuilder
已经默认排列好了调用链中的节点,其实内部就类似一个拦截器链,Slot是拦截器链中的拦截器节点,每个节点的功能不同,具体功能如下:
- NodeSelectorSlot:用于创建Node节点
- ClusterBuilderSlot:用于创建ClusterNode节点
- LogSlot:目前对于被规则限制的情况,交给了StatLogger处理,但是好像没啥效果?
- StatisticSlot:用于统计当前流量通过的情况
- SystemSlot:用于系统负载规则的处理
- AuthoritySlot: 用于黑白名单规则的处理
- FlowSlot:用于限流规则的处理
- DegradeSlot:用于降级规则的处理
DefaultProcessorSlotChain
这个调用链或者说拦截器链,一般来说是数组或者链表实现的,通过上面的addLast方法来看,应该用链表会比较合适(这个类似Netty的pipeline,有addLast
的话,应该有addFirst
,如果有addFirst的话,数组就不合适了,因为数组插入元素的话比较麻烦,而链表就比较容易了)
public class DefaultProcessorSlotChain extends ProcessorSlotChain {
AbstractLinkedProcessorSlot> first = new AbstractLinkedProcessorSlot
结构就是链表的结构,有头节点,尾节点,每个节点都有个next引用指向下一个节点,这里需要注意的是next引用它是在父类里的,这里可以类比一下Netty的pipeline,有少许不同,但是核心都差不多
还有个点需要看下,和Netty有点类似,以FlowSlot为例
public class FlowSlot extends AbstractLinkedProcessorSlot {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkFlow(resourceWrapper, context, node, count, prioritized);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
}
可以看到最后执行完checkFlow还调用了一次fireEntry,这个会继续往后触发,Netty的pipeline也是这样的触发形式
数据统计
上面介绍了几个Slot的作用,以常用的限流规则为例,我们在控制台配置限流规则:
例如配置qps为10,那么在
FlowSlot
会检查当前qps是否超过这个值,没超过则通过该请求,否则抛出异常,那么有个疑问,
FlowSlot
如何获取当前服务的一个qps或者说请求量呢?
这时候就轮到我们的主角StatisticSlot
登场了,其entry方法如下
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
try {
fireEntry(context, resourceWrapper, node, count, prioritized, args);// 1
node.increaseThreadNum();//2
node.addPassRequest(count);//3
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();//4
context.getCurEntry().getOriginNode().addPassRequest(count);//5
}
if (resourceWrapper.getType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();//6
Constants.ENTRY_NODE.addPassRequest(count);//7
}
// ....
} catch (BlockException e) {
// ....
node.increaseBlockQps(count);//8
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseBlockQps(count);//9
}
if (resourceWrapper.getType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseBlockQps(count);//10
}
// ....
throw e;
} catch (Throwable e) {
// ....
node.increaseExceptionQps(count);//11
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseExceptionQps(count);//12
}
if (resourceWrapper.getType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseExceptionQps(count);//13
}
throw e;
}
}
上面是entry方法做的逻辑,主要关注的几点已经标注出来了,fireEntry
这里是触发后续节点,从DefaultSlotChainBuilder#build
方法中可以看到StatisticSlot
后还有四个节点用来校验规则,即这里的fireEntry
会触发规则的校验,规则校验通过则往下走,失败的走catch块。
从2~13的方法名称中可以知道这里进行了流量的统计,例如增加线程数->increaseThreadNum
,增加通过的请求数->addPassRequest
,增加block请求qps->increaseBlockQps
,增加异常qps->increaseExceptionQps
,到这里就能知道Sentinel是如何统计请求数的(Node的具体原理后续分析)。
结合上述调用链的执行,整个流程如下(省略部分Slot的处理):
总结
Sentinel的保护措施是在一个Slot链中,Slot链有不同的节点,每个节点负责不同的事情,例如降级相关规则、系统负载相关规则、限流相关规则的处理,如果触发了某个规则(例如qps已经超过配置的规则),那么会抛出异常,而Slot链有个节点负责统计成功和异常的数量,然后这时候就不会执行保护的代码,达到一个保护的作用