官方实例程序
public static void main(String[] args) {
// 配置规则.
initFlowRules();
while (true) {
// 1.5.0 版本开始可以直接利用 try-with-resources 特性
try (Entry entry = SphU.entry("HelloWorld")) {
// 被保护的逻辑
System.out.println("hello world");
} catch (BlockException ex) {
// 处理被流控的逻辑
System.out.println("blocked!");
}
}
}
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("HelloWorld");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
执行结果
INFO: log base dir is: C:\Users\cuiyutian1\logs\csp\
INFO: log name use pid is: false
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
blocked!
blocked!
blocked!
blocked!
blocked!
通过上面的执行结果,我们可以看到程序被限流了!System.out.println(“hello world”); 针对我们保护的资源超出预期设置的流控规则时候将会走到 BlockException 异常分支 , 下面将开始一探究竟!
资源保护第一步 SphU.entry()
SphU.entry()
探究第一步 SphU.entry() 做了什么, 我们可以看到 SphU.entry() 执行了如下的逻辑 通过代码分析,可以得到 sentinel 对资源的访问分为入口和出口, 并且在运行的过程中需要上下文信息 (系统会默认初始化), 上下文信息使用Thread Local 进行存放 sentinel 会对访问的资源生成对应的 规则检查链条
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
@Override
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
// 将访问的资源包装成 StringResourceWrapper,并且设置成 EntryType.OUT
StringResourceWrapper resource = new StringResourceWrapper(name, type);
return entry(resource, count, args);
}
// 核心逻辑
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
// 获取上下文信息
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,
// so here init the entry only. No rule checking will be done.
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);
/*
* Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
* so no rule checking will be done.
*/
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
// 创建 CtEntry
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) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
上下文信息
使用默认上下文信息
InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME) 下面的逻辑中能够看到上下文的名称还有 origin 调用来源信息
Constants.CONTEXT_DEFAULT_NAME = sentinel_default_context;
private final static class InternalContextUtil extends ContextUtil {
static Context internalEnter(String name) {
// 会走到这个方法里面
return trueEnter(name, "");
}
static Context internalEnter(String name, String origin) {
return trueEnter(name, origin);
}
}
下面的过程中能看到 sentinel 维护了一个 EntranceNode 和 上下文名称 之间的全局映射关系 并且上下文对应的 EntranceNode 节点会挂在系统 machine-root 节点下
- Constants.ROOT 是一个EntranceNode节点 名称是 machine-root
// ContextUtil类
// 映射关系
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
protected static Context trueEnter(String name, String origin) {
// ThraedLocal中获取
Context context = contextHolder.get();
if (context == null) {
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
// 上下文名称获取 DefaultNode
if (node == null) {
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);
// 添加到 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);
// ThreadLocal存放
contextHolder.set(context);
}
return context;
}
上下文信息小结
经过上面的流程可以得到一些关键信息 Context 是存储ThreadLocal中的 并且系统中维护了上下文名称和 EntranceNode 节点的映射关系 我们的不同线程可以创建多个同名的上下文实例 但是在系统中只对应一个 EntranceNode 节点
资源保护的功能责任链
资源责任链 lookProcessChain(resourceWrapper)
CtSph 类中维护了一份资源和处理槽链的映射 , 如果映射中存在了 对应资源的 处理槽链 则直接返回 不存在时候进行创建 并且维护 chainMap 的映射关系
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
= new HashMap<ResourceWrapper, ProcessorSlotChain>();
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.
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;
}
这里看一下资源类的 hashCode() 和 equals() 方法 可以得到 chainMap 是按照资源名称做映射关系的
public abstract class ResourceWrapper {
protected final String name;
protected final EntryType entryType;
protected final int resourceType;
/**
* Only {@link #getName()} is considered.
*/
@Override
public int hashCode() {
return getName().hashCode();
}
/**
* Only {@link #getName()} is considered.
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof ResourceWrapper) {
ResourceWrapper rw = (ResourceWrapper)obj;
return rw.getName().equals(getName());
}
return false;
}
}
具体构建流程 SlotChainProvider.newSlotChain() 中 可以得到sentinel 使用SPI机制 对 SlotChainBuilder 进行了扩展,并且优先使用自定义的SlotChainBuilder, 无自定义情况下使用 DefaultSlotChainBuilder
public final class SlotChainProvider {
private static volatile SlotChainBuilder builder = null;
// SPI 机制
private static final ServiceLoader<SlotChainBuilder> LOADER = ServiceLoader.load(SlotChainBuilder.class);
// 创建执行槽链
public static ProcessorSlotChain newSlotChain() {
// 不为空则进行build()
if (builder != null) {
return builder.build();
}
// 解析builder
resolveSlotChainBuilder();
// 解析完毕之后, builder 还是空的,则使用默认 DefaultSlotChainBuilder
if (builder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
builder = new DefaultSlotChainBuilder();
}
return builder.build();
}
// 解析 SlotChainBuilder
private static void resolveSlotChainBuilder() {
List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
boolean hasOther = false;
// 拿到SPI获取的 builder
for (SlotChainBuilder builder : LOADER) {
if (builder.getClass() != DefaultSlotChainBuilder.class) {
// 发现了自定义的 builder
hasOther = true;
list.add(builder);
}
}
// 使用自定义的 builder
if (hasOther) {
builder = list.get(0);
} else {
// 没自定义 builder, 开始使用默认
builder = new DefaultSlotChainBuilder();
}
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
+ builder.getClass().getCanonicalName());
}
private SlotChainProvider() {}
}
DefaultSlotChainBuilder 是如何构建的呢 这里的逻辑比较简单 可以理解为使用虚拟头节点方式 尾插发构建了一个链表
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 SystemSlot());
chain.addLast(new AuthoritySlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
SlotChain 小结
通过SlotChain 的分析 可以得到 SlotChain 是和资源绑定在一起的 系统中维护了一份资源名称和SlotChain 的映射关系 结合上下文来说 可以理解为不同的上下文访问同一个资源时候 执行的是同一个 SlotChain
简单使用小结
sentinel 使用是需要建立在上下文中 并且存在以下关键信息: ResourceWrapper \ ProcessorSlotChain \ EntranceNode \ Context \ CtEntry 概念