sentinel 核心概念

前段時間筆者在團(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】閱讀更多精彩好文。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容