引言
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)非常的像一棵樹:
簡(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件事情
- 根據(jù)
ContextName
生成entranceNode
,并加入緩存叫挟,每個(gè)ContextName
對(duì)應(yīng)一個(gè)入口節(jié)點(diǎn)entranceNode
- 根據(jù)
ContextName
和entranceNode
初始化上下文對(duì)象艰匙,并將上下文對(duì)象設(shè)置到當(dāng)前線程中
這里有幾點(diǎn)需要注意:
- 入口節(jié)點(diǎn)數(shù)量不能大于2000限煞,大于會(huì)直接拋異常
- 每個(gè)
ContextName
對(duì)應(yīng)一個(gè)入口節(jié)點(diǎn)entranceNode
- 每個(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的核心邏輯
- 獲取上下文對(duì)象祟敛,如果上下文對(duì)象還未初始化,則使用默認(rèn)名稱初始化兆解。初始化邏輯在上文已經(jīng)分析過
- 判斷全局開關(guān)
- 根據(jù)給定的資源生成插槽鏈馆铁,插槽鏈?zhǔn)歉Y源相關(guān)的,Sentinel最關(guān)鍵的邏輯也都在各個(gè)插槽中锅睛。初始化的邏輯在
lookProcessChain(resourceWrapper);
中埠巨,下文會(huì)分析 - 依順序執(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;
}
- 根據(jù)資源嘗試從全局緩存中獲取插槽鏈现拒。每個(gè)資源對(duì)應(yīng)一個(gè)插槽鏈(資源嘴多只能定義6000個(gè))
- 初始化插槽鏈上的插槽(
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());
}
- 首先會(huì)嘗試獲取自定義的
SlotChainBuilder
來構(gòu)建插槽鏈,自定義的SlotChainBuilder
可以通過JAVA SPI機(jī)制來擴(kuò)展 - 如果未配置自定義的
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系列