Sentinel
做爲ali開源的一款輕量級流控框架,主要以流量爲切入點,從流量控制、熔斷降級、系統負載保護等多個維度來幫助用戶保護服務的穩定性。相比於Hystrix
,Sentinel
的設計更加簡單,在 Sentinel
中資源定義和規則配置是分離的,也就是說用戶能夠先經過Sentinel API
給對應的業務邏輯定義資源(埋點),而後在須要的時候再配置規則,經過這種組合方式,極大的增長了Sentinel
流控的靈活性。html
引入Sentinel
帶來的性能損耗很是小。只有在業務單機量級超過25W QPS的時候纔會有一些顯著的影響(5% - 10% 左右),單機QPS不太大的時候損耗幾乎能夠忽略不計。java
Sentinel
提供兩種埋點方式:node
try-catch
方式(經過 SphU.entry(...)
),用戶在 catch 塊中執行異常處理 / fallbackif-else
方式(經過 SphO.entry(...)
),當返回 false 時執行異常處理 / fallback在此以前,須要先了解一下Sentinel
的工做流程
在 Sentinel
裏面,全部的資源都對應一個資源名稱(resourceName
),每次資源調用都會建立一個 Entry
對象。Entry
能夠經過對主流框架的適配自動建立,也能夠經過註解的方式或調用 SphU API
顯式建立。Entry
建立的時候,同時也會建立一系列功能插槽(slot chain
),這些插槽有不一樣的職責,例如默認狀況下會建立一下7個插槽:緩存
NodeSelectorSlot
負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級;ClusterBuilderSlot
則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count
等等,這些信息將用做爲多維度限流,降級的依據;StatisticSlot
則用於記錄、統計不一樣緯度的 runtime
指標監控信息;FlowSlot
則用於根據預設的限流規則以及前面 slot
統計的狀態,來進行流量控制;AuthoritySlot
則根據配置的黑白名單和調用來源信息,來作黑白名單控制;DegradeSlot
則經過統計信息以及預設的規則,來作熔斷降級;SystemSlot
則經過系統的狀態,例如 load1
等,來控制總的入口流量注意:這裏的插槽鏈都是一一對應資源名稱的app
上面的所介紹的插槽(slot chain
)是Sentinel
很是重要的概念。同時還有一個很是重要的概念那就是Node
,爲了幫助理解,盡我所能畫了下面這張圖,能夠看到整個結構很是的像一棵樹:框架
簡單解釋下上圖:ide
node
節點爲根節點,全局惟一CentextName
(上下文名稱)一一對應一個
ResourceName
)以上2個概念務必要理清楚,以後再一步一步看源碼會比較清晰源碼分析
下面咱們將從入口源碼開始一步一步分析整個調用過程:性能
下面的是一個Sentinel
使用的示例代碼,咱們就從這裏切入開始分析ui
// 建立一個名稱爲entrance1,來源爲appA 的上下文Context ContextUtil.enter("entrance1", "appA"); // 建立一個資源名稱nodeA的Entry Entry nodeA = SphU.entry("nodeA"); if (nodeA != null) { nodeA.exit(); } // 清除上下文 ContextUtil.exit();
public static Context enter(String name, String origin) { // 判斷上下文名稱是否爲默認的名稱(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; // 嘗試從緩存中獲取該上下文名稱對應的 入口節點 DefaultNode node = localCacheNameMap.get(name); if (node == null) { // 判斷緩存中入口節點數量是否大於2000 if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { try { // 加鎖 LOCK.lock(); // 雙重檢查鎖 node = contextNameNodeMap.get(name); if (node == null) { // 判斷緩存中入口節點數量是否大於2000 if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { // 根據上下文名稱生成入口節點(entranceNode) node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); // 加入至全局根節點下 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; }
主要作了2件事情
ContextName
生成entranceNode
,並加入緩存,每一個ContextName
對應一個入口節點entranceNode
ContextName
和entranceNode
初始化上下文對象,並將上下文對象設置到當前線程中這裏有幾點須要注意:
ContextName
對應一個入口節點entranceNode
entranceNode
都有共同的父節點。也就是根節點// SphU.class public static Entry entry(String name) throws BlockException { // 默認爲 出口流量類型,單位統計數爲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 { // 生成資源對象 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); }
上面的代碼比較簡單,不指定EntryType
的話,則默認爲出口流量類型,最終會調用entryWithPriority
方法,主要業務邏輯也都在這個方法中
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // 獲取當前線程上下文對象 Context context = ContextUtil.getContext(); // 上下文名稱對應的入口節點是否已經超過閾值2000,超過則會返回空 CtEntry if (context instanceof NullContext) { return new CtEntry(resourceWrapper, null, context); } if (context == null) { // 若是沒有指定上下文名稱,則使用默認名稱,也就是默認入口節點 context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); } // 全局開關 if (!Constants.ON) { return new CtEntry(resourceWrapper, null, context); } // 生成插槽鏈 ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); /* * 表示資源(插槽鏈)超過6000,所以不會進行規則檢查。 */ if (chain == null) { return new CtEntry(resourceWrapper, null, context); } // 生成 Entry 對象 Entry e = new CtEntry(resourceWrapper, chain, context); try { // 開始執行插槽鏈 調用邏輯 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; }
這個方法能夠說是涵蓋了整個Sentinel的核心邏輯
lookProcessChain(resourceWrapper);
中,下文會分析lookProcessChain
方法爲指定資源生成插槽鏈,下面咱們來看下它的初始化邏輯
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) { // 根據資源嘗試從全局緩存中獲取 ProcessorSlotChain chain = chainMap.get(resourceWrapper); if (chain == null) { // 很是常見的雙重檢查鎖 synchronized (LOCK) { chain = chainMap.get(resourceWrapper); if (chain == null) { // 判斷資源數是否大於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; }
SlotChainProvider.newSlotChain()
方法中)下面咱們看下初始化插槽鏈上的插槽的邏輯
public static ProcessorSlotChain newSlotChain() { // 判斷是否已經初始化過 if (builder != null) { return builder.build(); } // 加載 SlotChain resolveSlotChainBuilder(); // 加載失敗則使用默認 插槽鏈 if (builder == null) { RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default"); builder = new DefaultSlotChainBuilder(); } // 構建完成 return builder.build(); } /** * java自帶 SPI機制 加載 slotChain */ private static void resolveSlotChainBuilder() { List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>(); boolean hasOther = false; // 嘗試獲取自定義SlotChainBuilder,經過JAVA SPI機制擴展 for (SlotChainBuilder builder : LOADER) { if (builder.getClass() != DefaultSlotChainBuilder.class) { hasOther = true; list.add(builder); } } if (hasOther) { builder = list.get(0); } else { // 未獲取到自定義 SlotChainBuilder 則使用默認的 builder = new DefaultSlotChainBuilder(); } RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: " + builder.getClass().getCanonicalName()); }
SlotChainBuilder
來構建插槽鏈,自定義的SlotChainBuilder
能夠經過JAVA SPI機制來擴展SlotChainBuilder
,則會使用默認的DefaultSlotChainBuilder
來構建插槽鏈,DefaultSlotChainBuilder
所構建的插槽就是文章開頭咱們提到的7種Slot
。每一個插槽都有其對應的職責,各司其職,後面咱們會詳細分析這幾個插槽的源碼,及所承擔的職責。文章開頭的提到的兩個點(插槽鏈和Node),這是Sentinel的重點,理解這兩點對於閱讀源碼來講事半功倍
Sentinel系列