目錄
1. 概述
之前分析過Sentinel是基於責任鏈的模式,其邏輯處理部分是一個有一個的Slot
這裏大概的介紹下每種Slot的功能職責:
-
NodeSelectorSlot
負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級; -
ClusterBuilderSlot
則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用作爲多維度限流,降級的依據; -
StatisticsSlot
則用於記錄,統計不同維度的 runtime 信息; -
SystemSlot
則通過系統的狀態,例如 load1 等,來控制總的入口流量; -
AuthoritySlot
則根據黑白名單,來做黑白名單控制; -
FlowSlot
則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流; -
DegradeSlot
則通過統計信息,以及預設的規則,來做熔斷降級;
每個Slot執行完業務邏輯處理後,會調用fireEntry()方法,該方法將會觸發下一個節點的entry方法,下一個節點又會調用他的fireEntry,以此類推直到最後一個Slot,由此就形成了sentinel的責任鏈。
2. 一些需要知道的前提
2.1. Resource
資源,是Sentinel世界中的抽象,任何東西都能被定義成資源,自己提供的服務,調用的服務,甚至一段代碼,有了資源才能在資源上定義規則,去進行限流降級之類的操作
在Sentinel中提供了兩個默認是Resource分別是StringResourceWrapper和MethodResourceWrapper
2.2. Context
Context是上下文的意思,一個線程對應一個Context
其中有三個屬性
- name:名字
- entranceNode:調用鏈入口
- curEntry:當前entry
- origin:調用者來源
- async:異步
2.3. Entry
每次調用 SphU.entry()
都會生成一個Entry入口,該入口中會保存了以下數據:入口的創建時間,當前入口所關聯的節點(Node),當前入口所關聯的調用源對應的節點。Entry是一個抽象類,他只有一個實現類,在CtSph中的一個靜態類:CtEntry
其中有這些屬性
- createtime
- curNode
- originNode
- error
- resourceWrapper
- parent
- child
- chain
- context
紅色的是抽象類Entry的,黑色的是CtEntry中的
一個Entry相當於一個token只有正常生成了一個entry才能算pass,不然報異常BlockException肯定是限流了
2.4. Node
節點是用來保存某個資源的各種實時統計信息的,他是一個接口,通過訪問節點,就可以獲取到對應資源的實時狀態,以此爲依據進行限流和降級操作。
有幾種節點類型
- StatisticNode:統計節點
- DefaultNode:默認節點,NodeSelectorSlot中創建的就是這個節點
- ClusterNode:集羣節點
- EntranceNode:該節點表示一棵調用鏈樹的入口節點,通過他可以獲取調用鏈樹中所有的子節點
3. 深入分析
3.1. demo啓動
Entry entry = null;
try {
entry = SphU.entry("abc");
entry = SphU.entry("abc");
} catch (BlockException e1) {
} finally {
if (entry != null) {
entry.exit();
}
}
CtSph.entryWithPriority
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
//先從ThreadLocal獲取,第一次肯定是null
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的部分
context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
}
//省略
}
3.2. 創建Context
ContextUtil.trueEnter
protected static Context trueEnter(String name, String origin) {
//從ThreadLocal中獲取,第一次肯定是null
Context context = contextHolder.get();
if (context == null) {
//這裏是根據Context的名字獲取Node
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 {
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中
Map<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(
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;
}
這裏的邏輯還是比較簡單的
- 首先在ThreadLocal獲取,獲取不到就創建,不然就返回
- 然後再Map中根據ContextName找一個Node
- 沒有找到Node就加鎖的方式,創建一個EntranceNode,然後放入Map中
- 創建Context,設置node,name,origin,再放入ThreadLocal中
到此Context就創建完成
目前Context對象的狀態如下圖
3.3. 創建Entry
新建Entry的過程
Entry e = new CtEntry(resourceWrapper, chain, context);
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper);
this.chain = chain;
this.context = context;
setUpEntryFor(context);
}
private void setUpEntryFor(Context context) {
// The entry should not be associated to NullContext.
if (context instanceof NullContext) {
return;
}
this.parent = context.getCurEntry();
if (parent != null) {
((CtEntry)parent).child = this;
}
context.setCurEntry(this);
}
當第一次Entry生成的時候,context.getCurEntry必定是NULL,那麼直接執行Context.setCurEntry方法
然後這個Context的狀態如下圖
再執行一次新的Sphu.entry後會再次新建一個Entry,這個時候curEntry不是null,那麼執行((CtEntry)parent).child = this;
結果如下圖
可以看出,原來的CtEntry被移出Context,新建的CtEntry和舊CtEntry通過內部的parent和child引用相連
3.4. 執行NodeSelectorSlot.entry
之前分析過,Sentinel是基於責任鏈模式,責任鏈第一個slot是NodeSelectorSlot
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
//這裏有個緩存,根據context的名字緩存node
DefaultNode node = map.get(context.getName());
//雙重檢測,線程安全
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
//這裏生成的是DefaultNode節點
node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
//下面這些邏輯是放入map的邏輯,因爲後期map比較大,所以這樣放入,性能會高一些
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
}
// 關鍵在這,這是修改調用鏈樹的地方
((DefaultNode)context.getLastNode()).addChild(node);
}
}
//替換context中的curEntry中的curNode
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
// Context.java
public Context setCurNode(Node node) {
this.curEntry.setCurNode(node);
return this;
}
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
//CteEntry.java
public Node getLastNode() {
return parent == null ? null : parent.getCurNode();
}
//DefaultNode.java
public void addChild(Node node) {
if (node == null) {
RecordLog.warn("Trying to add null child to node <{0}>, ignored", id.getName());
return;
}
if (!childList.contains(node)) {
synchronized (this) {
if (!childList.contains(node)) {
Set<Node> newSet = new HashSet<Node>(childList.size() + 1);
newSet.addAll(childList);
newSet.add(node);
childList = newSet;
}
}
RecordLog.info("Add child <{0}> to node <{1}>", ((DefaultNode)node).id.getName(), id.getName());
}
}
查詢緩存中是否有這個node這裏的邏輯也很簡單
- 根據ContextName查詢緩存是否有這個Node
- 沒有就生成這個DefaultNode,放入緩存,然後構造調用樹鏈
- Context的curEntry中的curnode設置爲這個node
但是這樣有個情況是第二步不是經常出現的,第二步出現的前提是node取不到,而node在緩存中獲取不到的條件是contextName不同,除非不同線程纔有可能不同contextName,不僅如此,還有非NodeSelectorSlot同對象,那麼其中的map是不同的
3.4.1. 相同資源名情況下
假設我們先回到現有一個Entry生成的的情況下
然後執行NodeSelectorSlot中的entry方法
這個時候curEntry是等於CtEntry,但是CtEntry中的parent是null,所以getLastNode還是返回entranceNode
然後再執行下面方法setCurNode
結果如下圖
注:這兩個Node是相同的Node,是一個對象
然後再執行一次生成Entry和一次NodeSelectorSlot中的entry方法
這一次Context還是相同的Context,因爲再一個線程中,那麼就不會再生成新的Node,只執行上述過程的第一步和第三步
結果如下圖
注:這三個Node都是相同的Node,因爲根據ContextName從緩存中獲取的
3.4.2. 不同資源名情況下
Entry entry = null;
try {
entry = SphU.entry("abc"); //資源名abc
System.out.println("pass");
entry = SphU.entry("abcd"); //資源名abcd
System.out.println("pass");
} catch (BlockException e1) {
System.out.println("block");
} finally {
if (entry != null) {
entry.exit();
}
}
爲什麼不同資源名會不同?
本質在於ChainSlot的生成問題
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
//可以看出,chain鏈根據資源來作爲key,不同的資源肯定是不同chain鏈
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 = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
然後在NodeSelectorSlot中
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
這是個私有屬性,不同資源情況下,默認的map中是沒有緩存的
下面開始分析
先回到第一個Entry生成並執行了NodeSelectorSlot的entry方法情況
然後在執行一次Entry生成並執行了NodeSelectorSlot的entry方法情況
結果如下圖