Sentinel 的核心骨架,将不同的 Slot 按照順序串在一起(責任鍊模式),進而将不同的功能(限流、降級、系統保護)組合在一起。slot chain 其實可以分為兩部分:統計資料建構部分(statistic)和判斷部分(rule checking)。核心結構:
業務埋點示例
// 資源的唯一辨別
String resourceName = "testSentinel";
Entry entry = null;
String retVal;
try {
entry = SphU.entry(resourceName, EntryType.IN);
// TODO 業務邏輯
retVal = "passed";
} catch (BlockException e) {
// TODO 降級邏輯
retVal = "blocked";
} catch (Exception e) {
// 異常數統計埋點
Tracer.trace(e);
throw new RuntimeException(e);
} finally {
if (entry != null) {
entry.exit();
}
}
這段代碼是Sentinel業務埋點示例,通過示例我們可以看出Sentinel對資源的控制入口是
SphU.entry(resourceName, EntryType.IN);
,源碼如下:
public static Entry entry(String name, EntryType type) throws BlockException {
return Env.sph.entry(name, type, 1, OBJECTS0);
}
這裡第一個參數是受保護資源的唯一名稱;第二個參數表示流量類型:
-
:是指進入我們系統的入口流量,比如 http 請求或者是其他的 rpc 之類的請求,設定為IN主要是為了保護自己系統。EntryType.IN
-
:是指我們系統調用其他第三方服務的出口流量,設定為OUT是為了保護第三方系統。EntryType.OUT
這段代碼沒什麼邏輯,隻是轉發了下,跟進源碼可以發現最終邏輯實在
CtSph#entryWithPriority(ResourceWrapper, int, boolean, Object...)
方法中。
Sentinel 骨架代碼
Sentinel的核心是資源,這裡的資源可以是任何東西,服務,服務裡的方法,甚至是一段代碼。而
SphU.entry(resourceName);
這段代碼的主要作用是 :
- 定義一個Sentinel資源
- 檢驗資源所對應的規則是否生效
核心代碼如下:
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
// 擷取目前線程上下文,Context是通過ThreadLocal維護,每一個Context都會有一個EntranceNode執行個體,它是dashboard【簇點鍊路】中的根節點,主要是用來區分調用鍊路的
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// 如果是 NullContext,表示 Context 個數超過了門檻值,這個時候 Sentinel 不會應用規則,即不會觸發限流降級等規則,也不會觸發QPS等資料統計。
// 門檻值大小 =Constants.MAX_CONTEXT_NAME_SIZE = 2000,具體可以檢視 ContextUtil#trueEnter。
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
// 如果沒有設定上下文,即使用預設上下文,預設上下文的名稱是 sentinel_default_context
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
if (!Constants.ON) {
// Sentinel 的全局控制開關,一旦關閉則不進行任何檢查
return new CtEntry(resourceWrapper, null, context);
}
// 通過Sentinel的官方文檔我們可以知道,Sentinel的核心功能是基于一系列的功能插槽來實作的,而組織這些功能插槽使用的是責任鍊模式。
// 這裡是通過資源(每個資源是唯一的),擷取第一個功能插,即該資源對應的規則入口。
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
/*
* Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
* so no rule checking will be done.
*/
// 如果一個服務中,資源數量操過門檻值(最大的插槽鍊),則傳回null,即不會再應用規則,直接傳回。
// 門檻值大小 = Constants.MAX_SLOT_CHAIN_SIZE = 6000
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
// 建構Sentinel調用鍊入口
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 開始執行插槽鍊,如果某個插槽比對上了某個規則,如限流規則,就會抛出BlockException異常,這時表示請求被拒絕了。
// 業務層面會去捕獲這個異常,然後做熔斷,降級操作。
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// Sentinel内部異常
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
核心邏輯如下:
- 通過目前線程的上下文,擷取到目前線程的【簇點鍊路】入口。
- 判斷全局開關是否關閉。
- 通過唯一的資源辨別擷取到對應的功能插槽鍊(ProcessorSlot)的第一個插槽。
- 建構Sentinel調用鍊入口,并執行調用鍊
- 如果抛出BlockException表示觸發了資源限制規則,需要進行熔斷降級。
這裡有兩個需要注意的地方:
- 【簇點鍊路】入口Context的數量是有限制的,最大2000個,通常情況下,我們都不需要顯示設定 context,使用預設的就好了,這樣Context數量限制基本上不會觸發。
-
,這裡的資源的唯一辨別
SphU.entry(resourceName, EntryType.IN)
也是有限制的,最大是6000。當Sentinel與 Servlet 的整合後,
resourceName
會将所有的對外接口定義成Sentinel的資源,資源名稱就是接口位址,是以要控制好服務接口數量。
CommonFilter
ContextUtil#enter
ContextUtil#enter(String name, String origin)
的主要作用就是建立目前線程的上下文Context,每個上下文會對應一個EntranceNode(入口節點)執行個體,通常情況下我們不需要顯示調用該方法。
-
:上下文的唯一辨別,也是入口節點的資源名稱。name
-
:表示來源,通常是服務消費者或調用者的應用名稱,當我們需要對不同來源的消費者或調用者進行限制時就會用到這個參數。orgin
源碼如下:
public static Context enter(String name, String origin) {
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;
// 根據上下文名稱擷取入口節點
DefaultNode node = localCacheNameMap.get(name);
// 入口節點節點也為空需要新建立入口節點
if (node == null) {
// 判斷是否超過最大長度限制(樂觀鎖機制)
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
try {
LOCK.lock();
// 雙重判斷
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
// 建立入口節點
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 将入口節點添加到全局根節點下(machine-root)
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();
}
}
}
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
如果我們再代碼中顯示調用這個方法:
ContextUtil.enter("context1", "service-1");
...
ContextUtil.exit();
ContextUtil.enter("context2", "service-1");
...
ContextUtil.exit();
那麼會建立如下一個樹結構圖:
這裡有兩點需要注意:
- 也就是上面說的數量限制,2000。
- ContextUtil是通過ThreadLocal來維護目前線程的上下文的,是以當遇到異步線程時需要手動調用
方法來完成父線程和子線程的上下文切換。
ContextUtil.runOnContext(context, f)
文檔中的Demo:
public void someAsync() {
try {
AsyncEntry entry = SphU.asyncEntry(resourceName);
// Asynchronous invocation.
doAsync(userId, result -> {
// 在異步回調中進行上下文變換,通過 AsyncEntry 的 getAsyncContext 方法擷取異步 Context
ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
try {
// 此處嵌套正常的資源調用.
handleResult(result);
} finally {
entry.exit();
}
});
});
} catch (BlockException ex) {
// Request blocked.
// Handle the exception (e.g. retry or fallback).
}
}
lookProcessChain
Sentinel的核心功能是使用的是責任鍊模式實作,
lookProcessChain(resourceWrapper)
的主要作用就是用來構造責任鍊,源碼如下:
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 根據資源的唯一辨別來做本地緩存
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 限制資源資對應調用鍊的總數,一個資源對應一條調用鍊
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;
}
進一步跟進方法會發現,責任鍊是由
SlotChainBuilder#build()````去建構的,預設實作類是
DefaultSlotChainBuilder```,源碼如下:
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// 找到ProcessorSlot所有的實作類,并排序
List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
// 将功能槽放到責任鍊最後
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
}
老版本直接是寫死方式:
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
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 AuthoritySlot());
chain.addLast(new SystemSlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
-
: 負責收集資源的路徑,并将這些資源的調用路徑,以樹狀結構存儲起來,用于根據調用路徑來限流降級;NodeSelectorSlot
-
: 則用于存儲資源的統計資訊以及調用者資訊,例如該資源的 RT, QPS, thread count 等等,這些資訊将用作為多元度限流,降級的依據;ClusterBuilderSlot
-
: 則用于記錄、統計不同緯度的 runtime 名額監控資訊;StatisticSlot
-
: 則用于根據預設的限流規則以及前面 slot 統計的狀态,來進行流量控制;FlowSlot
-
: 則根據配置的黑白名單和調用來源資訊,來做黑白名單控制;AuthoritySlot
-
: 則通過統計資訊以及預設的規則,來做熔斷降級;DegradeSlot
-
: 則通過系統的狀态,例如 load1 等,來控制總的入口流量;SystemSlot
總結
- Sentinel 通過責任鍊模式,将各功能塊隔離,即清晰劃分出了各功能塊的職責邊界,也非常友善擴充。新增功能直接新增功能插槽就行了,不需要改以前代碼。
- Sentinel 的本地緩存使用的是
,通過加鎖和寫複制的思想來解決HashMap
的線程安全性問題,在讀遠大于寫的場景這種方式非常值得借鑒。HashMap