Sentinel中有很多類(lèi)型的Node荠卷,例如DefaultNode豪筝、StatisticNode愚墓、ClusterNode输虱、還有個(gè)EntranceNode總共四種類(lèi)型的Node些楣,第一次看的時(shí)候非常懵逼,Node是啥宪睹?四個(gè)Node有什么不同愁茁?
上篇文章中,我們看到StatisticSlot中使用了Node去統(tǒng)計(jì)了請(qǐng)求信息亭病,那么Node應(yīng)該就是做請(qǐng)求統(tǒng)計(jì)用的鹅很,看下Node接口里定義
public interface Node {
long totalRequest();
long totalSuccess();
long blockRequest();
long totalException();
long passQps();
long blockQps();
long totalQps();
long successQps();
long maxSuccessQps();
long exceptionQps();
long avgRt();
long minRt();
int curThreadNum();
long previousBlockQps();
long previousPassQps();
Map<Long, MetricNode> metrics();
void addPassRequest(int count);
void addRtAndSuccess(long rt, int success);
void increaseBlockQps(int count);
void increaseExceptionQps(int count);
void increaseThreadNum();
void decreaseThreadNum();
void reset();
void debug();
}
方法比較多,但是方法名字很清晰罪帖,那么可以得出結(jié)論:
- 內(nèi)部實(shí)現(xiàn)可以不說(shuō)促煮,但是對(duì)外部來(lái)說(shuō)Node是做為一個(gè)請(qǐng)求數(shù)據(jù)統(tǒng)計(jì)和獲取的載體
另外再看下四個(gè)Node的關(guān)系:
那么這四個(gè)Node中邮屁,哪個(gè)去實(shí)現(xiàn)了Node接口的方法呢? 通過(guò)代碼看到是StatisticNode實(shí)現(xiàn)了Node接口的所有方法菠齿,也就是說(shuō)佑吝,另外三個(gè)Node有兩種可能的作用:
- DefaultNode、ClusterNode绳匀、EntranceNode繼承于StatisticNode芋忿,基于其提供的數(shù)據(jù)統(tǒng)計(jì)和獲取方法,實(shí)現(xiàn)自身一些特殊邏輯
- StatisticNode提供了默認(rèn)的方法疾棵,DefaultNode戈钢、ClusterNode、DntranceNode有自身的計(jì)算邏輯陋桂,需要重寫(xiě)這些方法
接下來(lái)通過(guò)具體代碼分析,來(lái)看下到底是哪種情況蝶溶。
EntranceNode
首先在上一篇文章中嗜历,我們從入口開(kāi)始分析了整個(gè)調(diào)用鏈路流程,最先遇到和Node相關(guān)的代碼的時(shí)候抖所,是在ContextUtil#trueEnter
中
//#com.alibaba.csp.sentinel.Constants
public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
Env.nodeBuilder.buildClusterNode());
//ContextUtil#trueEnter
protected static Context trueEnter(String name, String origin) {
Context context = contextHolder.get();
if (context == null) {
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
//....
} else {
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
Constants.ROOT.addChild(node);
Map<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(
contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
//....
}
return context;
}
這里可以看到梨州,一個(gè)ContextName會(huì)對(duì)應(yīng)一個(gè)EntranceNode,EntranceNode從字面意思來(lái)看田轧,叫做入口節(jié)點(diǎn)暴匠,也就是說(shuō),一個(gè)上下文開(kāi)始的時(shí)候傻粘,會(huì)創(chuàng)建一個(gè)EntranceNode與其對(duì)應(yīng)每窖,代表該上下文的入口。
另外看到EntranceNode會(huì)掛在ROOT節(jié)點(diǎn)下面弦悉,而ROOT又是一個(gè)EntranceNode節(jié)點(diǎn)窒典,而其是全局唯一的,他代表應(yīng)用的入口節(jié)點(diǎn)稽莉,如下
ROOT是一個(gè)EntranceNode類(lèi)型的節(jié)點(diǎn)瀑志,他可以?huà)熳庸?jié)點(diǎn),子節(jié)點(diǎn)為上下文節(jié)點(diǎn)污秆,那么上下文節(jié)點(diǎn)下面掛的是啥劈猪?別急,我們繼續(xù)分析
接下看下類(lèi)的定義
public class EntranceNode extends DefaultNode {
public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {
super(id, clusterNode);
}
@Override
public long avgRt() {
long total = 0;
long totalQps = 0;
for (Node node : getChildList()) {
total += node.avgRt() * node.passQps();
totalQps += node.passQps();
}
return total / (totalQps == 0 ? 1 : totalQps);
}
@Override
public long blockQps() {
long blockQps = 0;
for (Node node : getChildList()) {
blockQps += node.blockQps();
}
return blockQps;
}
@Override
public long blockRequest() {
long r = 0;
for (Node node : getChildList()) {
r += node.blockRequest();
}
return r;
}
@Override
public int curThreadNum() {
int r = 0;
for (Node node : getChildList()) {
r += node.curThreadNum();
}
return r;
}
@Override
public long totalQps() {
long r = 0;
for (Node node : getChildList()) {
r += node.totalQps();
}
return r;
}
@Override
public long successQps() {
long r = 0;
for (Node node : getChildList()) {
r += node.successQps();
}
return r;
}
@Override
public long passQps() {
long r = 0;
for (Node node : getChildList()) {
r += node.passQps();
}
return r;
}
@Override
public long totalRequest() {
long r = 0;
for (Node node : getChildList()) {
r += node.totalRequest();
}
return r;
}
}
可以看到EntranceNode重寫(xiě)了獲取數(shù)據(jù)統(tǒng)計(jì)的方法良拼,獲取的時(shí)候?qū)⑺凶庸?jié)點(diǎn)的數(shù)據(jù)全累加后返回
DefaultNode
第二次遇到與Node相關(guān)的應(yīng)該是調(diào)用鏈中的NodeSelectorSlot战得,在看代碼之前,先看下類(lèi)上的注釋?zhuān)渲凶⑨尷锂?huà)兩個(gè)圖:
machine-root
/ \
/ \
EntranceNode1 EntranceNode2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
| |
+- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
EntranceNode1和EntranceNode2表示兩個(gè)不同的上下文庸推,而上下文節(jié)點(diǎn)下分別掛了個(gè)DefaultNode
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
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.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
// com.alibaba.csp.sentinel.node.DefaultNodeBuilder#buildTreeNode
public DefaultNode buildTreeNode(ResourceWrapper id, ClusterNode clusterNode) {
return new DefaultNode(id, clusterNode);
}
這里與EntranceNode的創(chuàng)建有點(diǎn)類(lèi)似贡避,都是以ContextName為key去保存的痛黎,也就是說(shuō)DefaultNode也是和上下文相關(guān)的節(jié)點(diǎn)。
當(dāng)創(chuàng)建成功后刮吧,會(huì)調(diào)用DefaultNode的addChild方法將創(chuàng)建的DefaultNode掛在某個(gè)節(jié)點(diǎn)下湖饱,我們看下getLastNode返回的是什么
/**
* Current processing entry.
*/
private Entry curEntry;
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
curEntry是什么?在上篇文章分析到的entryWithPriority
方法中杀捻,有如下代碼
Entry e = new CtEntry(resourceWrapper, chain, context);// 1
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);
}
注意看標(biāo)記1這個(gè)位置井厌,這里創(chuàng)建了一個(gè)Entry,并將創(chuàng)建好的上下文對(duì)象作為參數(shù)傳進(jìn)去致讥,看下其構(gòu)造方法
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper);
this.chain = chain;
this.context = context;
setUpEntryFor(context);
}
private void setUpEntryFor(Context context) {
if (context instanceof NullContext) {
return;
}
this.parent = context.getCurEntry();//1
if (parent != null) {//2
((CtEntry)parent).child = this;//3
}
context.setCurEntry(this);//4
}
- 1~3:獲取當(dāng)前上下文的curEntry仅仆,如果不為空,則表示當(dāng)前上下文中垢袱,在創(chuàng)建Entry之前就已經(jīng)有一個(gè)Entry被創(chuàng)建過(guò)了墓拜,那么需要設(shè)置父子關(guān)系
- 4:這個(gè)地方就是我們要找的curEntry初始化的地方
setUpEntryFor方法可能有點(diǎn)難理解,什么情況下parent不為空请契,首先要知道的是只有我們調(diào)用SphU#entry
方法的時(shí)候才會(huì)創(chuàng)建一個(gè)Entry咳榜,即Entry代表一個(gè)當(dāng)前調(diào)用的一個(gè)標(biāo)志,假設(shè)我們?cè)谝淮紊舷挛闹姓{(diào)用了多次爽锥,也是可行的涌韩,那么這時(shí)候會(huì)創(chuàng)建多個(gè)Entry,如以下代碼:
ContextUtil.enter("contextName1");
Entry entry1 = null;
try {
entry1 = SphU.entry("resourceName1");
System.out.println("run method 1");
Entry entry2 = null;
try {
entry2 = SphU.entry("resourceName2");
System.out.println("run method 1");
}finally {
if (entry2 != null) {
entry2.exit();
}
}
} finally {
if (entry1 != null) {
entry1.exit();
}
}
這種情況下氯夷,entry2的parent就是entry1臣樱,而entry1的parent為空
分析完curEntry的獲取后 ,再回到getLastNode方法腮考,當(dāng)curEntry不為空雇毫,還需要再判斷一下curEntry.getLastNode
是否為空,看下其實(shí)現(xiàn)
//com.alibaba.csp.sentinel.CtEntry#getLastNode
public Node getLastNode() {
return parent == null ? null : parent.getCurNode();
}
如果當(dāng)前Entry有parent踩蔚,則返回其parent對(duì)應(yīng)的節(jié)點(diǎn)嘴拢,如果parent為空,則返回Context對(duì)應(yīng)的EntranceNode
那么在上面的栗子中寂纪,entry1進(jìn)入到NodeSelectorSlot#entry
方法的時(shí)候席吴,由于parent為空,所以curEntry != null && curEntry.getLastNode() != null
這行代碼為false捞蛋,Context#getLastNode
方法返回EntranceNode
孝冒,這樣說(shuō)可能不好理解,現(xiàn)在以上面的代碼為例拟杉,一步步分析Entry和Node所構(gòu)成的結(jié)構(gòu)
當(dāng)代碼剛進(jìn)入Entry的時(shí)候庄涡,此時(shí)的結(jié)構(gòu)如下:
- EntranceNode1在
ContextUtil#trueEnter
中被創(chuàng)建且和上下文Context綁定 - curEntry在
entryWithPriority
方法中初始化且和上下文Context綁定,curEntry也即使代碼中的entry1
這時(shí)候代碼執(zhí)行到((DefaultNode)context.getLastNode()).addChild
搬设,由于curEntry
即entry1沒(méi)有parent穴店,所以context.getLastNode()
返回的是EntranceNode1撕捍,將創(chuàng)建的DefaultNode掛在其下面,此時(shí)結(jié)構(gòu)如下:
代碼繼續(xù)走到context.setCurNode(node);
即curEntry.setCurNode(node)
泣洞,執(zhí)行完畢后忧风,此時(shí)結(jié)構(gòu)如下:
然后entry1繼續(xù)往后執(zhí)行,執(zhí)行完畢后球凰,entry2有執(zhí)行NodeSelectorSlot#entry
方法狮腿,此時(shí)結(jié)構(gòu)如下:
- curEntry的變化在entryWithPriority方法中處理
代碼繼續(xù)走到((DefaultNode)context.getLastNode()).addChild
,這時(shí)候由于curEntry的parent不為空呕诉,那么就會(huì)去到DefaultNode1缘厢,將創(chuàng)建的DefaultNode掛在其下面,執(zhí)行完畢后甩挫,此時(shí)結(jié)構(gòu)如下:
代碼繼續(xù)走到context.setCurNode(node)
贴硫,執(zhí)行完畢后,此時(shí)結(jié)構(gòu)如下:
好了伊者,到此為止英遭,了解整個(gè)節(jié)點(diǎn)鏈路的構(gòu)建過(guò)程,可以發(fā)現(xiàn)删壮,對(duì)于一個(gè)資源贪绘,在同個(gè)上下文中兑牡,多次調(diào)用entry央碟,會(huì)創(chuàng)建多個(gè)DefaultNode節(jié)點(diǎn),這些節(jié)點(diǎn)依次掛在上下文的入口節(jié)點(diǎn)EntranceNode下面均函,而每個(gè)節(jié)點(diǎn)會(huì)負(fù)責(zé)當(dāng)前上下文中調(diào)用entry后一個(gè)代碼塊的的請(qǐng)求數(shù)據(jù)的統(tǒng)計(jì)亿虽,記住,DefaultNode是與上下文相關(guān)的苞也,假設(shè)是不同上下文洛勉,那么會(huì)呈現(xiàn)之前發(fā)過(guò)的結(jié)構(gòu)
* machine-root
* / \
* / \
* EntranceNode1 EntranceNode2
* / \
* / \
* DefaultNode(nodeA) DefaultNode(nodeA)
* | |
* +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA)
最下面兩個(gè)DefaultNode是在不同上下文中調(diào)用entry所產(chǎn)生的結(jié)構(gòu)(每個(gè)上下文只調(diào)用一次entry)
ClusterNode
終于輪到最后一個(gè)Node了,在NodeSelectorSlot
之后如迟,還有個(gè)ClusterBuilderSlot
收毫,其中有ClusterNode
的處理
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args)
throws Throwable {
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
// Create the cluster node.
clusterNode = Env.nodeBuilder.buildClusterNode();
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(Math.max(clusterNodeMap.size(), 16));
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
node.setClusterNode(clusterNode);
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
可以看到這里也有個(gè)Map結(jié)構(gòu),但是殷勘,key是資源此再,而不是像以前那樣是上下文,所以這里就已經(jīng)清楚了玲销,ClusterNode和資源綁定输拇,即使是不同上下文,同一個(gè)資源贤斜,應(yīng)該都是只有一個(gè)ClusterNode策吠,由其進(jìn)行流量統(tǒng)計(jì)
另外逛裤,當(dāng)創(chuàng)建完成后還會(huì)調(diào)用node.setClusterNode(clusterNode);
將ClusterNode
與DefaultNode進(jìn)行關(guān)聯(lián),即不同的DefaultNode
都關(guān)聯(lián)了一個(gè)ClusterNode
猴抹,這樣我們?cè)诓煌舷挛闹卸伎梢阅玫疆?dāng)前資源一個(gè)總的流量統(tǒng)計(jì)情況
接著再看下DefaultNode重寫(xiě)的方法带族,以其中兩個(gè)為例
@Override
public void increaseBlockQps(int count) {
super.increaseBlockQps(count);
this.clusterNode.increaseBlockQps(count);
}
@Override
public void increaseExceptionQps(int count) {
super.increaseExceptionQps(count);
this.clusterNode.increaseExceptionQps(count);
}
統(tǒng)計(jì)方法中,除了調(diào)用父類(lèi)(即StatisticNode
)來(lái)統(tǒng)計(jì)本身的一個(gè)流量外洽糟,還會(huì)再調(diào)用ClusterNode
的相應(yīng)方法統(tǒng)計(jì)整個(gè)資源的一個(gè)流量
OriginNode
OriginNode
整個(gè)東西其實(shí)在代碼中沒(méi)有對(duì)應(yīng)的類(lèi)炉菲,只不過(guò)是概念上的,其本身還是StatisticNode
坤溃,這個(gè)東西又是什么呢拍霜,在ClusterBuilderSlot中有以下代碼:
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
假設(shè)origin屬性不為空,從通過(guò)origin去獲取一個(gè)Node節(jié)點(diǎn)薪介,然后放到Context中祠饺,getOrCreateOriginNode
方法內(nèi)部邏輯比較簡(jiǎn)單,就是通過(guò)origin去獲取一個(gè)StatisticNode
汁政,他是與origin屬性綁定的道偷,那么origin是什么呢?
在使用ContextUtil創(chuàng)建上下文的時(shí)候记劈,其實(shí)是可以傳入origin參數(shù)的勺鸦,這個(gè)就是上面的origin,他代表的是請(qǐng)求來(lái)源目木,例如我有三個(gè)dubbo服務(wù)换途,分別是A和B、C刽射,調(diào)用關(guān)系為A->C军拟,B->C那么C在限流的時(shí)候,可以將A和B作為origin傳入誓禁,那么ClusterBuilderSlot
就會(huì)為其創(chuàng)建對(duì)應(yīng)節(jié)點(diǎn)懈息,用來(lái)統(tǒng)計(jì)AB服務(wù)對(duì)B服務(wù)調(diào)用的一個(gè)總體情況
總結(jié)
- StatisticNode實(shí)現(xiàn)了Node接口,封裝了基礎(chǔ)的流量統(tǒng)計(jì)和獲取方法
- EntranceNode代表入口節(jié)點(diǎn)摹恰,每個(gè)上下文都會(huì)有一個(gè)入口節(jié)點(diǎn)辫继,用來(lái)統(tǒng)計(jì)當(dāng)前上下文的總體流量情況
- DefaultNode代表同個(gè)資源在不同上下文中各自的流量情況
- ClusterNode代表同個(gè)資源在不同上下文中總體的流量情況
- OriginNode是一個(gè)StatisticNode類(lèi)型的節(jié)點(diǎn),代表了同個(gè)資源請(qǐng)求來(lái)源的流量情況
為什么需要這樣設(shè)計(jì)俗慈?這和Sentinel后續(xù)的限流降級(jí)等規(guī)則的設(shè)計(jì)有關(guān)姑宽,后續(xù)會(huì)繼續(xù)分析