阿里Sentinel元件之開篇
1. 介紹
當前許多專案承載著大量業務功能,在單一工程中進行開發會造成程式碼量劇增,為專案的開發、部署、運維以及擴充套件造成障礙。在此背景下,微服務的出現有利於解決上述出現的問題,然而服務的穩定性又造成巨大的困擾。
本章介紹的阿里 Sentinel 元件以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。該元件的核心功能不依賴任何外部專案,同時官方稱引入 Sentinel 帶來的效能損耗非常小,只有在業務單機量級超過 25W QPS 的時候才會有一些顯著的影響(10% 左右),單機 QPS 不太大的時候損耗幾乎可以忽略不計。
Sentinel 具有以下特徵:
-
豐富的應用場景
- 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制檯中看到接入應用的單臺機器秒級資料,甚至 500 臺以下規模的叢集的彙總執行情況。
- 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模組,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
- 完善的 SPI 擴充套件點:Sentinel 提供簡單易用、完善的 SPI 擴充套件介面。您可以通過實現擴充套件介面來快速地定製邏輯。例如定製規則管理、適配動態資料來源等。
更多專案的介紹請移步官方檔案。
2. 基礎
Sentinel元件以資源為單位,進行資料統計,為控制提供基礎資料。每個資源都有一個資源名稱,在一次呼叫中會涉及到以下概念:
- slot:功能插槽。每個slot負責不同的職責,比如統計slot負責當前系統的資料統計,限流slot負責檢查限流規則是否成立。
- slot chain:功能插槽鏈。每一個資源都會涉及到不同的檢查規則,所以在Sentinel中每個資源都擁有單獨的slot chain,保證資源和slot chain一一對應。在請求資源X時,只需要執行資源X對應的slot chain即可。
- Node:統計節點,包含資源的全域性資料統計,單次鏈路呼叫的統計等多個節點。
- Entry:每一次資源的呼叫都會建立一個Entry,它儲存了本次的呼叫資訊,如建立時間、當前的統計節點等。一個Entry負責一次slot chain的執行。
- Context:呼叫鏈路上下文,通過ThreadLocal維持。在一次處理中,可能涉及到對一個資源的多次請求(可以理解為一個執行緒中請求獲取兩次Entry),例如使用者獲取文章的資訊,會涉及到獲取文章的閱讀量和評論數,這些資料都儲存在另外一個服務S中,那麼可能就會呼叫兩次資源S,所以建立兩個Entry,但是隻維護一個Context。
3. 執行流程
在專案中使用Sentinel,最基礎的使用方式如下:
public static void main(String[] args) {
// 不斷進行資源呼叫.
while (true) {
Entry entry = null;
try {
entry = SphU.entry("HelloWorld");
// 資源中的邏輯.
System.out.println("hello world");
} catch (BlockException e1) {
System.out.println("blocked!");
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
複製程式碼
我們通過跟蹤 entry = SphU.entry("HelloWorld"); 語句的執行,來看看Sentinel的執行流程。在此流程中,關鍵的程式碼是CtSph獲取Entry的方法。
private Entry entryWithPriority(ResourceWrapper resourceWrapper,int count,boolean prioritized,Object... args)
throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,// so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper,null,context);
}
if (context == null) {
// Using default context.
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// Global switch is close,no rule checking will do.
if (!Constants.ON) {
return new CtEntry(resourceWrapper,context);
}
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.
*/
if (chain == null) {
return new CtEntry(resourceWrapper,context);
}
Entry e = new CtEntry(resourceWrapper,chain,context);
try {
chain.entry(context,resourceWrapper,count,prioritized,args);
} catch (BlockException e1) {
e.exit(count,args);
throw e1;
} catch (Throwable e1) {
// This should not happen,unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception",e1);
}
return e;
}
複製程式碼
3.1. 規則開關
通過設定 Constants.ON 的值,可以控制是否進行規則檢查。
if (!Constants.ON) {
return new CtEntry(resourceWrapper,context);
}
複製程式碼
3.2. Context
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,// so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper,context);
}
if (context == null) {
// Using default context.
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
複製程式碼
該部分獲取上面提到的Conext物件,首先關注ContextUtil類:
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
public static Context getContext() {
return contextHolder.get();
}
複製程式碼
可以看見Sentinel為每一個執行緒例項化了一個Context物件,通過ThreadLocal進行儲存,通常一次使用者請求在一個執行緒中完成,所以保證了一個執行緒擁有一份唯一的上下文。 一般線上程第一次獲取上下文時,此方法返回為null,此時需要例項化,例項化方法如下:
protected static Context trueEnter(String name,String origin) {
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);
// Add entrance node.
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;
}
複製程式碼
由程式碼可知,Sentinel會建立一個 Context 放入到 contextHolder 中,下面關注下 contextNameNodeMap。該變數的申明如下:
private static volatile Map<String,DefaultNode> contextNameNodeMap = new HashMap<>();
複製程式碼
此變數以Context的Name為key,儲存了 EntranceNode 物件。在元件中,資源的呼叫路徑以樹狀結構儲存起來,用於根據呼叫路徑進行流量控制。在預設情況下,一次呼叫會形成以下結構:
machine-root
/
/
EntranceNode(sentinel_default_context)
/
/
DefaultNode(nodeA)
複製程式碼
上面 EntranceNode 是由上述程式碼生成的。注意在生成 EntranceNode 時,如果超過 MAX_CONTEXT_NAME_SIZE,就會返回 NullContext 型別的context。如果在一次呼叫中使用以下方法生成不同的ContextName,那麼就會形成下述結構。
ContextUtil.enter("entrance1","appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
ContextUtil.enter("entrance2","appA");
nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
複製程式碼
machine-root
/ \
/ \
EntranceNode1 EntranceNode2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
複製程式碼
詳細介紹見官方檔案
3.3. slot chain
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
複製程式碼
上述程式碼用於獲取此次執行的slot chain,即一系列資料統計和規則檢查的slot。 通過 lookProcessChain 方法,我們可以獲取以下資訊:
- 上面提到每一個資源都有一份slot chain,該資訊以資源的名稱為key,儲存於 Ctsph::chainMap中。
- 一個專案最多隻能存在 Constants.MAX_SLOT_CHAIN_SIZE(預設6000)個資源,超過後不再生效。
- 通過 SlotChainProvider::newSlotChain方法獲取slot chain,預設情況下會使用 **DefaultSlotChainBuilder ** 構造slot chain,可以通過SPI進行單獨的配置(SPI,即Service Provider Interface,具體詳情可以自行搜尋,後續文章會進行簡單介紹)。
在獲取slot chain後,構造出Entry物件,然後通過以下方法呼叫slot chain,依次執行每個slot,基本流程結束。
chain.entry(context,args);
複製程式碼
在 DefaultSlotChainBuilder 中,通過以下方法返回slot chain,其中的每個slot將會在以後的文章中詳細介紹。
4. 後記
本文簡單介紹了Sentinel,並講述了基本的執行流程。在此基礎上,後面會跟隨原始碼學習Sentinel的不同slot。