1. 程式人生 > 程式設計 >阿里Sentinel元件之開篇

阿里Sentinel元件之開篇

1. 介紹

當前許多專案承載著大量業務功能,在單一工程中進行開發會造成程式碼量劇增,為專案的開發、部署、運維以及擴充套件造成障礙。在此背景下,微服務的出現有利於解決上述出現的問題,然而服務的穩定性又造成巨大的困擾。

本章介紹的阿里 Sentinel 元件以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。該元件的核心功能不依賴任何外部專案,同時官方稱引入 Sentinel 帶來的效能損耗非常小,只有在業務單機量級超過 25W QPS 的時候才會有一些顯著的影響(10% 左右),單機 QPS 不太大的時候損耗幾乎可以忽略不計。

Sentinel 具有以下特徵:

  • 豐富的應用場景
    :Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、訊息削峰填谷、叢集流量控制、實時熔斷下游不可用應用等。
  • 完備的實時監控: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 方法,我們可以獲取以下資訊:

  1. 上面提到每一個資源都有一份slot chain,該資訊以資源的名稱為key,儲存於 Ctsph::chainMap中。
  2. 一個專案最多隻能存在 Constants.MAX_SLOT_CHAIN_SIZE(預設6000)個資源,超過後不再生效。
  3. 通過 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。