Sentinel原始碼分析(第二篇):NodeSelectorSlot和ClusterBuilderSlot分析
1. 前言
上一篇介紹了Sentinel的Context、Entry、Node相關的資訊。在建立Node時,涉及到了NodeSelectorSlot和ClusterBuilderSlot,建立Entry的時候,會建立一個chain。本文會通過原始碼分析這些物件的作用。
2. 功能插槽
2.1. 功能插槽的作用
當使用SphU.entry()獲取資源建立Entry物件的時候,會建立一系列的功能插槽,這些插槽以責任鏈的方式組合在一起,每個插槽分別處理不同的功能。
在CtSph#entryWithPriority()方法中會嘗試去獲取chain,如果沒有,則建立:
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
複製程式碼
具體的獲取流程:
原始碼位置:CtSph.java
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. 處理鏈的數量不能超過6000,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;
}
複製程式碼
可以看到,chain會先儲存到一個靜態的全域性map中,以資源名稱為key,chain為value,所以同一個resource只有一個chain。
實際建立chain的地方是SlotChainProvider.newSlotChain():
#原始碼位置:SlotChainProvider.java
private static volatile SlotChainBuilder slotChainBuilder = null;
public static ProcessorSlotChain newSlotChain() {
//如果builder不為空,建立chain,否則先建立builder,再建立chain
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
resolveSlotChainBuilder();
if (slotChainBuilder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder,using default");
slotChainBuilder = new DefaultSlotChainBuilder();
}
return slotChainBuilder.build();
}
複製程式碼
2.2. SlotChainBuilder
SlotChainBuilder負責建立功能插槽鏈,SlotChainBuilder是一個介面,裡面只有一個build方法,定義功能插槽如何建立。
可以看到首先看一下SlotChainBuilder有三個實現,不同的實現增加不同的處理能力。
- DefaultSlotChainBuilder:預設的功能插槽構造器
- GatewaySlotChainBuilder:閘道器插槽構造器,裡面新增了閘道器限流功能
- HotParamSlotChainBuilder:熱點引數插槽構造器,裡面新增了熱點引數功能
這裡看一下預設的功能插槽構造器,其他的兩個只是在適當的位置新增一個處理插槽。
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;
}
}
複製程式碼
可以看到build方法主要是建立一個DefaultProcessorSlotChain,然後將不同功能的插槽新增進去。
需要注意的是,Sentinel是通過SPI的形式選擇使用哪個SlotChainBuilder來建立功能插槽的。
private static final ServiceLoader<SlotChainBuilder> LOADER = ServiceLoader.load(SlotChainBuilder.class);
複製程式碼
2.3. ProcessorSlot
ProcessorSlot代表功能插槽物件,定義了進入功能插槽和退出功能插槽的介面,具體的功能插槽都實現了該介面。
ProcessorSlot繼承結構如下:
通過繼承可以發現,ProcessorSlot主要是兩類,一類是具有特定功能的功能插槽,比如限流、統計、降級等,還有一類是主要是形成插槽鏈,主要就是DefaultProcessorSlotChain。
2.3.1. DefaultProcessorSlotChain
DefaultProcessorSlotChain代表功能處理鏈的開始,裡面定義了兩個節點,第一個節點和最後一個節點,並提供新增節點的方法。
public class DefaultProcessorSlotChain extends ProcessorSlotChain {
//預設先建立一個頭結點插槽
AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
@Override
public void entry(Context context,ResourceWrapper resourceWrapper,Object t,int count,boolean prioritized,Object... args)
throws Throwable {
super.fireEntry(context,resourceWrapper,t,count,prioritized,args);
}
@Override
public void exit(Context context,Object... args) {
super.fireExit(context,args);
}
};
AbstractLinkedProcessorSlot<?> end = first;
@Override
public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
protocolProcessor.setNext(first.getNext());
first.setNext(protocolProcessor);
if (end == first) {
end = protocolProcessor;
}
}
@Override
public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
end.setNext(protocolProcessor);
end = protocolProcessor;
}
}
複製程式碼
2.3.2. AbstractLinkedProcessorSlot
AbstractLinkedProcessorSlot非常的重要,不僅實現了ProcessorSlot,還實現功能插槽鏈之間的如何連線的。
private AbstractLinkedProcessorSlot<?> next = null;
複製程式碼
AbstractLinkedProcessorSlot裡面通過新增一個next指標標記下一個功能處理槽是哪一個,這樣,在一個功能槽處理完流程後,通過next可以找到下一個功能槽,退出的時候也是一樣的邏輯。
2.4. 功能插槽小結
上面講了功能插槽建立的流程,Sentinel是如何將所有的插槽組織工作的。但具體每個功能插槽是怎麼工作的,在這個地方先不講。最後,通過一個圖看一下DefaultSlotChainBuilder建立的功能插槽鏈結構:
3. NodeSelectorSlot
官方檔案是這樣描述NodeSelectorSlot的:這個 slot 主要負責收集資源的路徑,並將這些資源的呼叫路徑以樹狀結構儲存起來,用於根據呼叫路徑進行流量控制。
先看原始碼:
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
/**
* {@link DefaultNode}s of the same resource in different context.
*/
private volatile Map<String,DefaultNode> map = new HashMap<String,DefaultNode>(10);
@Override
public void entry(Context context,Object obj,Object... args)
throws Throwable {
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
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.setCurNode(node);
fireEntry(context,node,args);
}
@Override
public void exit(Context context,Object... args) {
fireExit(context,args);
}
}
複製程式碼
NodeSelectorSlot會建立一個DefaultNode,然後將該節點設定到Context下的最新一個節點子節點列表中,然後將上下文的當前節點設定為本次建立的節點。
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
複製程式碼
上面是獲取最新節點的邏輯。
NodeSelectorSlot主要作用就是收集資源的路徑。在這個Slot中,建立了一個DefaultNode,在上一篇中講到DefaultNode是以Context和Resource為維度儲存統計指標的,但是這個地方的map是根據Context來儲存的,這個是為什麼呢?需要注意的是功能插槽處理鏈的建立是根據Resource來的,一個Resource只會建立一個chain,不同的Resource建立不同的chain,所以這地方雖然是使用Context作為key,但其實是Context和Resource為維度的。
4. ClusterBuilderSlot
上面的NodeSelector是建立DefaultNode節點,CLusterBuilderSlot主要是負責建立ClusterNode和來源節點。至於這兩個節點的作用,在上一篇已經講過。
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,DefaultNode node,Object... args)
throws Throwable {
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
// Create the cluster node.
clusterNode = new ClusterNode();
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 context origin is set,we should get or create a new {@link Node} of
* the specific origin.
*/
//如果context定義了來源名稱,需要給當前的entry建立一個originNode
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
fireEntry(context,args);
}
}
複製程式碼
可以看到,ClusterBuilderSlot中有一個ClusterNode型別的變數,因為之前說過,同一個Resource只有一個Chain,所以這個地方建立的ClusterNode是統計某個Resource的資料,所以ClusterNode的統計維度為Resource。
如果定義了入口的來源,這裡還會建立一個來源節點,作為統計來源節點相關資料的node,這個地方的統計維度是同一個Resource和orgin為維度。
5. 建立Node、Context、Entry整體流程
上面講NodeSelectorSlot和ClusterBuilderSlot的時候,只是把大致的流程說一下,至於建立了Node後整個結構是什麼樣的,這個需要再說一下,如果在看上一篇和這篇中的內容時比較亂,可以看了這一小節後,再去看一下之前的內容。因為到此,整個Sentinel儲存統計資料的流程才講完。
5.1. 再次回憶Context、Entry、Node的建立時機
- Context:在使用ContextUtil.entry()或者SphU.entry()的時候會建立Context。
- EntranceNode:在建立Context的時候建立EntranceNode,並賦值給Context。
- ProcessorSlot::在使用SphU.entry()建立,同一個Resource共享一個。
- Entry:在使用SphU.entry()建立,並通過parent和child將Entry鏈連線,同時將該Entry物件賦值給Context的curEntry.
- DefaultNode:在NodeSelectorSlot中建立,以Resource和Context為維度
- ClusterNode:在ClusterBuilderSlot中建立,以Resource為維度。
- 來源源節點:在ClusterBuilderSlot中建立,以Resource和orgin為維度。
5.2. 流程圖
首先先看一下程式碼:
ContextUtil.enter("context1","origin1");
Entry entry = SphU.entry("resource1");
複製程式碼
當建立一個Entry的時候,整個關係圖如下:
這個時候在獲取同一個資源一次:
Entry entry2 = SphU.entry("resource1");
複製程式碼
關係圖如下:
接下來在建立不同資源的:
Entry entry3 = SphU.entry("resource2");
Entry entry4 = SphU.entry("resource3");
複製程式碼
關係圖如下:
上面是在同一個Context下,如果是在不同的Context下,由於chain是以Resource為維度的,所以ClusterNode對同一個Resource只會存在一個,所以多個Context下,關係圖就是把上圖複製一份,只是ClusterNode指向同一個。
根據關係圖,可以很清晰的看到ClusterNode、DefaultNode、OrginNode是按什麼樣的維度來進行統計的。
6. 小結
本篇主要講Sentinel通過責任鏈的方式將各種插槽組合起來實現限流降級等功能,並分析了前兩個Slot:NodeSelectorSlot和ClusterNodeSlot的作用。最後結合第一篇的內容畫出不同場景下Context、Entry、Node的關係,更加體現ClusterNode、DefaultNode、源節點是按照什麼維度進行統計的。在下一篇中,將繼續分析Sentinel中的統計功能StatisticSlot,以及各種Node節點是怎麼儲存資料的。