官网上的架构图
Sentinel 的核心部分为 ProcessorSlotChain
,由图可知是由将不同的 slot 按照指定顺序串在一起(责任链模式),通过此过滤链可以将监控统计、限流、熔断降级等功能整合在一起。系统会为每一个资源创建一套 SlotChain!
ps:使用@SentinelResource 注解定义资源并配置 blockHandler 和 fallback 函数来进行限流之后的处理
分析
在springboot中会有自动配置功能,通过查看spring.factories文件找到 SentinelAutoConfiguration类
此类中,通过下面方法来实现了 Sentinel最核心的功能
通过切面进行方法增强
点进去查看
此类是利用AspectJ 来实现 aop 增强,对 @SentinelResource标注的资源进行加强!Entry 可以是可以操作资源的对象
创建Entry对象
进入红框内的方法,经过一系列ctrl + 左键进入到如下方法
从上图可以总结出,Entry对象的创建有两大步:1. 创建 Context 2. 创建 ProcessorSlot
让我们再来重点看看这两个方法
创建 Context 对象
protected static Context trueEnter(String name, String origin) {
// 尝试着从ThreadLocal中获取Context
Context context = contextHolder.get();
// 若ThreadLocal中没有context,则尝试着从缓存map中获取
if (context == null) {
// 缓存map的key为context名称,value为EntranceNode
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
// 获取EntranceNode——双重检测锁DCL——为了防止并发创建
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 若缓存map的size 大于 context数量的最大阈值,则直接返回NULL_CONTEXT
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);
// Add entrance node.将新建的node添加到ROOT
Constants.ROOT.addChild(node);
// 将新建node写入到缓存map
// 为了防止“迭代稳定性问题”——iterate stable——对于共享集合的写操作
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
// 将context的name与entranceNode封装为context
context = new Context(node, name);
// 初始化context的来源
context.setOrigin(origin);
// 将context写入到ThreadLocal
contextHolder.set(context);
}
return context;
}
重点看一下 DCL 机制 和 如何解决集合迭代性问题
创建 ProcessorSlot
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 从缓存map中获取当前资源的SlotChain
// 缓存map的key为资源,value为其相关的SlotChain
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
// DCL
// 若缓存中没有相关的SlotChain,则创建一个并放入到缓存
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.
// 缓存map的size >= chain数量最大阈值,则直接返回null,不再创建新的chain
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// 创建新的chain
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;
}
**SlotChainProvider.newSlotChain(); **
public static ProcessorSlotChain newSlotChain() {
// 若builder不为null,则直接使用builder构建一个chain,否则先创建一个builder
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// Resolve the slot chain builder SPI.
// 通过SPI方式创建一个builder
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
// 若通过SPI方式未能创建builder,则手工new一个DefaultSlotChainBuilder
if (slotChainBuilder == null) {
// Should not go through here.
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
slotChainBuilder.getClass().getCanonicalName());
}
// 构建一个chain
return slotChainBuilder.build();
}
通过 SPI 机制来加载指定的对象
接下来,开始执行操作(即在上面创建 Entry步骤中)
参考责任链模式,就是通过一个链表把不同 Slot 串联起来。
- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
- StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
- FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
- AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
- DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
- SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
滑动时间窗算法
先了解一下固定的时间窗算法
该算法原理是,系统会自动选定一个时间窗口的起始零点,然后按照固定长度将时间轴划分为若干定长的时间窗口。
当请求到达时,系统会查看请求到达的时间点所在的时间窗口,其统计的数据是否超出了预定的阈值。未超出,则请求通过,否则被限流
存在的问题
跨窗口的时间窗长度范围内统计的数据却超出了阈值。这就是滑动时间窗要解决的问题
算法原理
滑动时间窗限流算法并没有划分固定的时间窗起点与终点,而是将每一次请求到来的时间点作为统计时间窗的终点,起点则是终点向前推时间窗长度的时间点。
算法改进:将时间窗口拆分为若干固定长度的样本窗口
这样每个样本窗口内统计值其对应时间段内的流量数据,可以提高数据的重复利用率
一些比较重要的概念:
- context:表示资源的上下文信息。一个资源在不同的context中可能会有不同的规则
- Node:用于完成数据统计的接口
- entry 操作资源的对象