逅弈 轉載請註明原創出處,謝謝!java
Sentinel 原理-全解析node
Sentinel 原理-實體類github
Sentinel 系列教程,現已上傳到 github 和 gitee 中:ide
咱們已經知道了sentinel實現限流降級的原理,其核心就是一堆Slot組成的調用鏈。ui
這裏大概的介紹下每種Slot的功能職責:
NodeSelectorSlot
負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級;ClusterBuilderSlot
則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用做爲多維度限流,降級的依據;StatisticsSlot
則用於記錄,統計不一樣維度的 runtime 信息;SystemSlot
則經過系統的狀態,例如 load1 等,來控制總的入口流量;AuthoritySlot
則根據黑白名單,來作黑白名單控制;FlowSlot
則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流;DegradeSlot
則經過統計信息,以及預設的規則,來作熔斷降級;每一個Slot執行完業務邏輯處理後,會調用fireEntry()方法,該方法將會觸發下一個節點的entry方法,下一個節點又會調用他的fireEntry,以此類推直到最後一個Slot,由此就造成了sentinel的責任鏈。
下面咱們就來詳細研究下這些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方法時,該節點可能下會建立有一系列的子節點。<br />另外每一個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的功能已經基本分析清楚了。