編者注:前段時間筆者在團隊內部分享了sentinel原理設計與實現,主要講解了sentinel基礎概念和工做原理,工做原理部分你們聽了基本都瞭解了,可是對於sentinel的幾個概念及其之間的關係還有挺多同窗有點模糊的,趁着這幾天比較空,針對sentinel的幾個核心概念,作了一些總結,但願能幫助一些sentinel初學者理清這些概念之間的關係。node
PS:本文主要參考sentinel源碼實現和部分官方文檔,建議小夥伴閱讀本文的同時也大體看下官方文檔和源碼,學習效果更好呦 : ) 官方文檔講解的其實仍是挺詳細的,可是對於這些概念之間的關係可能對於初學者來講還有點不夠。git
估計挺多小夥伴還不知道Sentinel是個什麼東東,Sentinel是一個以流量爲切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性的框架。github地址爲:https://github.com/alibaba/Sentinelgithub
資源是 Sentinel 的關鍵概念。它能夠是 Java 應用程序中的任何內容,例如,由應用程序提供的服務,或由應用程序調用的其它應用提供的服務,甚至能夠是一段代碼。只要經過 Sentinel API 定義的代碼,就是資源,可以被 Sentinel 保護起來。大部分狀況下,可使用方法簽名,URL,甚至服務名稱做爲資源名來標示資源。數組
圍繞資源的實時狀態設定的規則,能夠包括流量控制規則、熔斷降級規則以及系統保護規則。全部規則能夠動態實時調整。緩存
sentinel中調用SphU或者SphO的entry方法獲取限流資源,不一樣的是前者獲取限流資源失敗時會拋BlockException異常,後者或捕獲該異常並返回false,兩者的實現都是基於CtSph類完成的。簡單的sentinel示例:安全
1 Entry entry = null; 2 try { 3 entry = SphU.entry(KEY); 4 System.out.println("entry ok..."); 5 } catch (BlockException e1) { 6 // 獲取限流資源失敗 7 } catch (Exception e2) { 8 // biz exception 9 } finally { 10 if (entry != null) { 11 entry.exit(); 12 } 13 } 14 15 Entry entry = null; 16 if (SphO.entry(KEY)) { 17 System.out.println("entry ok"); 18 } else { 19 // 獲取限流資源失敗 20 }
SphU和SphO兩者沒有孰優孰略問題,底層實現是同樣的,根據不一樣場景選舉合適的一個便可。看了簡單示例以後,一塊兒來看下sentinel中的核心概念,便於理解後續內容。數據結構
resource是sentinel中最重要的一個概念,sentinel經過資源來保護具體的業務代碼或其餘後方服務。sentinel把複雜的邏輯給屏蔽掉了,用戶只須要爲受保護的代碼或服務定義一個資源,而後定義規則就能夠了,剩下的統統交給sentinel來處理了。而且資源和規則是解耦的,規則甚至能夠在運行時動態修改。定義完資源後,就能夠經過在程序中埋點來保護你本身的服務了,埋點的方式有兩種:app
try-catch 方式(經過 SphU.entry(...)
),當 catch 到BlockException時執行異常處理(或fallback)框架
if-else 方式(經過 SphO.entry(...)
),當返回 false 時執行異常處理(或fallback)async
以上這兩種方式都是經過硬編碼的形式定義資源而後進行資源埋點的,對業務代碼的侵入太大,從0.1.1版本開始,sentinel加入了註解的支持,能夠經過註解來定義資源,具體的註解爲:SentinelResource 。經過註解除了能夠定義資源外,還能夠指定 blockHandler 和 fallback 方法。
在sentinel中具體表示資源的類是:ResourceWrapper ,他是一個抽象的包裝類,包裝了資源的 Name 和EntryType。他有兩個實現類,分別是:StringResourceWrapper 和 MethodResourceWrapper。顧名思義,StringResourceWrapper 是經過對一串字符串進行包裝,是一個通用的資源包裝類,MethodResourceWrapper 是對方法調用的包裝。
Context是對資源操做時的上下文環境,每一個資源操做(針對Resource進行的entry/exit
)必須屬於一個Context,若是程序中未指定Context,會建立name爲"sentinel_default_context"的默認Context。一個Context生命週期內可能有多個資源操做,Context生命週期內的最後一個資源exit時會清理該Context,這也預示這整個Context生命週期的結束。Context主要屬性以下:
1 public class Context { 2 // context名字,默認名字 "sentinel_default_context" 3 private final String name; 4 // context入口節點,每一個context必須有一個entranceNode 5 private DefaultNode entranceNode; 6 // context當前entry,Context生命週期中可能有多個Entry,全部curEntry會有變化 7 private Entry curEntry; 8 // The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP). 9 private String origin = ""; 10 private final boolean async; 11 }
注意:一個Context生命期內Context只能初始化一次,由於是存到ThreadLocal中,而且只有在非null時纔會進行初始化。
若是想在調用 SphU.entry() 或 SphO.entry() 前,自定義一個context,則經過ContextUtil.enter()方法來建立。context是保存在ThreadLocal中的,每次執行的時候會優先到ThreadLocal中獲取,爲null時會調用 MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType())
建立一個context。當Entry執行exit方法時,若是entry的parent節點爲null,表示是當前Context中最外層的Entry了,此時將ThreadLocal中的context清空。
剛纔在Context身影中也看到了Entry的出現,如今就談談Entry。每次執行 SphU.entry() 或 SphO.entry() 都會返回一個Entry,Entry表示一次資源操做,內部會保存當前invocation信息。在一個Context生命週期中屢次資源操做,也就是對應多個Entry,這些Entry造成parent/child結構保存在Entry實例中,entry類CtEntry結構以下:
1 class CtEntry extends Entry { 2 protected Entry parent = null; 3 protected Entry child = null; 4 5 protected ProcessorSlot<Object> chain; 6 protected Context context; 7 } 8 public abstract class Entry implements AutoCloseable { 9 private long createTime; 10 private Node curNode; 11 /** 12 * {@link Node} of the specific origin, Usually the origin is the Service Consumer. 13 */ 14 private Node originNode; 15 private Throwable error; // 是否出現異常 16 protected ResourceWrapper resourceWrapper; // 資源信息 17 }
Entry實例代碼中出現了Node,這個又是什麼東東呢 :(,我們接着往下看:
Node(關於StatisticNode的討論放到下一小節)默認實現類DefaultNode,該類還有一個子類EntranceNode;context有一個entranceNode屬性,Entry中有一個curNode屬性。
EntranceNode:該類的建立是在初始化Context時完成的(ContextUtil.trueEnter方法),注意該類是針對Context維度的,也就是一個context有且僅有一個EntranceNode。
DefaultNode:該類的建立是在NodeSelectorSlot.entry完成的,當不存在context.name對應的DefaultNode時會新建(new DefaultNode(resourceWrapper, null),對應resouce)並保存到本地緩存(NodeSelectorSlot中private volatile Map<String, DefaultNode> map);獲取到context.name對應的DefaultNode後會將該DefaultNode設置到當前context的curEntry.curNode屬性,也就是說,在NodeSelectorSlot中是一個context有且僅有一個DefaultNode。
看到這裏,你是否是有疑問?爲何一個context有且僅有一個DefaultNode,咱們的resouece跑哪去了呢,其實,這裏的一個context有且僅有一個DefaultNode是在NodeSelectorSlot範圍內,NodeSelectorSlot是ProcessorSlotChain中的一環,獲取ProcessorSlotChain是根據Resource維度來的。總結爲一句話就是:針對同一個Resource,多個context對應多個DefaultNode;針對不一樣Resource,(不論是否是同一個context)對應多個不一樣DefaultNode。這還沒看明白 : (,好吧,我不bb了,上圖吧:
DefaultNode結構以下:
1 public class DefaultNode extends StatisticNode { 2 private ResourceWrapper id; 3 /** 4 * The list of all child nodes. 5 * 子節點集合 6 */ 7 private volatile Set<Node> childList = new HashSet<>(); 8 /** 9 * Associated cluster node. 10 */ 11 private ClusterNode clusterNode; 12 }
一個Resouce只有一個clusterNode,多個defaultNode對應一個clusterNode,若是defaultNode.clusterNode爲null,則在ClusterBuilderSlot.entry中會進行初始化。
同一個Resource,對應同一個ProcessorSlotChain,這塊處理邏輯在lookProcessChain方法中,以下:
1 ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) { 2 ProcessorSlotChain chain = chainMap.get(resourceWrapper); 3 if (chain == null) { 4 synchronized (LOCK) { 5 chain = chainMap.get(resourceWrapper); 6 if (chain == null) { 7 // Entry size limit. 8 if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) { 9 return null; 10 } 11 12 chain = SlotChainProvider.newSlotChain(); 13 Map<ResourceWrapper, ProcessorSlotChain> newMap = newHashMap<ResourceWrapper, ProcessorSlotChain>( 14 chainMap.size() + 1); 15 newMap.putAll(chainMap); 16 newMap.put(resourceWrapper, chain); 17 chainMap = newMap; 18 } 19 } 20 } 21 return chain; 22 }
StatisticNode中保存了資源的實時統計數據(基於滑動時間窗口機制),經過這些統計數據,sentinel才能進行限流、降級等一系列操做。StatisticNode屬性以下:
1 public class StatisticNode implements Node { 2 /** 3 * 秒級的滑動時間窗口(時間窗口單位500ms) 4 */ 5 private transient volatile Metric rollingCounterInSecond = newArrayMetric(SampleCountProperty.SAMPLE_COUNT, 6 IntervalProperty.INTERVAL); 7 /** 8 * 分鐘級的滑動時間窗口(時間窗口單位1s) 9 */ 10 private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000,false); 11 /** 12 * The counter for thread count. 13 * 線程個數用戶觸發線程數流控 14 */ 15 private LongAdder curThreadNum = new LongAdder(); 16 } 17 public class ArrayMetric implements Metric { 18 private final LeapArray<MetricBucket> data; 19 } 20 public class MetricBucket { 21 // 保存統計值 22 private final LongAdder[] counters; 23 // 最小rt 24 private volatile long minRt; 25 }
其中MetricBucket.counters數組大小爲MetricEvent枚舉值的個數,每一個枚舉對應一個統計項,好比PASS表示經過個數,限流可根據經過的個數和設置的限流規則配置count大小比較,得出是否觸發限流操做,全部枚舉值以下:
public enum MetricEvent { PASS, // Normal pass. BLOCK, // Normal block. EXCEPTION, SUCCESS, RT, OCCUPIED_PASS }
slot是另外一個sentinel中很是重要的概念,sentinel的工做流程就是圍繞着一個個插槽所組成的插槽鏈來展開的。須要注意的是每一個插槽都有本身的職責,他們各司其職無缺的配合,經過必定的編排順序,來達到最終的限流降級的目的。默認的各個插槽之間的順序是固定的,由於有的插槽須要依賴其餘的插槽計算出來的結果才能進行工做。
可是這並不意味着咱們只能按照框架的定義來,sentinel 經過 SlotChainBuilder 做爲 SPI 接口,使得 Slot Chain 具有了擴展的能力。咱們能夠經過實現 SlotsChainBuilder 接口加入自定義的 slot 並自定義編排各個 slot 之間的順序,從而能夠給 sentinel 添加自定義的功能。
那SlotChain是在哪建立的呢?是在 CtSph.lookProcessChain() 方法中建立的,而且該方法會根據當前請求的資源先去一個靜態的HashMap中獲取,若是獲取不到纔會建立,建立後會保存到HashMap中。這就意味着,同一個資源會全局共享一個SlotChain。默認生成ProcessorSlotChain爲:
1 // DefaultSlotChainBuilder 2 public ProcessorSlotChain build() { 3 ProcessorSlotChain chain = new DefaultProcessorSlotChain(); 4 chain.addLast(new NodeSelectorSlot()); 5 chain.addLast(new ClusterBuilderSlot()); 6 chain.addLast(new LogSlot()); 7 chain.addLast(new StatisticSlot()); 8 chain.addLast(new SystemSlot()); 9 chain.addLast(new AuthoritySlot()); 10 chain.addLast(new FlowSlot()); 11 chain.addLast(new DegradeSlot()); 12 13 return chain;
到這裏本文結束了,謝謝小夥伴們的閱讀~ 在理解了這些核心概念以後,相信聰明的你回過頭再看sentinel源碼就不會以爲有很大難度了 : )
往期精選