本文主要来解析Sentinel的核心源码,基于当前最新的release版本1.8.0,如果你尚未了解Sentinel的核心功能,可以查看Sentinel的官方文档或者这篇文章Sentinel核心功能实战
1、Sentinel案例解析
public UserOrder getUserOrderByUserId(Long userId) {
ContextUtil.enter("UserService");
Entry entry = null;
try {
entry = SphU.entry("getUserOrderByUserId", EntryType.IN);
//根据用户id查询用户信息
User userInfo = getUserById(userId);
//根据用户id查询订单信息
List<Order> orderList = orderService.getOrderByUserId(userId);
return UserOrder.builder()
.user(userInfo)
.orderList(orderList)
.build();
} catch (BlockException ex) {
log.error("系统繁忙", ex);
throw new RuntimeException("系统繁忙");
} finally {
if (entry != null) {
entry.exit();
}
}
}
public List<Order> getOrderByUserId(Long userId) {
Entry entry = null;
try {
entry = SphU.entry("getOrderByUserId");
//根据用户id查询订单信息
return new ArrayList<>();
} catch (BlockException ex) {
log.error("系统繁忙", ex);
return null;
} finally {
if (entry != null) {
entry.exit();
}
}
}
1)Context代表一个调用链的入口,Context实例设置在ThreadLocal中,所以它是跟着线程走的,如果要切换线程,需要手动通过ContextUtil.runOnContext(context, f)
切换
ContextUtil.enter()
有两个参数:
第一个参数是context name,它代表调用链的入口,作用是为了区分不同的调用链路,默认为sentinel_default_context
第二个参数代表调用方标识origin,目前它有两个作用,一是用于黑白名单的授权控制,二是可以用来统计诸如从应用A发起的对当前应用xxx接口的调用,目前这个数据会被统计,但是dashboard中并不展示
2)进入BlockException异常分支,代表该次请求被流量控制规则限制了,一般会让代码走入到熔断降级的逻辑里面。BlockException有好多个子类,如DegradeException、FlowException等,也可以catch具体的子类来进行处理
3)SphU.entry()
方法:
第一个参数标识资源,通常就是接口标识,对于数据统计、规则控制等,一般都是在这个粒度上进行的,根据这个字符串来唯一标识,它会被包装成ResourceWrapper实例
第二个参数标识资源的类型,EntryType.IN
代表这个是入口流量,比如我们的接口对外提供服务,那么我们通常就是控制入口流量;EntryType.OUT
代表出口流量,比如上面的getOrderByUserId()
方法(没写默认就是OUT),它的业务需要调用订单服务,像这种情况,压力其实都在订单服务中,那么我们就指定它为出口流量
在SystemSlot
类中,它用于实现自适应限流,根据系统健康状态来判断是否要限流,如果是OUT类型,由于压力在外部系统中,所以就不需要执行这个规则
4)getOrderByUserId()
方法中嵌套使用了Entry。如果我们在一个方法中写的话,要注意内层的Entry先exit,才能做外层的exit,否则会抛出异常。源码角度来看,是在Context实例中,保存了当前的Entry实例
2、ContextUtil#enter
public static Context enter(String name, String origin) {
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> contextHolder
Context context = contextHolder.get();
if (context == null) {
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
//初始化EntranceNode实例
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
//ROOT_ID为machine-root
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;
}
如果不显式调用ContextUtil.enter()
方法的话,那root就只有一个default子节点sentinel_default_context
ContextUtil.enter("UserService")
实际上会添加名为UserService的EntranceNode节点,可以得到下面这棵树:
Context代表线程执行的上下文,在Sentinel中,对于一个新的context name,Sentinel会往树中添加一个EntranceNode实例。它的作用是为了区分调用链路,标识调用入口。在Sentinel Dashboard中,可以很直观地看出调用链路
3、SphU#entry
SphU.entry()
最终会调用CtSph类中的entryWithPriority()
方法,详细代码如下:
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
public class CtSph implements Sph {
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);
}
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
//从ThreadLocal中获取Context实例
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
//如果不显式调用ContextUtil.enter(),会进入到默认的context中
if (context == null) {
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
//Sentinel的全局开关,Sentinel提供了接口让用户可以在Dashboard开启或关闭
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
//用于构建一个责任链,入参是resource,资源的唯一标识是resource name(责任链模式)
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
//执行这个责任链 如果抛出BlockException,说明链上的某一环拒绝了该请求
//把这个异常往上层业务层抛,业务层处理BlockException应该进入到熔断降级逻辑中
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) {
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
lookProcessChain()
用于构建一个责任链。Sentinel的处理核心都在这个责任链中,链中每一个节点是一个Slot实例,这个链通过BlockException异常来告知调用入口最终的执行情况
Sentinel的核心骨架将不同的Slot按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。核心结构如下图:
默认的DefaultSlotChainBuilder生成的责任链,详细代码如下:
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
}
使用SPI机制加载ProcessorSlot的所有子类,加载的顺序取决于子类上注解@SpiOrder的值,resources/META-INF/services
目录下com.alibaba.csp.sentinel.slotchain.ProcessorSlot的文件内容如下:
# Sentinel default ProcessorSlots
com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
com.alibaba.csp.sentinel.slots.logger.LogSlot
com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
com.alibaba.csp.sentinel.slots.system.SystemSlot
com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
接下来,就按照默认的DefaultSlotChainBuilder生成的责任链往下看源码
对于相同的resource,使用同一个责任链实例,不同的resource,使用不同的责任链实例
4、NodeSelectorSlot
NodeSelectorSlot负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级
@SpiOrder(-10000)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
//key是context name,value是DefaultNode实例
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
//初始化DefaultNode
node = new DefaultNode(resourceWrapper, null);
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
// Build invocation tree
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
//设置Context的当前Node
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
责任链实例和resource name相关,和线程无关,所以当处理同一个resource的时候,会进入到同一个NodeSelectorSlot实例中
所以NodeSelectorSlot主要就是要处理:不同的context name,同一个resource name的情况
结合前面讲解的那棵树,可以得出下面这棵树:
5、ClusterBuilderSlot
ClusterBuilderSlot用于存储资源的统计信息以及调用者信息,例如该资源的RT、QPS、thread count等等,这些信息将用作为多维度限流、降级的依据
@SpiOrder(-9000)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
private static final Object lock = new Object();
private volatile ClusterNode clusterNode = null;
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args)
throws Throwable {
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
//初始化ClusterNode
clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
node.setClusterNode(clusterNode);
if (!"".equals(context.getOrigin())) {
//初始化originNode
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
每一个resource会对应一个ClusterNode实例,如果不存在,就创建一个实例
ClusterNode是用来做数据统计的。比如getUserOrderByUserId这个接口,由于从不同的context name中开启调用链,它有多个DefaultNode实例,但是只有一个ClusterNode,通过这个实例,我们可以知道这个接口现在的QPS是多少
origin代表调用方标识,当设置了origin的时候,这里会额外生成一个StatisticNode实例,挂在ClusterNode上
getUserOrderByUserId这个接口接收到了来自application-a和application-b两个应用的请求,那么树会变成下面这样:
它的作用是用来统计从不同来源过来的访问getUserOrderByUserId这个接口的信息
截止到这里Sentinel中各种统计节点都介绍完了,下面来总结下:
- EntranceNode:入口节点,特殊的链路节点,对应某个Context入口的所有调用数据。Constants.ROOT节点也是入口节点
- DefaultNode:链路节点,用于统计调用链路上某个资源的数据,维持树状结构
- ClusterNode:簇点,用于统计每个资源全局的数据(不区分调用链路),以及存放该资源的按来源区分的调用数据(类型为 StatisticNode)。特别地,Constants.ENTRY_NODE节点用于统计全局的入口资源数据
- StatisticNode:最为基础的统计节点,包含秒级和分钟级两个滑动窗口结构
构建的时机:
- EntranceNode在
ContextUtil.enter()
的时候就创建了,然后塞到Context里面- NodeSelectorSlot根据context创建DefaultNode,然后set curNode to context
- ClusterBuilderSlot首先根据resourceName创建ClusterNode,并且set clusterNode to defaultNode;然后再根据origin创建来源节点(类型为StatisticNode),并且set originNode to curEntry
几种Node的维度(数目):
- EntranceNode的维度是context,存在ContextUtil类的contextNameNodeMap里面
- DefaultNode的维度是
resource * context
,存在每个NodeSelectorSlot的map里面- ClusterNode的维度是resource
- StatisticNode的维度是
resource * origin
,存在每个ClusterNode的originCountMap里面
6、LogSlot
@SpiOrder(-8000)
public class LogSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object... args)
throws Throwable {
try {
fireEntry(context, resourceWrapper, obj, count, prioritized, args);
} catch (BlockException e) {
EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(),
context.getOrigin(), count);
throw e;
} catch (Throwable e) {
RecordLog.warn("Unexpected entry exception", e);
}
}
如果抛出了BlockException,这里调用了EagleEyeLogUtil.log()
方法,将被设置的规则block的信息记录到日志文件sentinel-block.log中,记录哪些接口被规则挡住了
7、StatisticSlot&滑动窗口
StatisticSlot用于记录、统计不同纬度的runtime指标监控信息
@SpiOrder(-7000)
public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@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);
//累加线程数threadNum,累加通过的request数量
//对于QPS统计,使用滑动窗口;而对于线程并发的统计,使用了LongAdder
node.increaseThreadNum();
node.addPassRequest(count);
if (context.getCurEntry().getOriginNode() != null) {
//如果originNode存在(类型为StatisticNode),则也需要增加originNode的线程数和请求通过数
context.getCurEntry().getOriginNode().increaseThreadNum();
context.getCurEntry().getOriginNode().addPassRequest(count);
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
//如果资源包装类型是IN的话,则需要累加ENTRY_NODE的线程数和请求通过数
//ENTRY_NODE是sentinel全局的统计节点(类型为ClusterNode),用于后续系统规则检查
Constants.ENTRY_NODE.increaseThreadNum();
Constants.ENTRY_NODE.addPassRequest(count);
}
//循环处理注册了ProcessorSlotEntryCallback的StatisticSlot
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (PriorityWaitException ex) {
node.increaseThreadNum();
if (context.getCurEntry().getOriginNode() != null) {
// Add count for origin node.
context.getCurEntry().getOriginNode().increaseThreadNum();
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
Constants.ENTRY_NODE.increaseThreadNum();
}
// Handle pass event with registered entry callback handlers.
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (BlockException e) {
// Blocked, set block exception to current entry.
context.getCurEntry().setBlockError(e);
// Add block count.
node.increaseBlockQps(count);
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseBlockQps(count);
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
Constants.ENTRY_NODE.increaseBlockQps(count);
}
// Handle block event with registered entry callback handlers.
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onBlocked(e, context, resourceWrapper, node, count, args);
}
throw e;
} catch (Throwable e) {
// Unexpected internal error, set error to current entry.
context.getCurEntry().setError(e);
throw e;
}
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
Node node = context.getCurNode();
if (context.getCurEntry().getBlockError() == null) {
//计算响应时间,通过当前时间-CurEntry的创建时间取毫秒值
long completeStatTime = TimeUtil.currentTimeMillis();
context.getCurEntry().setCompleteTimestamp(completeStatTime);
long rt = completeStatTime - context.getCurEntry().getCreateTimestamp();
Throwable error = context.getCurEntry().getError();
recordCompleteFor(node, count, rt, error);
recordCompleteFor(context.getCurEntry().getOriginNode(), count, rt, error);
if (resourceWrapper.getEntryType() == EntryType.IN) {
recordCompleteFor(Constants.ENTRY_NODE, count, rt, error);
}
}
// Handle exit event with registered exit callback handlers.
Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
for (ProcessorSlotExitCallback handler : exitCallbacks) {
handler.onExit(context, resourceWrapper, count, args);
}
fireExit(context, resourceWrapper, count);
}
private void recordCompleteFor(Node node, int batchCount, long rt, Throwable error) {
if (node == null) {
return;
}
//新增响应时间和成功数
node.addRtAndSuccess(rt, batchCount);
//线程数减1
node.decreaseThreadNum();
if (error != null && !(error instanceof BlockException)) {
node.increaseExceptionQps(batchCount);
}
}
数据统计的代码在StatisticNode中
public class StatisticNode implements Node {
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
//使用AtomicInteger来统计当前线程数
private LongAdder curThreadNum = new LongAdder();
从上面的代码也可以知道,Sentinel统计了秒和分两个维度的数据,下面来看下实现类ArrayMetric的源码设计
public class ArrayMetric implements Metric {
private final LeapArray<MetricBucket> data;
public ArrayMetric(int sampleCount, int intervalInMs) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
if (enableOccupy) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
} else {
this.data = new BucketLeapArray(sampleCount, intervalInMs);
}
}
ArrayMetric的内部是一个LeapArray,以分钟维度统计的使用来说,它使用子类BucketLeapArray实现
public abstract class LeapArray<T> {
protected int windowLengthInMs;
protected int sampleCount;
protected int intervalInMs;
private double intervalInSecond;
protected final AtomicReferenceArray<WindowWrap<T>> array;
//对于分钟维度的设置,sampleCount为60,intervalInMs为60*1000
public LeapArray(int sampleCount, int intervalInMs) {
AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");
//单个窗口长度,这里是1000ms
this.windowLengthInMs = intervalInMs / sampleCount;
//一轮总时长60000ms
this.intervalInMs = intervalInMs;
this.intervalInSecond = intervalInMs / 1000.0;
//60个窗口
this.sampleCount = sampleCount;
this.array = new AtomicReferenceArray<>(sampleCount);
}
它的内部核心是一个数组array,它的长度为60,也就是有60个窗口,每个窗口长度为1秒,刚好一分钟走完一轮。然后下一轮开启覆盖操作
每个窗口是一个WindowWrap类实例
- 添加数据的时候,先判断当前走到哪个窗口了(当前时间(s) % 60即可),然后需要判断这个窗口是否是过期数据,如果是过期数据(窗口代表的时间距离当前已经超过1分钟),需要先重置这个窗口实例的数据
- 统计数据同理,如统计过去一分钟的QPS数据,就是将每个窗口的值相加,当中需要判断窗口数据是否是过期数据,即判断窗口的WindowWrap实例是否是一分钟内的数据
核心逻辑都封装在了currentWindow(long timeMillis)
和values(long timeMillis)
方法中
添加数据的时候,我们要先获取操作的目标窗口,也就是currentWindow()
这个方法,Sentinel 在这里处理初始化和过期重置的情况:
public abstract class LeapArray<T> {
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
//获取窗口下标
int idx = calculateTimeIdx(timeMillis);
//计算该窗口的理论开始时间
long windowStart = calculateWindowStart(timeMillis);
//嵌套在一个循环中,因为有并发的情况
while (true) {
WindowWrap<T> old = array.get(idx);
if (old == null) {
//窗口未实例化的情况,使用CAS来设置该窗口实例
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
//存在竞争
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
//当前数组中的窗口没有过期
return old;
} else if (windowStart > old.windowStart()) {
//该窗口已过期,重置窗口的值 使用一个锁来控制并发
if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
//正常情况都不会走到这个分支,异常情况其实就是时钟回拨,这里返回一个WindowWrap是容错
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
获取数据,使用的是values()
方法,这个方法返回有效的窗口中的数据:
public List<T> values(long timeMillis) {
if (timeMillis < 0) {
return new ArrayList<T>();
}
int size = array.length();
List<T> result = new ArrayList<T>(size);
for (int i = 0; i < size; i++) {
WindowWrap<T> windowWrap = array.get(i);
//过滤掉过期数据,判断当前窗口的数据是否是60秒内的
if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
continue;
}
result.add(windowWrap.value());
}
return result;
}
StatisticNode类注解总结了数据统计的原理:
Sentinel使用滑动窗口来记录和统计实时调用数据
- 当第一个请求到来,Sentinel会创建一个特殊的时间片(time-span)去保存运行时的数据,比如:响应时间(rt)、QPS、block request,在这里叫做滑动窗口(window bucket),这个滑动窗口通过sample count定义。Sentinel通过滑动窗口有效的数据来决定当前请求是否通过,滑动窗口将记录所有的QPS,将其与规则中定义的阈值进行比较
- 不同的请求进来,根据不同的时间存放在不同滑动窗口中
- 请求不断的进入系统,先前的滑动窗口将会过期无效
接下来要介绍的几个Slot,需要通过Dashboard进行开启,因为需要配置规则
8、AuthoritySlot
AuthoritySlot根据配置的黑白名单和调用来源信息,来做黑白名单控制
在Sentinel Dashboard上新增授权规则:
@SpiOrder(-6000)
public class AuthoritySlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
//校验黑白名单授权
checkBlackWhiteAuthority(resourceWrapper, context);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();
if (authorityRules == null) {
return;
}
//根据资源名获取授权规则
Set<AuthorityRule> rules = authorityRules.get(resource.getName());
if (rules == null) {
return;
}
for (AuthorityRule rule : rules) {
//如果passCheck校验返回false,抛出AuthorityException
if (!AuthorityRuleChecker.passCheck(rule, context)) {
throw new AuthorityException(context.getOrigin(), rule);
}
}
}
final class AuthorityRuleChecker {
static boolean passCheck(AuthorityRule rule, Context context) {
String requester = context.getOrigin();
if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
return true;
}
//匹配的时候根据origin判断
int pos = rule.getLimitApp().indexOf(requester);
boolean contain = pos > -1;
if (contain) {
boolean exactlyMatch = false;
String[] appArray = rule.getLimitApp().split(",");
for (String app : appArray) {
if (requester.equals(app)) {
exactlyMatch = true;
break;
}
}
contain = exactlyMatch;
}
int strategy = rule.getStrategy();
if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
return false;
}
if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
return false;
}
return true;
}
private AuthorityRuleChecker() {}
}
9、SystemSlot
SystemSlot通过系统的状态,例如load1等,来控制总的入口流量
在Sentinel Dashboard上新增系统保护规则包含以下几个类型:
@SpiOrder(-5000)
public class SystemSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
SystemRuleManager.checkSystem(resourceWrapper);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
SystemSlot依赖于SystemRuleManager来做检查
public final class SystemRuleManager {
public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
if (resourceWrapper == null) {
return;
}
//检查系统状态是否为false,如果为false,则代表不检查
//如果不配置SystemRule,则不检查
if (!checkSystemStatus.get()) {
return;
}
//系统检查状态,只检查外部调内部的接口状态,EntryType.IN内部调用外部接口不检查
if (resourceWrapper.getEntryType() != EntryType.IN) {
return;
}
//获取当前系统的QPS,根据ClusterNode的successQps计算successQps总数/时间 每秒成功的记录
double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
if (currentQps > qps) {
throw new SystemBlockException(resourceWrapper.getName(), "qps");
}
//总线程数
int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
if (currentThread > maxThread) {
throw new SystemBlockException(resourceWrapper.getName(), "thread");
}
//平均响应时长
double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
if (rt > maxRt) {
throw new SystemBlockException(resourceWrapper.getName(), "rt");
}
//系统负载
if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
if (!checkBbr(currentThread)) {
throw new SystemBlockException(resourceWrapper.getName(), "load");
}
}
//CPU使用率超过限制
if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
throw new SystemBlockException(resourceWrapper.getName(), "cpu");
}
}
由于系统的平均RT、当前线程数、QPS都可以从ENTRY_NODE中获得,所以限制代码非常简单,比较一下大小就可以了。如果超过阈值,抛出SystemBlockException
ENTRY_NODE是ClusterNode类型的,而ClusterNode对于RT、QPS都是统计的秒维度的数据
而对于系统负载和CPU资源的保护,核心类是SystemStatusListener
public class SystemStatusListener implements Runnable {
@Override
public void run() {
try {
OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
currentLoad = osBean.getSystemLoadAverage();
double systemCpuUsage = osBean.getSystemCpuLoad();
...
} catch (Throwable e) {
RecordLog.warn("[SystemStatusListener] Failed to get system metrics from JMX", e);
}
}
Sentinel通过调用OperatingSystemMXBean中的方法获取当前的系统负载和CPU使用率,Sentinel起了一个后台线程,每秒查询一次
10、FlowSlot
FlowSlot则用于根据预设的限流规则以及前面slot统计的状态,来进行流量控制
11、DegradeSlot
DegradeSlot通过统计信息以及预设的规则,来做熔断降级
参考:
https://www.javadoop.com/post/sentinel
https://blog.csdn.net/qq924862077/article/details/97423682
https://github.com/alibaba/Sentinel/blob/master/doc/awesome-sentinel.md
https://github.com/alibaba/Sentinel/wiki/Sentinel%E5%B7%A5%E4%BD%9C%E4%B8%BB%E6%B5%81%E7%A8%8B
https://github.com/alibaba/Sentinel/wiki/Sentinel-%E6%A0%B8%E5%BF%83%E7%B1%BB%E8%A7%A3%E6%9E%90