启动
触发方式
SphU.entry("自定义资源名")
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
- 执行 Env 静态代码块
- 进入 CtSph#entry 方法
Env静态代码块
在 InitExecutor#doInit 中,通过SPI机制加载所有 InitFunc
实现类,然后按顺序调用他们的 init() 方法
public class Env {
public static final Sph sph = new CtSph();
static {
// If init fails, the process will exit.
InitExecutor.doInit();
}
}
常见的 InitFunc 实现类
- com.alibaba.csp.sentinel.metric.extension.MetricCallbackInit 统计Metric信息
- com.alibaba.csp.sentinel.transport.init.CommandCenterInitFunc transport相关
- com.alibaba.csp.sentinel.transport.init.HeartbeatSenderInitFunc transport相关
CtSph#entry
基于 name、type 包装一个 StringResourceWrapper 对象,即抽象的资源;
进入 CtSph#entryWithPriority 方法,
创建一个默认的 Context 对象
InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME) =>
ContextUtil#trueEnter =>
protected static Context trueEnter(String name, String origin) {
// 从 ThreadLocal 中获取
Context context = contextHolder.get();
if (context == null) {
// contextNameNodeMap: key -> DefaultNode , key为contextName , value为EntranceNode
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// Context 最大值为 2000
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
try {
LOCK.lock();
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);
// 添加到 根节点
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();
}
}
}
// 基于 EntranceNode 创建 Context , 保存到 ThreadLocal 并返回
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
Slot链
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
// 双重校验
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 每个资源对应一个 Slot链 , 资源数最大为 6000
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// 先通过SPI获取ProcessorSlotChain, 如果没有返回默认的 DefaultSlotChainBuilder
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;
}
DefaultSlotChainBuilder 中添加了一系列的 Solt , 各个 Solt 执行的顺序,就是创建时添加的顺序:
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
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 AuthoritySlot());
chain.addLast(new SystemSlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
addLast方法主要两行代码:
1. ProcessorSlotChain.end.next = 入参Solt
2. ProcessorSlotChain.end = 入参
即最后的调用顺序如下: NodeSelectorSlot => ClusterBuilderSlot => LogSlot => StatisticSlot => AuthoritySlot => SystemSlot => FlowSlot => DegradeSlot
如果想改变他们的调用顺序,可通过SPI机制实现
NodeSelectorSlot
构造调用链,具体参考 NodeSelectorSlot
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
// 每个资源对应一个 ProcessorSlotChain
// 一个资源可以对应多个Context
// 一个ContextName 对应一个 DefaultNode , 即 一个资源可能对应多个 DefaultNode, 但 一个资源只有一个 ClusterNode
// 针对同一段代码,不同线程对应的Context实例是不一样的,但是对应的Context Name是一样的,所以这时认为是同一个Context,Context我们用Name区分
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
node = new DefaultNode(resourceWrapper, null);
// key 为 ontextName , vaue 为 DefaultNode
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.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
ClusterBuilderSlot
具体参考 ClusterBuilderSlot
每个资源对应一个ClusterNode,并且DefaultNode引用了ClusterNode
LogSlot
记录日志用的,先执行下面的 Solt, 如果报错了或者被Block了,记录到日志中
@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);
}
}
StatisticSlot
核心实现,各种计数的实现逻辑,基于时间窗口实现。 基于触发请求通过 和 请求Block 的回调逻辑,回调逻辑在 MetricCallbackInit 中初始化了, 最终还是靠 StatisticSlotCallbackRegistry
// 省略了一些代码
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
try {
// 执行下来的Solt ,判断是否通过
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// Request passed, add thread count and pass count.
node.increaseThreadNum();
node.addPassRequest(count);
} catch (BlockException e) {
// Blocked, set block exception to current entry.
context.getCurEntry().setError(e);
// Add block count.
node.increaseBlockQps(count);
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseBlockQps(count);
}
}
}
DefaultNode 继承自 StatisticNode , 在 StatisticNode 中有两个属性
// 第一个参数表示 窗口的个数;第二个参数表示 窗口对多长时间进行统计 比如 QPS xx/秒 那就是 1000 毫秒, 所以窗口的长度为 1000/个数
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);
// 窗口长度为1000 60个 刚好一分钟
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
ArrayMetric 持有 LeapArray , LeapArray 主要有两个实现类 OccupiableBucketLeapArray 、 BucketLeapArray , 但根据当前时间获取窗口的核心实现在 LeapArray 抽象类中
滑动窗口简单理解就是: 根据任何时间,都可以获取一个对应的窗口,在该窗口内,保存着在窗口长度时间内通过的请求数、被block的请求数、异常数、RT。基于这些数据,我们就可以得到对应的资源的QPS、RT等指标信息。
核心方法在 LeapArray#currentWindow , 整体思路如下
- 根据当前时间获取时间窗口的下标 (time/windowLength) % array.length()
- 计算当前时间对应时间窗口的开始时间 time - time % windowLength
- 根据下标获取时间窗口,这里分三种情况:
(1) 根据下标没有获取到窗口,此时创建一个窗口。此时代表窗口没有创建 或者 窗口还没有开始滑动, 所以对应的下标位置为null
(2) 根据下标获取到窗口,并且该窗口的开始时间和上面计算的开始时间一样,此时直接返回该窗口
(3) 根据下标获取到窗口,但是该窗口的开始时间大于上面计算的开始时间,这时需要用计算的开始时间重置该窗口的开始时间,这就类似于窗口在滑动
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) {
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
// Successfully updated, return the created bucket.
return window;
} else {
// 循环重试 Contention failed, the thread will yield its time slice to wait for bucket available.
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
return old;
} else if (windowStart > old.windowStart()) {
if (updateLock.tryLock()) {
try {
// Successfully get the update lock, now we reset the bucket.
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
// Contention failed, the thread will yield its time slice to wait for bucket available.
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
// Should not go through here, as the provided time is already behind.
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
todo OccupiableBucketLeapArray 还不太理解
AuthoritySlot
黑白名单规则校验,非常简单
@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);
}
- 加载所有的黑白名单规则
- 遍历所有黑白名单规则,调用 AuthorityRuleChecker#passCheck 方法,如果不通过则抛出 AuthorityException
- 校验逻辑:从 Context 中拿到 originName, 然后判断 originName 是否在 规则的 limitApp 中, 然后判断是 黑名单 还是白名单,然后校验返回结果
SystemSlot
仅对入口流量有效,校验顺序 QPS -> 线程数 -> RT -> BBR -> CPU
@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);
}
FlowSlot
限流处理
三种拒绝策略:直接拒绝、WarnUP、匀速排队
三种限流模式:直接、关联、链路
@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);
}
FlowRuleChecker#checkFlow
- 获取所有限流规则
- 遍历规则,执行 FlowRuleChecker#canPassCheck => FlowRuleChecker#passLocalCheck => rule.getRater().canPass(selectedNode, acquireCount, prioritized)
- rule.getRater() 返回一个 TrafficShapingController 对象, 它有3种实现(代码中有4中,但官方文档只介绍了3种),即对应上面的三种流控模式,每个规则对用的 TrafficShapingController 是在加载规则的时候就确定了
// FlowRuleUtil#generateRater
private static TrafficShapingController generateRater(/*@Valid*/ FlowRule rule) {
if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
switch (rule.getControlBehavior()) {
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP:
return new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(),
ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER:
return new RateLimiterController(rule.getMaxQueueingTimeMs(), rule.getCount());
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER:
return new WarmUpRateLimiterController(rule.getCount(), rule.getWarmUpPeriodSec(),
rule.getMaxQueueingTimeMs(), ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_DEFAULT:
default:
// Default mode or unknown mode: default traffic shaping controller (fast-reject).
}
}
return new DefaultController(rule.getCount(), rule.getGrade());
}
DefaultController
比较简单,判断逻辑 (当前的Count + 本次调用) 是否大于 规则中设置的 阈值
WarmUpController
让QPS在指定的时间内增加到 阈值, 目前每太看懂
RateLimiterController
也比较简单,先按规则中配置的QPS计算每个请求的平均响应时间,然后判断当前请求是否能够等那么久(规则中的时间窗口)
三种限流模式在哪里体现?
其实这个主要就是判断 你的指标数据应该要从哪个 Node 中获取,这部分逻辑在 FlowRuleChecker#selectNodeByRequesterAndStrategy 方法中
- 直接: 根据你的 originName 和 limitApp 来判断是取 ClusterNode 还是 OriginNode
- 关联: 根据关联的资源名取对应的 ClusterNode
- 链路: 判断关联的资源 和 当前的 contextName 是否一致,是则返回 当前的 DefaultNode
DegradeSlot
降级处理
目前有三种降级模式:基于RT、基于异常比例、基于一分钟异常数
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
基于RT
从时间窗口获取RT和规则中配置的阈值进行比较, 通过则 重置计数,然后直接返回; 不通过则 计数加1,如果 计数 >= 5,则进行降级处理
基于异常比例
前提条件 QPS > =5 , 然后用 1s异常数/1s总请求数 , 和规则中配置的阈值进行比较
基于1分钟异常数
直接用1分钟内的异常数和规则的阈值做比较
如何按时间窗口降级
定时任务 + flag
如果降级了, 设置 flag = true , 在 时间窗口秒后, 重置 flag = false ,然后再 passCheck 方法的入口处, 如果 flag = true 就直接降级
和控制台交互
控制台:client端
sentinel-transport模块:Server端,有两种实现: Netty 和 Java原生ServerSocket
在我们引入sentinel-transport模块之后,就可以通过 HTTP API 来获取一些信息,例如:
http://localhost:8719/getRules?type=<XXXX>
http://localhost:8719/getParamRules
核心原理就是sentinel-transport模块启动一个http server,大概流程:
- client向server发送请求
- server端解析请求,根据url内容找到对应的CommandHandler
- server端执行对应的CommandHandler逻辑
- 将结果返回给client端
控制台 和 server 之间会维持心跳,大致流程:
- server向控制台发送心跳, 发送到
控制台地址/registry/machine
这个地址 - 控制台接收心跳,从中获取机器信息
- 控制台将机器信息展示到界面
Server端
源码在 sentinel-transport 模块中,分为Netty实现和Http实现
sentinel-transport-common 公用模块,被其它两个模块引用
sentinel-transport-netty-http 基于Netty实现
sentinel-transport-simple-http 基于Java原生ServerSocket实现
Common模块
在 sentinel-transport-common 的 resources/META-INFO/services 目录下,提供了两个 SPI 接口: com.alibaba.csp.sentinel.init.InitFunc 和 com.alibaba.csp.sentinel.command.CommandHandler
InitFunc之前已经介绍过了,在 Env 的静态代码块中会通过SPI机制加载所有的 InitFunc 实现类,这里主要包括两个: com.alibaba.csp.sentinel.transport.init.CommandCenterInitFunc 和 com.alibaba.csp.sentinel.transport.init.HeartbeatSenderInitFunc
CommandCenterInitFunc
该类主要负责启动Server端
- 通过 CommandCenterProvider 获取到优先级最高的 CommandCenter ,这部分逻辑在 CommandCenterProvider 类的静态代码块中是实现。 如果同时引入了 sentinel-transport-netty-http 和 sentinel-transport-simple-http 模块,默认 SimpleHttpCommandCenter 优先级更高
- 执行 CommandCenter#beforeStart 方法,该步骤主要是通过SPI加载所有 CommandHandler 实现类然后缓存起来;
- 执行 CommandCenter#start 方法,该步骤用于启动Server
HeartbeatSenderInitFunc
从名字上可以猜测到是和心跳检测相关的
- 通过 HeartbeatSenderProvider 获取优先级最高的 HeartbeatSender,这部分逻辑在 HeartbeatSenderProvider 类的静态代码块中是实现。两个模块的实现类分别对应 HttpHeartbeatSender 和 SimpleHttpHeartbeatSender
- 初始化 ScheduledExecutorService ,用于 定时发送心跳
- 设置发送心跳的间隔,全局属性
csp.sentinel.heartbeat.interval.ms
- 通过线程池定时执行 HeartbeatSender#sendHeartbeat 方法
CommandHandler
这个有点类似于web应用中的Controller层,不同的 CommandHandler 实现类对应不同请求url的逻辑。
而所有的 CommandHandler 实现类是在 CommandCenter#beforeStart 方法中通过SPI加载的:
Map<String, CommandHandler> handlers = CommandHandlerProvider.getInstance().namedHandlers();
基于Netty实现
主要类: NettyHttpCommandCenter 、 HttpHeartbeatSender 、 HttpServerHandler
基于ServerSocket实现
主要类: SimpleHttpCommandCenter 、 SimpleHttpHeartbeatSender 、 HttpEventTask
Client端
Server端向控制台发送心跳,控制台解析心跳包获取机器信息 , 对应URL /registry/machine
, 即 MachineRegistryController#receiveHeartBeat 方法,获取机器信息之后添加到缓存中