Sentinel源碼解析一(流程總覽)

引言

Sentinel作為ali開源的一款輕量級(jí)流控框架储藐,主要以流量為切入點(diǎn)辰妙,從流量控制葱绒、熔斷降級(jí)秉宿、系統(tǒng)負(fù)載保護(hù)等多個(gè)維度來幫助用戶保護(hù)服務(wù)的穩(wěn)定性奶稠。相比于Hystrix睡陪,Sentinel的設(shè)計(jì)更加簡(jiǎn)單盖矫,在 Sentinel中資源定義和規(guī)則配置是分離的种柑,也就是說用戶可以先通過Sentinel API給對(duì)應(yīng)的業(yè)務(wù)邏輯定義資源(埋點(diǎn))厌丑,然后在需要的時(shí)候再配置規(guī)則定欧,通過這種組合方式,極大的增加了Sentinel流控的靈活性怒竿。

引入Sentinel帶來的性能損耗非常小砍鸠。只有在業(yè)務(wù)單機(jī)量級(jí)超過25W QPS的時(shí)候才會(huì)有一些顯著的影響(5% - 10% 左右),單機(jī)QPS不太大的時(shí)候損耗幾乎可以忽略不計(jì)耕驰。

Sentinel提供兩種埋點(diǎn)方式:

  • try-catch 方式(通過 SphU.entry(...))爷辱,用戶在 catch 塊中執(zhí)行異常處理 / fallback
  • if-else 方式(通過 SphO.entry(...)),當(dāng)返回 false 時(shí)執(zhí)行異常處理 / fallback

寫在前面

在此之前朦肘,需要先了解一下Sentinel的工作流程
Sentinel 里面饭弓,所有的資源都對(duì)應(yīng)一個(gè)資源名稱(resourceName),每次資源調(diào)用都會(huì)創(chuàng)建一個(gè) Entry 對(duì)象媒抠。Entry 可以通過對(duì)主流框架的適配自動(dòng)創(chuàng)建弟断,也可以通過注解的方式或調(diào)用 SphU API 顯式創(chuàng)建。Entry 創(chuàng)建的時(shí)候趴生,同時(shí)也會(huì)創(chuàng)建一系列功能插槽(slot chain)阀趴,這些插槽有不同的職責(zé)昏翰,例如默認(rèn)情況下會(huì)創(chuàng)建一下7個(gè)插槽:

  • NodeSelectorSlot 負(fù)責(zé)收集資源的路徑,并將這些資源的調(diào)用路徑刘急,以樹狀結(jié)構(gòu)存儲(chǔ)起來矩父,用于根據(jù)調(diào)用路徑來限流降級(jí);
  • ClusterBuilderSlot 則用于存儲(chǔ)資源的統(tǒng)計(jì)信息以及調(diào)用者信息排霉,例如該資源的 RT, QPS, thread count 等等窍株,這些信息將用作為多維度限流,降級(jí)的依據(jù)攻柠;
  • StatisticSlot 則用于記錄球订、統(tǒng)計(jì)不同緯度的 runtime 指標(biāo)監(jiān)控信息;
  • FlowSlot 則用于根據(jù)預(yù)設(shè)的限流規(guī)則以及前面 slot 統(tǒng)計(jì)的狀態(tài)瑰钮,來進(jìn)行流量控制冒滩;
  • AuthoritySlot 則根據(jù)配置的黑白名單和調(diào)用來源信息,來做黑白名單控制浪谴;
  • DegradeSlot 則通過統(tǒng)計(jì)信息以及預(yù)設(shè)的規(guī)則开睡,來做熔斷降級(jí);
  • SystemSlot 則通過系統(tǒng)的狀態(tài)苟耻,例如 load1 等篇恒,來控制總的入口流量

注意:這里的插槽鏈都是一一對(duì)應(yīng)資源名稱的

上面的所介紹的插槽(slot chain)是Sentinel非常重要的概念。同時(shí)還有一個(gè)非常重要的概念那就是Node凶杖,為了幫助理解胁艰,盡我所能畫了下面這張圖,可以看到整個(gè)結(jié)構(gòu)非常的像一棵樹:

image.png

簡(jiǎn)單解釋下上圖:

  • 頂部藍(lán)色的node節(jié)點(diǎn)為根節(jié)點(diǎn)智蝠,全局唯一
  • 下面黃色的節(jié)點(diǎn)為入口節(jié)點(diǎn)腾么,每個(gè)CentextName(上下文名稱)一一對(duì)應(yīng)一個(gè)
    • 可以有多個(gè)子節(jié)點(diǎn)(對(duì)應(yīng)多種資源)
  • 中間綠色框框中的節(jié)點(diǎn)都是屬于同一個(gè)資源的(相同的ResourceName)
  • 最底下紫色的節(jié)點(diǎn)是集群節(jié)點(diǎn),可以理解成綠色框框中Node資源的整合
  • 最右邊的指的是不同的來源(origin)流量杈湾,同一個(gè)EntranceNode可以有多個(gè)來源

以上2個(gè)概念務(wù)必要理清楚解虱,之后再一步一步看源碼會(huì)比較清晰

下面我們將從入口源碼開始一步一步分析整個(gè)調(diào)用過程:

源碼分析

下面的是一個(gè)Sentinel使用的示例代碼,我們就從這里切入開始分析

// 創(chuàng)建一個(gè)名稱為entrance1漆撞,來源為appA 的上下文Context
ContextUtil.enter("entrance1", "appA");
// 創(chuàng)建一個(gè)資源名稱nodeA的Entry
 Entry nodeA = SphU.entry("nodeA");
 if (nodeA != null) {
    nodeA.exit();
 }
 // 清除上下文
 ContextUtil.exit();

ContextUtil.enter("entrance1", "appA")

public static Context enter(String name, String origin) {
      // 判斷上下文名稱是否為默認(rèn)的名稱(sentinel_default_context) 是的話直接拋出異常
    if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
        throw new ContextNameDefineException(
            "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
    }
    return trueEnter(name, origin);
}

protected static Context trueEnter(String name, String origin) {
      // 先從ThreadLocal中嘗試獲取殴泰,獲取到則直接返回
    Context context = contextHolder.get();
    if (context == null) {
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // 嘗試從緩存中獲取該上下文名稱對(duì)應(yīng)的 入口節(jié)點(diǎn)
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
             // 判斷緩存中入口節(jié)點(diǎn)數(shù)量是否大于2000
            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                setNullContext();
                return NULL_CONTEXT;
            } else {
                try {
                        // 加鎖
                    LOCK.lock();
                    // 雙重檢查鎖
                    node = contextNameNodeMap.get(name);
                    if (node == null) {
                         // 判斷緩存中入口節(jié)點(diǎn)數(shù)量是否大于2000
                        if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                            setNullContext();
                            return NULL_CONTEXT;
                        } else {
                            // 根據(jù)上下文名稱生成入口節(jié)點(diǎn)(entranceNode)
                            node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                            // 加入至全局根節(jié)點(diǎn)下
                            Constants.ROOT.addChild(node);
                            // 加入緩存中
                            Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                            newMap.putAll(contextNameNodeMap);
                            newMap.put(name, node);
                            contextNameNodeMap = newMap;
                        }
                    }
                } finally {
                    LOCK.unlock();
                }
            }
        }
        // 初始化上下文對(duì)象
        context = new Context(node, name);
        // 設(shè)置當(dāng)前線程流量來源 origin
        context.setOrigin(origin);
        // 設(shè)置到當(dāng)前線程中
        contextHolder.set(context);
    }

    return context;
}

主要做了2件事情

  1. 根據(jù)ContextName生成entranceNode,并加入緩存叫挟,每個(gè)ContextName對(duì)應(yīng)一個(gè)入口節(jié)點(diǎn)entranceNode
  2. 根據(jù)ContextNameentranceNode初始化上下文對(duì)象艰匙,并將上下文對(duì)象設(shè)置到當(dāng)前線程中

這里有幾點(diǎn)需要注意:

  1. 入口節(jié)點(diǎn)數(shù)量不能大于2000限煞,大于會(huì)直接拋異常
  2. 每個(gè)ContextName對(duì)應(yīng)一個(gè)入口節(jié)點(diǎn)entranceNode
  3. 每個(gè)entranceNode都有共同的父節(jié)點(diǎn)抹恳。也就是根節(jié)點(diǎn)

Entry nodeA = SphU.entry("nodeA")

// SphU.class
public static Entry entry(String name) throws BlockException {
    // 默認(rèn)為 出口流量類型,單位統(tǒng)計(jì)數(shù)為1
    return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}

// CtSph.class
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    // 生成資源對(duì)象
    StringResourceWrapper resource = new StringResourceWrapper(name, type);
    return entry(resource, count, args);
}
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
    return entryWithPriority(resourceWrapper, count, false, args);
}

上面的代碼比較簡(jiǎn)單署驻,不指定EntryType的話奋献,則默認(rèn)為出口流量類型健霹,最終會(huì)調(diào)用entryWithPriority方法,主要業(yè)務(wù)邏輯也都在這個(gè)方法中

  • entryWithPriority方法
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 獲取當(dāng)前線程上下文對(duì)象
    Context context = ContextUtil.getContext();
    // 上下文名稱對(duì)應(yīng)的入口節(jié)點(diǎn)是否已經(jīng)超過閾值2000瓶蚂,超過則會(huì)返回空 CtEntry
    if (context instanceof NullContext) {
        return new CtEntry(resourceWrapper, null, context);
    }

    if (context == null) {
        // 如果沒有指定上下文名稱糖埋,則使用默認(rèn)名稱,也就是默認(rèn)入口節(jié)點(diǎn)
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }

    // 全局開關(guān)
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }
    // 生成插槽鏈
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

    /*
     * 表示資源(插槽鏈)超過6000窃这,因此不會(huì)進(jìn)行規(guī)則檢查瞳别。
     */
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }
    // 生成 Entry 對(duì)象
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        // 開始執(zhí)行插槽鏈 調(diào)用邏輯
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        // 清除上下文
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        // 除非Sentinel內(nèi)部存在錯(cuò)誤,否則不應(yīng)發(fā)生這種情況杭攻。
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

這個(gè)方法可以說是涵蓋了整個(gè)Sentinel的核心邏輯

  1. 獲取上下文對(duì)象祟敛,如果上下文對(duì)象還未初始化,則使用默認(rèn)名稱初始化兆解。初始化邏輯在上文已經(jīng)分析過
  2. 判斷全局開關(guān)
  3. 根據(jù)給定的資源生成插槽鏈馆铁,插槽鏈?zhǔn)歉Y源相關(guān)的,Sentinel最關(guān)鍵的邏輯也都在各個(gè)插槽中锅睛。初始化的邏輯在lookProcessChain(resourceWrapper);中埠巨,下文會(huì)分析
  4. 依順序執(zhí)行每個(gè)插槽邏輯

lookProcessChain(resourceWrapper)方法

lookProcessChain方法為指定資源生成插槽鏈,下面我們來看下它的初始化邏輯

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    // 根據(jù)資源嘗試從全局緩存中獲取
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    if (chain == null) {
        // 非常常見的雙重檢查鎖
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // 判斷資源數(shù)是否大于6000
                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;
}
  1. 根據(jù)資源嘗試從全局緩存中獲取插槽鏈现拒。每個(gè)資源對(duì)應(yīng)一個(gè)插槽鏈(資源嘴多只能定義6000個(gè))
  2. 初始化插槽鏈上的插槽(SlotChainProvider.newSlotChain()方法中)

下面我們看下初始化插槽鏈上的插槽的邏輯

SlotChainProvider.newSlotChain()

public static ProcessorSlotChain newSlotChain() {
    // 判斷是否已經(jīng)初始化過
    if (builder != null) {
        return builder.build();
    }
      // 加載 SlotChain 
    resolveSlotChainBuilder();
    // 加載失敗則使用默認(rèn) 插槽鏈 
    if (builder == null) {
        RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
        builder = new DefaultSlotChainBuilder();
    }
    // 構(gòu)建完成
    return builder.build();
}

/**
 * java自帶 SPI機(jī)制 加載 slotChain
 */
private static void resolveSlotChainBuilder() {
    List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
    boolean hasOther = false;
    // 嘗試獲取自定義SlotChainBuilder辣垒,通過JAVA SPI機(jī)制擴(kuò)展
    for (SlotChainBuilder builder : LOADER) {
        if (builder.getClass() != DefaultSlotChainBuilder.class) {
            hasOther = true;
            list.add(builder);
        }
    }
    if (hasOther) {
        builder = list.get(0);
    } else {
        // 未獲取到自定義 SlotChainBuilder 則使用默認(rèn)的
        builder = new DefaultSlotChainBuilder();
    }

    RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
        + builder.getClass().getCanonicalName());
}
  1. 首先會(huì)嘗試獲取自定義的SlotChainBuilder來構(gòu)建插槽鏈,自定義的SlotChainBuilder可以通過JAVA SPI機(jī)制來擴(kuò)展
  2. 如果未配置自定義的SlotChainBuilder印蔬,則會(huì)使用默認(rèn)的DefaultSlotChainBuilder來構(gòu)建插槽鏈乍构,DefaultSlotChainBuilder所構(gòu)建的插槽就是文章開頭我們提到的7種Slot。每個(gè)插槽都有其對(duì)應(yīng)的職責(zé)扛点,各司其職哥遮,后面我們會(huì)詳細(xì)分析這幾個(gè)插槽的源碼,及所承擔(dān)的職責(zé)陵究。

總結(jié)

文章開頭的提到的兩個(gè)點(diǎn)(插槽鏈和Node)眠饮,這是Sentinel的重點(diǎn),理解這兩點(diǎn)對(duì)于閱讀源碼來說事半功倍

Sentinel系列

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末铜邮,一起剝皮案震驚了整個(gè)濱河市仪召,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌松蒜,老刑警劉巖扔茅,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異秸苗,居然都是意外死亡召娜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門惊楼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玖瘸,“玉大人秸讹,你說我怎么就攤上這事⊙诺梗” “怎么了璃诀?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蔑匣。 經(jīng)常有香客問我劣欢,道長(zhǎng),這世上最難降的妖魔是什么裁良? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任氧秘,我火速辦了婚禮,結(jié)果婚禮上趴久,老公的妹妹穿的比我還像新娘丸相。我一直安慰自己,他們只是感情好彼棍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布灭忠。 她就那樣靜靜地躺著,像睡著了一般座硕。 火紅的嫁衣襯著肌膚如雪弛作。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天华匾,我揣著相機(jī)與錄音映琳,去河邊找鬼。 笑死蜘拉,一個(gè)胖子當(dāng)著我的面吹牛萨西,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播旭旭,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼谎脯,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了持寄?” 一聲冷哼從身側(cè)響起源梭,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稍味,沒想到半個(gè)月后废麻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡模庐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年烛愧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡屑彻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出顶吮,到底是詐尸還是另有隱情社牲,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布悴了,位于F島的核電站搏恤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏湃交。R本人自食惡果不足惜熟空,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搞莺。 院中可真熱鬧息罗,春花似錦、人聲如沸才沧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽温圆。三九已至挨摸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間岁歉,已是汗流浹背得运。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锅移,地道東北人熔掺。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像非剃,于是被迫代替她去往敵國(guó)和親瞬女。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容