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