前言
本文基于sentinel-1.8.0版本
Sentinel 是面向分布式服務(wù)架構(gòu)的流量控制組件,主要以流量為切入點(diǎn),從限流、流量整形、熔斷降級(jí)、系統(tǒng)負(fù)載保護(hù)、熱點(diǎn)防護(hù)等多個(gè)維度來(lái)幫助開(kāi)發(fā)者保障微服務(wù)的穩(wěn)定性。
sentinel整體設(shè)計(jì)的很精巧,只需要一個(gè)sentinel-core便可以運(yùn)行,它提供了諸如服務(wù)降級(jí)、黑白名單校驗(yàn)、QPS、線程數(shù)、系統(tǒng)負(fù)載、CPU負(fù)載、流控等功能,可謂是功能非常的強(qiáng)大。
大家都知道sentinel使用SphU或者SphO標(biāo)示一個(gè)被保護(hù)的資源,比如:
Entry entry = SphU.entry(“HelloWorld”, EntryType.IN);
上述代碼標(biāo)示了一個(gè)名為HelloWorld的被保護(hù)資源,并且檢查入口流量(SystemSlot只對(duì)入口流量生效)。在這行代碼之后,便可以訪問(wèn)被保護(hù)的資源了,那么SphU.entry()內(nèi)部究竟做了什么?訪問(wèn)資源結(jié)束后,還要執(zhí)行entry.exit(),那么entry.exit()又做了什么?本文接下來(lái)詳細(xì)分析SphU.entry()和entry.exit()方法的執(zhí)行原理。
1、基本原理
sentinel在內(nèi)部創(chuàng)建了一個(gè)責(zé)任鏈,責(zé)任鏈?zhǔn)怯梢幌盗蠵rocessorSlot對(duì)象組成的,每個(gè)ProcessorSlot對(duì)象負(fù)責(zé)不同的功能,外部請(qǐng)求是否允許訪問(wèn)資源,需要通過(guò)責(zé)任鏈的校驗(yàn),只有校驗(yàn)通過(guò)的,才可以訪問(wèn)資源,如果被校驗(yàn)失敗,會(huì)拋出BlockException異常。
sentinel提供了8個(gè)ProcessorSlot的實(shí)現(xiàn)類(lèi),下面實(shí)現(xiàn)類(lèi)功能介紹:
- DegradeSlot:用于服務(wù)降級(jí),如果發(fā)現(xiàn)服務(wù)超時(shí)次數(shù)或者報(bào)錯(cuò)次數(shù)超過(guò)限制,DegradeSlot將禁止再次訪問(wèn)服務(wù),等待一段時(shí)間后,DegradeSlot試探性的放過(guò)一個(gè)請(qǐng)求,然后根據(jù)該請(qǐng)求的處理情況,決定是否再次降級(jí)。
- AuthoritySlot:黑白名單校驗(yàn),按照字符串匹配,如果在黑名單,則禁止訪問(wèn)。
- ClusterBuilderSlot:構(gòu)建ClusterNode對(duì)象,該對(duì)象用于統(tǒng)計(jì)訪問(wèn)資源的QPS、線程數(shù)、異常、響應(yīng)時(shí)間等,每個(gè)資源對(duì)應(yīng)一個(gè)ClusterNode對(duì)象。
- SystemSlot:校驗(yàn)QPS、并發(fā)線程數(shù)、系統(tǒng)負(fù)載、CPU使用率、平均響應(yīng)時(shí)間是否超過(guò)限制,使用滑動(dòng)窗口算法統(tǒng)計(jì)上述這些數(shù)據(jù)。
- StatisticSlot:用于從多個(gè)維度(入口流量、調(diào)用者、當(dāng)前被訪問(wèn)資源)統(tǒng)計(jì)響應(yīng)時(shí)間、并發(fā)線程數(shù)、處理失敗個(gè)數(shù)、處理成功個(gè)數(shù)等。
- FlowSlot:用于流控,可以根據(jù)QPS或者每秒并發(fā)線程數(shù)控制,當(dāng)QPS或者并發(fā)線程數(shù)超過(guò)設(shè)定值,便會(huì)拋出FlowException異常。FlowSlot依賴(lài)于StatisticSlot的統(tǒng)計(jì)數(shù)據(jù)。
- NodeSelectorSlot:負(fù)責(zé)收集資源路徑,并將這些資源的調(diào)用路徑,以樹(shù)狀結(jié)構(gòu)存儲(chǔ)起來(lái),用于根據(jù)調(diào)用路徑來(lái)限流降級(jí)、數(shù)據(jù)統(tǒng)計(jì)。
- LogSlot:打印日志。
比如本文開(kāi)頭的例子,當(dāng)請(qǐng)求要訪問(wèn)HelloWorld資源時(shí),該請(qǐng)求需要順次經(jīng)過(guò)上述這些slot的檢查,同時(shí)當(dāng)訪問(wèn)結(jié)束時(shí)StatisticSlot里面也記錄下HelloWorld資源被訪問(wèn)的統(tǒng)計(jì)數(shù)據(jù),當(dāng)后面的請(qǐng)求再次訪問(wèn)該資源時(shí),F(xiàn)lowSlot、DegradeSlot可以使用這些統(tǒng)計(jì)數(shù)據(jù)做檢查。
sentinel使用SPI加載這些slot,并根據(jù)注解@SpiOrder的屬性value對(duì)它們排序,value越小優(yōu)先級(jí)越高。在sentinel中,這些slot的順序是:
我們也可以添加自定義的slot,只需要實(shí)現(xiàn)ProcessorSlot接口,在com.alibaba.csp.sentinel.slotchain.ProcessorSlot文件中添加自定義類(lèi)的全限定名,然后使用注解@SpiOrder指定順序即可。
對(duì)于每個(gè)slot的實(shí)現(xiàn)原理在后面的文章做介紹。下面通過(guò)代碼介紹一下SphU.entry()和entry.exit()內(nèi)部都做了什么。
2、SphU.entry()
在介紹代碼前先介紹兩個(gè)對(duì)象。
2.1、StringResourceWrapper
entry()方法內(nèi)部首先創(chuàng)建一個(gè)StringResourceWrapper對(duì)象,該對(duì)象表示被保護(hù)的資源,資源使用字符串命名,StringResourceWrapper對(duì)象有三個(gè)參數(shù):
//資源名,也就是entry()方法的第一個(gè)入?yún)?/span>
protected final String name;
//表示是入口流量(IN)還是出口流量(OUT),
//兩個(gè)參數(shù)的區(qū)別在于是否被SystemSlot檢查,IN會(huì)被檢查,OUT不會(huì),默認(rèn)是OUT
protected final EntryType entryType;
//表示資源類(lèi)型,sentinel提供了common、web、sql、api等類(lèi)型,資源類(lèi)型用于統(tǒng)計(jì)使用
protected final int resourceType;
任何一個(gè)被保護(hù)的資源都被封裝成StringResourceWrapper對(duì)象,sentinel也是使用該對(duì)象識(shí)別被保護(hù)資源。
2.2、Entry
有了表示資源的對(duì)象后,接下來(lái)創(chuàng)建Entry對(duì)象,這個(gè)對(duì)象也是SphU.entry()方法的返回值,Entry對(duì)象持有資源對(duì)象,ProcessorSlot鏈,sentinel上下文對(duì)象Context,通過(guò)Entry對(duì)象應(yīng)用程序可以窺探sentinel內(nèi)部情況。
SphU.entry()通過(guò)一系列的調(diào)用最終調(diào)用到CtSph的entryWithPriority()方法上:
//resourceWrapper:是StringResourceWrapper對(duì)象,表示資源
//count:表示令牌數(shù),默認(rèn)是1,一般一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)令牌,也可以指定一個(gè)請(qǐng)求對(duì)應(yīng)多個(gè)令牌,如果令牌不夠,則禁止訪問(wèn)
//prioritized:在FlowSlot里面使用,沒(méi)找到具體的使用含義,有看懂的小伙伴可以告知一下
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
//構(gòu)建上下文對(duì)象,上下文對(duì)象存儲(chǔ)在ThreadLocal中
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
//一般的線程第一次訪問(wèn)資源,context都是null,我們也可以在應(yīng)用程序中使用ContextUtil自己創(chuàng)建Context對(duì)象
if (context == null) {
//下面創(chuàng)建了一個(gè)名字為sentinel_default_context的Context對(duì)象
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
//全局開(kāi)關(guān),可以使用它來(lái)關(guān)閉sentinel
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
//使用SPI構(gòu)建slot鏈,每個(gè)slot對(duì)象都有一個(gè)next屬性,可以使用該屬性指定下一個(gè)slot對(duì)象
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
//創(chuàng)建Entry對(duì)象
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
//對(duì)該請(qǐng)求,遍歷每個(gè)slot對(duì)象
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
entryWithPriority()方法首先創(chuàng)建一個(gè)Context對(duì)象,這個(gè)對(duì)象將會(huì)貫穿整個(gè)請(qǐng)求的過(guò)程,一些共享數(shù)據(jù)可以放在這里面,既可以使用上面的代碼創(chuàng)建名字為sentinel_default_context的Context對(duì)象,也可以在應(yīng)用程序中創(chuàng)建Context對(duì)象,如果在應(yīng)用程序中創(chuàng)建的話,上面代碼就不會(huì)再次創(chuàng)建了:
//第一個(gè)參數(shù)表示Context名字,
//第二個(gè)參數(shù)表示請(qǐng)求方或者調(diào)用方的名字,當(dāng)需要根據(jù)調(diào)用方進(jìn)行控制的時(shí)候,第二個(gè)參數(shù)就會(huì)起作用
ContextUtil.enter("HelloWorld", "app");
Entry entry = SphU.entry("HelloWorld", EntryType.IN);
創(chuàng)建完Context對(duì)象后,使用SPI構(gòu)建slot鏈,之后是創(chuàng)建Entry對(duì)象,之后就是遍歷slot鏈以決定是否允許該請(qǐng)求訪問(wèn)資源。
3、entry.exit()
訪問(wèn)完資源后,需要調(diào)用entry.exit()以告知sentinel結(jié)束訪問(wèn),sentinel會(huì)做一些資源的清理和數(shù)據(jù)統(tǒng)計(jì)工作。
entry.exit()方法最后調(diào)用到CtEntry.exitForContext()方法上:
protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException {
if (context != null) {
if (context instanceof NullContext) {
return;
}
//如果Context對(duì)象記錄的Entry對(duì)象不是當(dāng)前對(duì)象,
//意味著entry.exit()與SphU.entry()不是成對(duì)出現(xiàn)的,
//sentinel要求兩者必須成對(duì)出現(xiàn),而且要一一對(duì)應(yīng),否則拋出異常
//Context有父子關(guān)系,這個(gè)在文章后面介紹
if (context.getCurEntry() != this) {
String curEntryNameInContext = context.getCurEntry() == null ? null
: context.getCurEntry().getResourceWrapper().getName();
// Clean previous call stack.
CtEntry e = (CtEntry) context.getCurEntry();
while (e != null) {
e.exit(count, args);
e = (CtEntry) e.parent;
}
String errorMessage = String.format("The order of entry exit can't be paired with the order of entry"
+ ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext,
resourceWrapper.getName());
throw new ErrorEntryFreeException(errorMessage);
} else {
//在遍歷每個(gè)slot的exit方法,每個(gè)slot清理和統(tǒng)計(jì)數(shù)據(jù)
if (chain != null) {
chain.exit(context, resourceWrapper, count, args);
}
//遍歷exitHandlers,相當(dāng)于回調(diào),一般的DegradeSlot有回調(diào),
//DegradeSlot根據(jù)服務(wù)訪問(wèn)狀態(tài),決定是否將降級(jí)狀態(tài)由HALF_OPEN變?yōu)镺PEN
callExitHandlersAndCleanUp(context);
//設(shè)置為上一級(jí)Context對(duì)象
context.setCurEntry(parent);
if (parent != null) {
((CtEntry) parent).child = null;
}
if (parent == null) {
// Default context (auto entered) will be exited automatically.
if (ContextUtil.isDefaultContext(context)) {
ContextUtil.exit();
}
}
//設(shè)置當(dāng)前對(duì)象的this.context = null
clearEntryContext();
}
}
}
entry.exit()相對(duì)比較簡(jiǎn)單,它按照順序再次遍歷訪問(wèn)每個(gè)slot的exit()方法。
4、Context
Context是sentinel中的上下文對(duì)象,Context貫穿整個(gè)資源的訪問(wèn)過(guò)程。Context保存在ThreadLocal中。
創(chuàng)建Context有多種方式,可以像第二小節(jié)里面一樣,創(chuàng)建一個(gè)默認(rèn)的Context對(duì)象,也可以在訪問(wèn)資源前使用ContextUtil創(chuàng)建Context對(duì)象:
//name表示Context的名稱(chēng)或者鏈路入口的名稱(chēng),origin表示調(diào)用來(lái)源的名稱(chēng),默認(rèn)為空字符串
public static Context enter(String name, String origin);
public static Context enter(String name);
無(wú)論是上面兩種創(chuàng)建方式還是第二小節(jié)里面的創(chuàng)建方式,最終都是調(diào)用ContextUtil.trueEnter()方法:
protected static Context trueEnter(String name, String origin) {
//contextHolder是ThreadLocal<Context>類(lèi)型
Context context = contextHolder.get();
if (context == null) {
//contextNameNodeMap持有系統(tǒng)所有的入口節(jié)點(diǎn)
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
//sentinel最大只能支撐2000個(gè)入口節(jié)點(diǎn),如果超過(guò)2000個(gè),sentinel無(wú)法提供對(duì)資源的保護(hù)
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
//創(chuàng)建入口節(jié)點(diǎn)
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
//入口節(jié)點(diǎn)作為虛擬根節(jié)點(diǎn)的子節(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();
}
}
}
//創(chuàng)建Context對(duì)象,可以看到Context對(duì)象與入口節(jié)點(diǎn)一一對(duì)應(yīng)
context = new Context(node, name);
//設(shè)置調(diào)用來(lái)源
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
Context對(duì)象持有名稱(chēng)和一個(gè)入口節(jié)點(diǎn)對(duì)象,入口節(jié)點(diǎn)與對(duì)應(yīng)了線程訪問(wèn)的第一個(gè)資源,Context對(duì)象對(duì)應(yīng)了線程對(duì)資源的一次訪問(wèn),一個(gè)線程對(duì)應(yīng)一個(gè)Context對(duì)象。而且每個(gè)入口節(jié)點(diǎn)對(duì)象都是虛擬根對(duì)象ROOT的子節(jié)點(diǎn),虛擬根對(duì)象的定義如下:
//ROOT_ID=machine-root
public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));
虛擬根對(duì)象的名字為machine-root??偟膩?lái)說(shuō),Context是為了在訪問(wèn)資源的過(guò)程中保存共享數(shù)據(jù)使用的。
下面詳細(xì)介紹一下sentinel中的訪問(wèn)鏈路樹(shù)。
假如使用如下代碼訪問(wèn)資源(來(lái)源官網(wǎng)):
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
ContextUtil.enter("entrance2", "appA");
nodeA = SphU.entry("nodeB");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
以上代碼將在內(nèi)存中生成以下結(jié)構(gòu):
machine-root
/ \
/ \
entrance1 entrance2 -------表示入口節(jié)點(diǎn)對(duì)象EntranceNode
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeB) ---------內(nèi)部創(chuàng)建DefaultNode節(jié)點(diǎn)
| |
| |
ClusterNode(nodeA) ClusterNode(nodeB) ------------記錄資源的訪問(wèn)數(shù)據(jù)
再看下面這個(gè)訪問(wèn)方式:
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
Entry nodeB = SphU.entry("nodeB");
if (nodeB != null) {
nodeB.exit();
}
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
上面這個(gè)代碼創(chuàng)建的訪問(wèn)鏈路樹(shù)如下:
machine-root
/
/
entrance1 -------表示入口節(jié)點(diǎn)對(duì)象EntranceNode
/
/
DefaultNode(nodeA) ---------內(nèi)部創(chuàng)建DefaultNode節(jié)點(diǎn),持有一個(gè)ClusterNode對(duì)象
/
/
DefaultNode(nodeB) ------------記錄資源的訪問(wèn)數(shù)據(jù),持有一個(gè)ClusterNode對(duì)象
每調(diào)用一次SphU.entry()方法都會(huì)在訪問(wèn)鏈路樹(shù)上增加一個(gè)子節(jié)點(diǎn),通過(guò)這個(gè)樹(shù)可以還原出資源的訪問(wèn)路徑。
每訪問(wèn)一個(gè)資源,Context對(duì)象都使用curEntry屬性記錄下正在訪問(wèn)資源對(duì)應(yīng)的Entry對(duì)象,Entry對(duì)象有一個(gè)parent屬性記錄下父Entry,比如上面代碼中,nodeB的父Entry是nodeA,Entry還有一個(gè)curNode屬性,該屬性記錄了對(duì)應(yīng)的DefaultNode對(duì)象。每個(gè)DefaultNode對(duì)象還有一個(gè)ClusterNode類(lèi)的屬性clusterNode,clusterNode的作用是記錄被訪問(wèn)的資源的統(tǒng)計(jì)數(shù)據(jù),比如平均響應(yīng)時(shí)間、總請(qǐng)求數(shù)、QPS等,F(xiàn)lowSlot便是依據(jù)這些數(shù)據(jù)來(lái)判斷是否允許訪問(wèn)資源。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-612644.html
Context可以通過(guò)上述這些屬性構(gòu)建出一個(gè)完整的資源訪問(wèn)樹(shù),并將資源訪問(wèn)數(shù)據(jù)更新到對(duì)應(yīng)的ClusterNode對(duì)象中。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-612644.html
到了這里,關(guān)于【Spring Cloud Alibaba】Sentinel運(yùn)行原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!