逅弈 轉載請註明原創出處,謝謝!java
Sentinel 原理-全解析node
咱們已經知道了sentinel實現限流降級的原理,其核心就是一堆Slot組成的調用鏈。post
這裏大概的介紹下每種Slot的功能職責:ui
NodeSelectorSlot
負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級;ClusterBuilderSlot
則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用做爲多維度限流,降級的依據;StatisticsSlot
則用於記錄,統計不一樣維度的 runtime 信息;SystemSlot
則經過系統的狀態,例如 load1 等,來控制總的入口流量;AuthoritySlot
則根據黑白名單,來作黑白名單控制;FlowSlot
則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流;DegradeSlot
則經過統計信息,以及預設的規則,來作熔斷降級;每一個Slot執行完業務邏輯處理後,會調用fireEntry()方法,該方法將會觸發下一個節點的entry方法,下一個節點又會調用他的fireEntry,以此類推直到最後一個Slot,由此就造成了sentinel的責任鏈。this
下面咱們就來詳細研究下這些Slot的原理。
NodeSelectorSlot
是用來構造調用鏈的,具體的是將資源的調用路徑,封裝成一個一個的節點,再組成一個樹狀的結構來造成一個完整的調用鏈,NodeSelectorSlot
是全部Slot中最關鍵也是最複雜的一個Slot,這裏涉及到如下幾個核心的概念:
資源是 Sentinel 的關鍵概念。它能夠是 Java 應用程序中的任何內容,例如,由應用程序提供的服務,或由應用程序調用的其它服務,甚至能夠是一段代碼。
只要經過 Sentinel API 定義的代碼,就是資源,可以被 Sentinel 保護起來。大部分狀況下,可使用方法簽名,URL,甚至服務名稱做爲資源名來標示資源。
簡單來講,資源就是 Sentinel 用來保護系統的一個媒介。源碼中用來包裝資源的類是:com.alibaba.csp.sentinel.slotchain.ResourceWrapper
,他有兩個子類:StringResourceWrapper
和 MethodResourceWrapper
,經過名字就知道能夠將一段字符串或一個方法包裝爲一個資源。
打個比方,我有一個服務A,請求很是多,常常會被陡增的流量沖垮,爲了防止這種狀況,簡單的作法,咱們能夠定義一個 Sentinel 的資源,經過該資源來對請求進行調整,使得容許經過的請求不會把服務A搞崩潰。
每一個資源的狀態也是不一樣的,這取決於資源後端的服務,有的資源可能比較穩定,有的資源可能不太穩定。那麼在整個調用鏈中,Sentinel 須要對不穩定資源進行控制。當調用鏈路中某個資源出現不穩定,例如,表現爲 timeout,或者異常比例升高的時候,則對這個資源的調用進行限制,並讓請求快速失敗,避免影響到其它的資源,最終致使雪崩的後果。
上下文是一個用來保存調用鏈當前狀態的元數據的類,每次進入一個資源時,就會建立一個上下文。**相同的資源名可能會建立多個上下文。**一個Context中包含了三個核心的對象:
1)當前調用鏈的根節點:EntranceNode
2)當前的入口:Entry
3)當前入口所關聯的節點:Node
上下文中只會保存一個當前正在處理的入口Entry,另外還會保存調用鏈的根節點。須要注意的是,每次進入一個新的資源時,都會建立一個新的上下文。
每次調用 SphU#entry()
都會生成一個Entry入口,該入口中會保存瞭如下數據:入口的建立時間,當前入口所關聯的節點,當前入口所關聯的調用源對應的節點。Entry是一個抽象類,他只有一個實現類,在CtSph中的一個靜態類:CtEntry
節點是用來保存某個資源的各類實時統計信息的,他是一個接口,經過訪問節點,就能夠獲取到對應資源的實時狀態,以此爲依據進行限流和降級操做。
可能看到這裏,你們仍是比較懵,這麼多類到底有什麼用,接下來就讓咱們更進一步,挖掘一下這些類的做用,在這以前,我先給你們展現一下他們之間的關係,以下圖所示:
這裏把幾種Node的做用先大概介紹下:
節點 | 做用 |
---|---|
StatisticNode | 執行具體的資源統計操做 |
DefaultNode | 該節點持有指定上下文中指定資源的統計信息,當在同一個上下文中屢次調用entry方法時,該節點可能下會建立有一系列的子節點。 另外每一個DefaultNode中會關聯一個ClusterNode |
ClusterNode | 該節點中保存了資源的整體的運行時統計信息,包括rt,線程數,qps等等,相同的資源會全局共享同一個ClusterNode,無論他屬於哪一個上下文 |
EntranceNode | 該節點表示一棵調用鏈樹的入口節點,經過他能夠獲取調用鏈樹中全部的子節點 |
首先咱們要清楚的一點就是,每次執行entry()方法,試圖衝破一個資源時,都會生成一個上下文。這個上下文中會保存着調用鏈的根節點和當前的入口。
Context是經過ContextUtil建立的,具體的方法是trueEntry,代碼以下:
protected static Context trueEnter(String name, String origin) {
// 先從ThreadLocal中獲取
Context context = contextHolder.get();
if (context == null) {
// 若是ThreadLocal中獲取不到Context
// 則根據name從map中獲取根節點,只要是相同的資源名,就能直接從map中獲取到node
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 省略部分代碼
try {
LOCK.lock();
node = contextNameNodeMap.get(name);
if (node == null) {
// 省略部分代碼
// 建立一個新的入口節點
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
Constants.ROOT.addChild(node);
// 省略部分代碼
}
} finally {
LOCK.unlock();
}
}
// 建立一個新的Context,並設置Context的根節點,即設置EntranceNode
context = new Context(node, name);
context.setOrigin(origin);
// 將該Context保存到ThreadLocal中去
contextHolder.set(context);
}
return context;
}
複製代碼
上面的代碼中我省略了部分代碼,只保留了核心的部分。從源碼中仍是能夠比較清晰的看出生成Context的過程:
那保存在ThreadLocal中的上下文何時會清除呢?從代碼中能夠看到具體的清除工做在ContextUtil的exit方法中,當執行該方法時,會將保存在ThreadLocal中的context對象清除,具體的代碼很是簡單,這裏就不貼代碼了。
那ContextUtil.exit方法何時會被調用呢?有兩種狀況:一是主動調用ContextUtil.exit的時候,二是當一個入口Entry要退出,執行該Entry的trueExit方法的時候,此時會觸發ContextUtil.exit的方法。可是有一個前提,就是當前Entry的父Entry爲null時,此時說明該Entry已是最頂層的根節點了,能夠清除context。
當在一個上下文中屢次調用了 SphU#entry() 方法時,就會建立一棵調用鏈樹。具體的代碼在entry方法中建立CtEntry對象時:
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper);
this.chain = chain;
this.context = context;
// 獲取「上下文」中上一次的入口
parent = context.getCurEntry();
if (parent != null) {
// 而後將當前入口設置爲上一次入口的子節點
((CtEntry)parent).child = this;
}
// 設置「上下文」的當前入口爲該類自己
context.setCurEntry(this);
}
複製代碼
這裏可能看代碼沒有那麼直觀,能夠用一些圖形來描述一下這個過程。
context的建立在上面已經分析過了,初始化的時候,context中的curEntry屬性是沒有值的,以下圖所示:
每建立一個新的Entry對象時,都會從新設置context的curEntry,並將context原來的curEntry設置爲該新Entry對象的父節點,以下圖所示:
某個Entry退出時,將會從新設置context的curEntry,當該Entry是最頂層的一個入口時,將會把ThreadLocal中保存的context也清除掉,以下圖所示:
上面的過程是構造了一棵調用鏈的樹,可是這棵樹只有樹幹,沒有葉子,那葉子節點是在何時建立的呢?DefaultNode就是葉子節點,在葉子節點中保存着目標資源在當前狀態下的統計信息。經過分析,咱們知道了葉子節點是在NodeSelectorSlot的entry方法中建立的。具體的代碼以下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable {
// 根據「上下文」的名稱獲取DefaultNode
// 多線程環境下,每一個線程都會建立一個context,
// 只要資源名相同,則context的名稱也相同,那麼獲取到的節點就相同
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);
// 省略部分代碼
}
// 將當前node做爲「上下文」的最後一個節點的子節點添加進去
// 若是context的curEntry.parent.curNode爲null,則添加到entranceNode中去
// 不然添加到context的curEntry.parent.curNode中去
((DefaultNode)context.getLastNode()).addChild(node);
}
}
// 將該節點設置爲「上下文」中的當前節點
// 實際是將當前節點賦值給context中curEntry的curNode
// 在Context的getLastNode中會用到在此處設置的curNode
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, args);
}
複製代碼
上面的代碼能夠分解成下面這些步驟: 1)獲取當前上下文對應的DefaultNode,若是沒有的話會爲當前的調用新生成一個DefaultNode節點,它的做用是對資源進行各類統計度量以便進行流控; 2)將新建立的DefaultNode節點,添加到context中,做爲「entranceNode」或者「curEntry.parent.curNode」的子節點; 3)將DefaultNode節點,添加到context中,做爲「curEntry」的curNode。
上面的第2步,不是每次都會執行。咱們先看第3步,把當前DefaultNode設置爲context的curNode,其實是把當前節點賦值給context中curEntry的curNode,用圖形表示就是這樣:
屢次建立不一樣的Entry,而且執行NodeSelectorSlot的entry方法後,就會變成這樣一棵調用鏈樹:
PS:這裏圖中的node0,node1,node2多是相同的node,由於在同一個context中從map中獲取的node是同一個,這裏只是爲了表述的更清楚因此用了不一樣的節點名。
上面已經分析了葉子節點的構造過程,葉子節點是保存在各個Entry的curNode屬性中的。
咱們知道context中只保存了入口節點和當前Entry,那子節點是何時保存的呢,其實子節點就是上面代碼中的第2步中保存的。
下面咱們來分析上面的第2步的狀況:
第一次調用NodeSelectorSlot的entry方法時,map中確定是沒有DefaultNode的,那就會進入第2步中,建立一個node,建立完成後會把該節點加入到context的lastNode的子節點中去。咱們先看一下context的getLastNode方法:
public Node getLastNode() {
// 若是curEntry不存在時,返回entranceNode
// 不然返回curEntry的lastNode,
// 須要注意的是curEntry的lastNode是獲取的parent的curNode,
// 若是每次進入的資源不一樣,就會每次都建立一個CtEntry,則parent爲null,
// 因此curEntry.getLastNode()也爲null
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
複製代碼
代碼中咱們能夠知道,lastNode的值多是context中的entranceNode也多是curEntry.parent.curNode,可是他們都是「DefaultNode」類型的節點,DefaultNode的全部子節點是保存在一個HashSet中的。
第一次調用getLastNode方法時,context中curEntry是null,由於curEntry是在第3步中才賦值的。因此,lastNode最初的值就是context的entranceNode。那麼將node添加到entranceNode的子節點中去以後就變成了下面這樣:
緊接着再進入一次,資源名不一樣,會再次生成一個新的Entry,上面的圖形就變成下圖這樣:
此時再次調用context的getLastNode方法,由於此時curEntry的parent再也不是null了,因此獲取到的lastNode是curEntry.parent.curNode,在上圖中能夠很方便的看出,這個節點就是node0。那麼把當前節點node1添加到lastNode的子節點中去,上面的圖形就變成下圖這樣:
而後將當前node設置給context的curNode,上面的圖形就變成下圖這樣:
假如再建立一個Entry,而後再進入一次不一樣的資源名,上面的圖就變成下面這樣:
至此NodeSelectorSlot的基本功能已經大體分析清楚了。
PS:以上的分析是基於每次執行SphU.entry(name)時,資源名都是不同的前提下。若是資源名都同樣的話,那麼生成的node都相同,則只會再第一次把node加入到entranceNode的子節點中去,其餘的時候,只會建立一個新的Entry,而後替換context中的curEntry的值。
NodeSelectorSlot的entry方法執行完以後,會調用fireEntry方法,此時會觸發ClusterBuilderSlot的entry方法。
ClusterBuilderSlot的entry方法比較簡單,具體代碼以下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
// Create the cluster node.
clusterNode = Env.nodeBuilder.buildClusterNode();
// 將clusterNode保存到全局的map中去
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
// 將clusterNode塞到DefaultNode中去
node.setClusterNode(clusterNode);
// 省略部分代碼
fireEntry(context, resourceWrapper, node, count, args);
}
複製代碼
NodeSelectorSlot的職責比較簡單,主要作了兩件事:
1、爲每一個資源建立一個clusterNode,而後把clusterNode塞到DefaultNode中去
2、將clusterNode保持到全局的map中去,用資源做爲map的key
PS:一個資源只有一個ClusterNode,可是能夠有多個DefaultNode
StatisticSlot負責來統計資源的實時狀態,具體的代碼以下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
try {
// 觸發下一個Slot的entry方法
fireEntry(context, resourceWrapper, node, count, args);
// 若是能經過SlotChain中後面的Slot的entry方法,說明沒有被限流或降級
// 統計信息
node.increaseThreadNum();
node.addPassRequest();
// 省略部分代碼
} catch (BlockException e) {
context.getCurEntry().setError(e);
// Add block count.
node.increaseBlockedQps();
// 省略部分代碼
throw e;
} catch (Throwable e) {
context.getCurEntry().setError(e);
// Should not happen
node.increaseExceptionQps();
// 省略部分代碼
throw e;
}
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
DefaultNode node = (DefaultNode)context.getCurNode();
if (context.getCurEntry().getError() == null) {
long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
if (rt > Constants.TIME_DROP_VALVE) {
rt = Constants.TIME_DROP_VALVE;
}
node.rt(rt);
// 省略部分代碼
node.decreaseThreadNum();
// 省略部分代碼
}
fireExit(context, resourceWrapper, count);
}
複製代碼
代碼分紅了兩部分,第一部分是entry方法,該方法首先會觸發後續slot的entry方法,即SystemSlot、FlowSlot、DegradeSlot等的規則,若是規則不經過,就會拋出BlockException,則會在node中統計被block的數量。反之會在node中統計經過的請求數和線程數等信息。第二部分是在exit方法中,當退出該Entry入口時,會統計rt的時間,並減小線程數。
這些統計的實時數據會被後續的校驗規則所使用,具體的統計方式是經過 滑動窗口
來實現的。後面我會詳細分析滑動窗口的原理。
SystemSlot就是根據總的請求統計信息,來作流控,主要是防止系統被搞垮,具體的代碼以下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
SystemRuleManager.checkSystem(resourceWrapper);
fireEntry(context, resourceWrapper, node, count, args);
}
public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
// 省略部分代碼
// total qps
double currentQps = Constants.ENTRY_NODE.successQps();
if (currentQps > qps) {
throw new SystemBlockException(resourceWrapper.getName(), "qps");
}
// total thread
int currentThread = Constants.ENTRY_NODE.curThreadNum();
if (currentThread > maxThread) {
throw new SystemBlockException(resourceWrapper.getName(), "thread");
}
double rt = Constants.ENTRY_NODE.avgRt();
if (rt > maxRt) {
throw new SystemBlockException(resourceWrapper.getName(), "rt");
}
// 徹底按照RT,BBR算法來
if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
if (currentThread > 1 &&
currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) {
throw new SystemBlockException(resourceWrapper.getName(), "load");
}
}
}
複製代碼
其中的Constants.ENTRY_NODE是一個全局的ClusterNode,該節點的值是在StatisticsSlot中進行統計的。
AuthoritySlot作的事也比較簡單,主要是根據黑白名單進行過濾,只要有一條規則校驗不經過,就拋出異常。
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
AuthorityRuleManager.checkAuthority(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, args);
}
public static void checkAuthority(ResourceWrapper resource, Context context, DefaultNode node, int count) throws BlockException {
if (authorityRules == null) {
return;
}
// 根據資源名稱獲取相應的規則
List<AuthorityRule> rules = authorityRules.get(resource.getName());
if (rules == null) {
return;
}
for (AuthorityRule rule : rules) {
// 只要有一條規則校驗不經過,就拋出AuthorityException
if (!rule.passCheck(context, node, count)) {
throw new AuthorityException(context.getOrigin());
}
}
}
複製代碼
FlowSlot主要是根據前面統計好的信息,與設置的限流規則進行匹配校驗,若是規則校驗不經過則進行限流,具體的代碼以下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
FlowRuleManager.checkFlow(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, args);
}
public static void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count) throws BlockException {
List<FlowRule> rules = flowRules.get(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
if (!rule.passCheck(context, node, count)) {
throw new FlowException(rule.getLimitApp());
}
}
}
}
複製代碼
DegradeSlot主要是根據前面統計好的信息,與設置的降級規則進行匹配校驗,若是規則校驗不經過則進行降級,具體的代碼以下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, args);
}
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count) throws BlockException {
List<DegradeRule> rules = degradeRules.get(resource.getName());
if (rules != null) {
for (DegradeRule rule : rules) {
if (!rule.passCheck(context, node, count)) {
throw new DegradeException(rule.getLimitApp());
}
}
}
}
複製代碼
sentinel的限流降級等功能,主要是經過一個SlotChain實現的。在鏈式插槽中,有7個核心的Slot,這些Slot各司其職,能夠分爲如下幾種類型:
1、進行資源調用路徑構造的NodeSelectorSlot和ClusterBuilderSlot
2、進行資源的實時狀態統計的StatisticsSlot
3、進行系統保護,限流,降級等規則校驗的SystemSlot、AuthoritySlot、FlowSlot、DegradeSlot
後面幾個Slot依賴於前面幾個Slot統計的結果。至此,每種Slot的功能已經基本分析清楚了。
更多原創好文,請關注公衆號「逅弈逐碼」