編者註:前段時間筆者在團隊內部分享了sentinel原理設計與實現,主要講解了sentinel基礎概念和工作原理,工作原理部分大家聽了基本都瞭解了,但是對於sentinel的幾個概念及其之間的關係還有挺多同學有點模糊的,趁著這幾天比較空,針對sentinel的幾個核心概念,做了一些總結,希望能幫助一 ...
編者註:前段時間筆者在團隊內部分享了sentinel原理設計與實現,主要講解了sentinel基礎概念和工作原理,工作原理部分大家聽了基本都瞭解了,但是對於sentinel的幾個概念及其之間的關係還有挺多同學有點模糊的,趁著這幾天比較空,針對sentinel的幾個核心概念,做了一些總結,希望能幫助一些sentinel初學者理清這些概念之間的關係。
PS:本文主要參考sentinel源碼實現和部分官方文檔,建議小伙伴閱讀本文的同時也大致看下官方文檔和源碼,學習效果更好呦 : ) 官方文檔講解的其實還是挺詳細的,但是對於這些概念之間的關係可能對於初學者來說還有點不夠。
估計挺多小伙伴還不知道Sentinel是個什麼東東,Sentinel是一個以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性的框架。github地址為:https://github.com/alibaba/Sentinel
資源和規則
資源是 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
resource是sentinel中最重要的一個概念,sentinel通過資源來保護具體的業務代碼或其他後方服務。sentinel把複雜的邏輯給屏蔽掉了,用戶只需要為受保護的代碼或服務定義一個資源,然後定義規則就可以了,剩下的通通交給sentinel來處理了。並且資源和規則是解耦的,規則甚至可以在運行時動態修改。定義完資源後,就可以通過在程式中埋點來保護你自己的服務了,埋點的方式有兩種:
-
try-catch 方式(
通過 SphU.entry(...)
),當 catch 到BlockException時執行異常處理(或fallback) -
if-else 方式(
通過 SphO.entry(...)
),當返回 false 時執行異常處理(或fallback)
以上這兩種方式都是通過硬編碼的形式定義資源然後進行資源埋點的,對業務代碼的侵入太大,從0.1.1版本開始,sentinel加入了註解的支持,可以通過註解來定義資源,具體的註解為:SentinelResource 。通過註解除了可以定義資源外,還可以指定 blockHandler 和 fallback 方法。
在sentinel中具體表示資源的類是:ResourceWrapper ,他是一個抽象的包裝類,包裝了資源的 Name 和EntryType。他有兩個實現類,分別是:StringResourceWrapper 和 MethodResourceWrapper。顧名思義,StringResourceWrapper 是通過對一串字元串進行包裝,是一個通用的資源包裝類,MethodResourceWrapper 是對方法調用的包裝。
Context
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清空。
Entry
剛纔在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,這個又是什麼東東呢 :(,咱們接著往下看:
DefaultNode
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
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
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源碼就不會覺得有很大難度了 : )
往期精選
覺得文章不錯,對你有所啟發和幫助,希望能轉發給更多的小伙伴。如果有問題,請關註下麵公眾號,發送問題給我,多謝。
歡迎小伙伴關註【TopCoder】閱讀更多精彩好文。