sentinel流量控制和熔斷降級執行流程之原始碼分析

努力工作的小碼農發表於2021-02-04

前言:

 sentinel是阿里針對服務流量控制、熔斷降級的框架,如何使用官方都有很詳細的文件,下載它的原始碼包 裡面對各大主流框都做了適配按理,本系列文章目的 主要通過原始碼分析sentinel流量控制和熔斷降級的流程

提前準備好sentinel控制檯 如有下載原始碼啟動sentinel dashboard模組

本文演示的專案通過引入spring-cloud-starter-alibaba-sentinel包來實現接入sentinel功能 

入門案例

下面舉一個最簡單的案例埋點來引出流控入口

public String getOrderInfo(String orderNo) {
     ContextUtil.enter("getOrderInfo", "application-a"); Entry entry
= null; try { // name:資源名 EntryType 流量型別為入口還是出口,系統規則只針對入口流量, batchCount:當前請求流量, args:引數 entry = SphU.entry("getOrderInfo", EntryType.IN, 1, orderNo); getUserInfo(); } catch (BlockException e) { e.printStackTrace(); } finally { entry.exit(); } return "orderInfo = " + orderNo; } public String getUserInfo() { Entry entry = null; try { entry = SphU.entry("getUserInfo", EntryType.OUT, 1); } catch (BlockException e) { e.printStackTrace(); } finally { entry.exit(); } return "userInfo"; }
public static Entry entry(String name, EntryType trafficType, int batchCount, Object... args) throws BlockException 

也可以通過註解的方式引入,執行方法時SentinelResourceAspect會做攔截進行流控處理,當然什麼都不配也是可以的,因為引入spring-cloud-starter-alibaba-sentinel包spring mvc和spring webflux做了適配,自動會對每一個請求做埋點

@GetMapping("getOrderInfo")
@SentinelResource(value = "/getOrderInfo", entryType = EntryType.IN)
public String getOrderInfo(@RequestParam("orderNo") String orderNo) {
    return "orderInfo = " + orderNo;
}

ContextUtil.enter("getOrderInfo", "application-a") 來表示呼叫鏈的入口,可以暫時理解為上下文,一般不做宣告 後面會預設建立

第一個引數為context-name,區分不同的呼叫鏈入口,預設常量值sentinel_default_context,

第二引數為呼叫來源,這個引數可以細分從不同應用來源發出的請求,授權規則白名單和黑名單會根據該引數做限制,

然後通過SphU.entry()埋點進入,下面說下這個方法幾個引數的含義

  • name:當前資源名
  • trafficType:流量型別 分別為入口流量和出口流量。入口流量和出口流量執行過程都是差不多,只是入口流量會多了一個系統規則攔截,像是上面案例從訂單服務呼叫getUserInfo,getUserInfo是去呼叫使用者服務,它的流量方式是出去的,壓力都在使用者服務那邊,不用考慮當前伺服器的壓力,所以標為出口流量
  • batchCount:當前流量數量,一般預設為1
  • args:引數,後面做熱點引數規則時用到

BlockException:當某一規則不通過時會丟擲對應異常

SphU.entry(xxx) 需要與 entry.exit() 方法成對出現,匹配呼叫,如有巢狀像上面,需先退出getUserInfo的entry在退出getOrderInfo的entry

開啟打控制檯,此時應該是空白的,sentinel控制檯是懶載入模式,需要呼叫一下相關資源介面就可以看到

 

 

 可以看到sentinel規則配置主要有流控規則,降級規則,熱點規則,系統規則,授權規則,先簡單介紹下規則作用,其它配置也很簡單 一目瞭然,後面通過結合原始碼來深入分析

  • 流控規則:針對資源流量控制
  • 熱點規則:針對資源的熱點引數做流量控制
  • 降級規則:針對資源的排程情況來做降級處理
  • 系統規則:針對當前服務做全域性流量控制
  • 授權規則:對訪問資源的特定應用做授權處理

我們隨著SphU.entry()入口來走進原始碼

SphU.entry(xxx)執行過程原始碼分析

流控規則執行前的準備

public static Entry entry(String name, EntryType trafficType, int batchCount, Object... args)
    throws BlockException {
    return Env.sph.entry(name, trafficType, batchCount, args);
}

public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    // 將資源名稱和流量型別進行包裝
    StringResourceWrapper resource = new StringResourceWrapper(name, type);
    return entry(resource, count, args);
}

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 這裡返回當前執行緒持有的context
    Context context = ContextUtil.getContext();
    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);
    // 建立一個流量入口,將context curEntry進行指定
    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) {
        // This should not happen, unless there are errors existing in Sentinel internal.
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

可以看到準備過程主要做了五件事

  1. 將資源名稱和流量型別進行包裝
  2. 從當前執行緒得到context,如果之前沒有建立context,則這裡會建立一個context-name為sentinel_default_name、original為""的context
  3. 新增一個規則檢查呼叫鏈,根據我們配置的規則一層一層進行檢查,只要在某一個規則未通過就提前結束丟擲該規則對應的異常
  4. 建立一個流量入口entry,它用來儲存本次呼叫的資訊,將context的curEntry進行指定
  5. 開始執行規則檢查呼叫鏈

context建立過程

來看下context的建立過程,因為裡面還涉及到一個非常重要的類Node,它的作用是統計資源的呼叫資訊,如QPS、rt等資訊

protected static Context trueEnter(String name, String origin) {
    // 從當前執行緒上下文中拿
    Context context = contextHolder.get();
    if (context == null) {
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // 獲取context-name對應的DefaultNode
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            // 限制2000,也就是最多申明2000不同名稱的上下文
            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 {
                            // 建立EntranceNode
                            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;
}

Node之間的樹形結構

在建立context會先建立DefaultNode 實際是它的父類EntranceNode,context可以相同context-name反覆申明建立,但是DefaultNode同一context-name只會建立一次,DefaultNode包含了一個鏈路所有的資源,每一個資源對應一個ClusterNode,ClusterNode再根據來源細分為StatisticNode,它們之間的關係就是一個樹形結構 如下:

 

 EntranceNode:根據context-name來建立,就算同一個context-name多次建立context,entranceNode也只會建立一次, 用來統計該鏈路上所有的資源資訊

 DefaultNode:根據context-name + resource-name建立,用來統計某鏈路上的資源資訊

 ClusterNode:根據resource-name來建立,用來統計資源資訊

 StatisticsNode:根據origin-name+resource-name來建立,針對請求來源統計該來源的資源資訊,上面幾個node都是它的子類,基於它的資料做彙總

 讀者一定要搞清楚這幾個node之間的關係和作用,下面重點來看StatisticsNode,它用來完成資訊統計 以供後續的限流規則使用, 它只統計了兩個維度資料,qps和執行緒數

Node中滑動視窗實現

 

 

執行緒數統計很簡單,通過LongAdder來完成,比較簡單不過敘述,qps採用滑動視窗演算法完成,但它跟普通的滑動視窗演算法不太一樣,它的資料結構是固定,可以重複利用 減少了記憶體消耗,可以看到預設建立了兩個時間維度的視窗,分別以秒(細分為2個子視窗 500ms為間隔)和分鐘(細分為60個子視窗 1s為間隔)

 

 

 ArrayMetric 的內部是一個 LeapArray,分鐘維度使用由子類 BucketLeapArray 實現,秒維度由OccpiableBucketLeapArray實現,我們先來看BucketLeapArray ,OccpiableBucketLeapArray會在後面具體使用到時候再進行額外講解

 BucketLeapArray比較簡單,內部就實現了LeapArray兩個鉤子方法,newEmptyBucket建立空桶, MetricBuket它視窗用來統計資料的類,裡面是一個陣列LongAdder 依次存放qps、rt等資訊

 resetWindoTo 重置視窗資料,再跟到LeapArray類中

public abstract class LeapArray<T> {

    // 每個視窗長度(佔用時間)
    protected int windowLengthInMs;
    // 滑動視窗的個數
    protected int sampleCount;
    // 全部視窗的總間隔時間 (毫秒)
    protected int intervalInMs;
    // 全部視窗的總間隔時間 (秒)
    private double intervalInSecond;
    // 儲存視窗的陣列,length = sampleCount
    protected final AtomicReferenceArray<WindowWrap<T>> array;
    

    public WindowWrap<T> currentWindow(long timeMillis) {
        if (timeMillis < 0) {
            return null;
        }
        // 根據時間計算索引
        int idx = calculateTimeIdx(timeMillis);
        // 根據時間計算視窗開始時間
        long windowStart = calculateWindowStart(timeMillis);

        /*
         * 從array中獲取視窗
         *
         * (1) array中不存在,建立一個視窗 並cas加入其中
         * (2) array中視窗開始的時間=當前當前視窗開始時間,說明當前視窗剛不久已經建立過
         * (3) array中視窗開始的時間<當前獲取的時間,表示old視窗已過期,重置視窗資料並返回
         */
        while (true) {
            WindowWrap<T> old = array.get(idx);
            if (old == null) {
                WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                if (array.compareAndSet(idx, null, window)) {
                    return window;
                } else {
                    Thread.yield();
                }
            } else if (windowStart == old.windowStart()) {
                return old;
            } else if (windowStart > old.windowStart()) {
                if (updateLock.tryLock()) {
                    try {
                        return resetWindowTo(old, windowStart);
                    } finally {
                        updateLock.unlock();
                    }
                } else {
                    Thread.yield();
                }
            } else if (windowStart < old.windowStart()) {
                // Should not go through here, as the provided time is already behind.
                return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }
    }
    
    // 獲取視窗資料
    public T getWindowValue(long timeMillis) {
        if (timeMillis < 0) {
            return null;
        }
        // 根據時間計算索引
        int idx = calculateTimeIdx(timeMillis);

        WindowWrap<T> bucket = array.get(idx);

        if (bucket == null || !bucket.isTimeInWindow(timeMillis)) {
            return null;
        }

        return bucket.value();
    }


}

LeapArray主要兩個方法,currentWindow(long timeMillis),根據當前時間獲取當前視窗,getWindowValue(long timeMillis),根據當前時間獲取當前視窗的值

這裡跟一般的滑動視窗演算法不太一樣,一般的滑動視窗演算法 視窗的大小不是固定 可以實時擴容,但這裡它的大小在初始化就決定好了,當第一分鐘中的60個視窗已經全部被建立,後續時間進來獲取視窗會不斷進行覆蓋

sentinel規則呼叫鏈

我們再回到Sphu.entry()方法中來,裡面還有個sentinel規則呼叫鏈的構建,針對當前資源的排程資訊進行規則校驗

ProcessorSlot<Object> chain = this.lookProcessChain(resourceWrapper);

 

 

構建後會形成一個這樣的呼叫鏈

NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

 進入規則呼叫鏈,跟著順序一個一個來看

chain.entry(context, resourceWrapper, null, count, prioritized, args)

NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

NodeSelectorSlot:用來建立DefaultNode,前面講解node已經提到過

NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

ClusterBuilderSlot: 用來建立ClusterBuilderSlot

NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

LogSlot:發生BlockException異常,記錄日誌

NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

StatisticSlot:這個類就非常重要了,它用來進行資料統計,它先向後繼續傳遞,等後續slot全部執行後再執行統計

public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // 向後傳遞
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            // 對DefaultNode新增執行緒數和qps
            node.increaseThreadNum();
            node.addPassRequest(count);

            if (context.getCurEntry().getOriginNode() != null) {
                // Add count for origin node.
                context.getCurEntry().getOriginNode().increaseThreadNum();
                context.getCurEntry().getOriginNode().addPassRequest(count);
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseThreadNum();
                Constants.ENTRY_NODE.addPassRequest(count);
            }

            // Handle pass event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (PriorityWaitException ex) {
            xxx
        } catch (BlockException e) {
            xxx
            throw e;
        } catch (Throwable e) {
            context.getCurEntry().setError(e);
            throw e;
        }
    }
}

NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

ParamFlowSlot:校驗熱點引數規則

 

 

 NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

 SystemSlot:系統規則校驗,只對入站流量做校驗,彙總當前入站所有的資源資訊然後進行校驗

 

 

 

 

 

 NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

 AuthoritySlot:授權規則校驗,對呼叫方進行白名單或黑名單限制

 

 

 NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

 FlowSlot:校驗流控規則,具體有四種規則,由TrafficShapingController的實現類完成,裡面實現比較複雜,在下一章節會進行詳解

 NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot

 DegradeSlot校驗熔斷降級規則,分別有三種策略:慢比例呼叫、異常比例、異常數,降級規則校驗跟之前規則校驗流程不太一樣,它是直接對資源進行校驗,內部通過CircuitBreaker(斷路器)來實現,慢比例呼叫由ResponseTimeCircuitBreaker實現,

異常比例、異常數由ExceptionCircuitBreaker實現

public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        performChecking(context, resourceWrapper);

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    void performChecking(Context context, ResourceWrapper r) throws BlockException {
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
        if (circuitBreakers == null || circuitBreakers.isEmpty()) {
            return;
        }
        for (CircuitBreaker cb : circuitBreakers) {
            // 嘗試通過
            if (!cb.tryPass(context)) {
                throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
            }
        }
    }
}

 

 斷路器有三種狀態,CLOSE:正常通行,HALF_OPEN:允許探測通行,OPEN:拒絕通行,這裡解釋下為啥會有HALF_OPEN狀態出現,比如我們對同一個資源設定了兩個降級規則 R1:熔斷時間為100ms,R2:熔斷時間為200ms,當R1已到恢復點 此時R2還未恢復,

 R1狀態會從OPEN變為HALF_OPEN,R1本次校驗通過,由於R2還未恢復 R2校驗不通過,本次資源請求依然是不通過的,但如果R1、R2都已恢復 正常通行,在entry.exit()會將狀態設定為CLOSE後續請求正常通行,這就是HALF_OPEN出現的目的

 

/**************************************AbstractCircuitBreaker**************************************/
public boolean tryPass(Context context) {
    // 正常通行
    if (currentState.get() == State.CLOSED) {
        return true;
    }
    // 嘗試通行
    if (currentState.get() == State.OPEN) {
        // For half-open state we allow a request for probing.
        return retryTimeoutArrived() && fromOpenToHalfOpen(context);
    }
    return false;
}

protected boolean fromOpenToHalfOpen(Context context) {
    // 嘗試將狀態從OPEN設定為HALF_OPEN
    if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
        // 狀態變化通知
        notifyObservers(State.OPEN, State.HALF_OPEN, null);
        Entry entry = context.getCurEntry();
        // 在entry新增一個exitHandler entry.exit()時會呼叫
        entry.whenTerminate(new BiConsumer<Context, Entry>() {
            @Override
            public void accept(Context context, Entry entry) {
                // 如果有發生異常,重新將狀態設定為OPEN 請求不同通過
                if (entry.getBlockError() != null) {
                    currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
                    notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
                }
            }
        });
        // 此時狀態已設定為HALF_OPEN正常通行
        return true;
    }
    return false;
}

/**************************************CtEntry**************************************/
private void callExitHandlersAndCleanUp(Context ctx) {
    if (exitHandlers != null && !exitHandlers.isEmpty()) {
        for (BiConsumer<Context, Entry> handler : this.exitHandlers) {
            try {
                handler.accept(ctx, this);
            } catch (Exception e) {
                RecordLog.warn("Error occurred when invoking entry exit handler, current entry: "
                    + resourceWrapper.getName(), e);
            }
        }
        exitHandlers = null;
    }
}

上面只看到了狀態從OPEN變為HALF_OPEN,HALF_OPEN變為OPEN,但沒有看到狀態如何從HALF_OPEN變為CLOSE的,它的變化過程是在正常執行完請求後,entry.exit()會呼叫DegradeSlot.exit()方法來改變狀態

/**************************************DegradeSlot**************************************/
public void exit(Context context, ResourceWrapper r, int count, Object... args) {
    Entry curEntry = context.getCurEntry();
    if (curEntry.getBlockError() != null) {
        fireExit(context, r, count, args);
        return;
    }
    List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
    if (circuitBreakers == null || circuitBreakers.isEmpty()) {
        fireExit(context, r, count, args);
        return;
    }

    if (curEntry.getBlockError() == null) {
        // passed request
        for (CircuitBreaker circuitBreaker : circuitBreakers) {
            circuitBreaker.onRequestComplete(context);
        }
    }

    fireExit(context, r, count, args);
}

/**************************************ExceptionCircuitBreaker**************************************/
public void onRequestComplete(Context context) {
    Entry entry = context.getCurEntry();
    if (entry == null) {
        return;
    }
    Throwable error = entry.getError();
    SimpleErrorCounter counter = stat.currentWindow().value();
    if (error != null) {
        // 發生異常 新增異常數
        counter.getErrorCount().add(1);
    }
    counter.getTotalCount().add(1);
    
    handleStateChangeWhenThresholdExceeded(error);
}
    
    
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
    if (currentState.get() == State.OPEN) {
        return;
    }
    
    if (currentState.get() == State.HALF_OPEN) {
        // In detecting request
        if (error == null) {
            // 未發生異常 HALF_OPEN >>> CLOSE
            fromHalfOpenToClose();
        } else {
            // 發生異常 HALF_OPEN >>> OPEN
            fromHalfOpenToOpen(1.0d);
        }
        return;
    }
    // 代表此時狀態為CLOSE
    List<SimpleErrorCounter> counters = stat.values();
    long errCount = 0;
    long totalCount = 0;
    for (SimpleErrorCounter counter : counters) {
        errCount += counter.errorCount.sum();
        totalCount += counter.totalCount.sum();
    }
    if (totalCount < minRequestAmount) {
        return;
    }
    // 當前異常數
    double curCount = errCount;
    if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
        // 算出當前的異常比例
        curCount = errCount * 1.0d / totalCount;
    }
   // 判斷當前異常數或異常比例是否達到設定的閥值
if (curCount > threshold) { // 超出設定 將狀態設定為OPEN transformToOpen(curCount); } }

到此整個規則呼叫的流程我們都大致過了一遍,除了FlowSlot和DegradeSlot比較複雜,其它的規則校驗原始碼都不算難,讀者自行了解

相關文章