Sentinel 原理-全解析

逅弈逐碼發表於2019-01-09

逅弈 轉載請註明原創出處,謝謝!

系列文章

Sentinel 原理-呼叫鏈

Sentinel 原理-滑動視窗

Sentinel 原理-實體類

Sentinel 實戰-限流篇

Sentinel 實戰-控制檯篇

Sentinel 實戰-規則持久化

Sentinel 是阿里中介軟體團隊開源的,面向分散式服務架構的輕量級高可用流量控制元件,主要以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度來幫助使用者保護服務的穩定性。

大家可能會問:Sentinel 和之前常用的熔斷降級庫 Netflix Hystrix 有什麼異同呢?Sentinel官網有一個對比的文章,這裡摘抄一個總結的表格,具體的對比可以點此 連結 檢視。

對比內容 Sentinel Hystrix
隔離策略 訊號量隔離 執行緒池隔離/訊號量隔離
熔斷降級策略 基於響應時間或失敗比率 基於失敗比率
實時指標實現 滑動視窗 滑動視窗(基於 RxJava)
規則配置 支援多種資料來源 支援多種資料來源
擴充套件性 多個擴充套件點 外掛的形式
基於註解的支援 支援 支援
限流 基於 QPS,支援基於呼叫關係的限流 不支援
流量整形 支援慢啟動、勻速器模式 不支援
系統負載保護 支援 不支援
控制檯 開箱即用,可配置規則、檢視秒級監控、機器發現等 不完善
常見框架的適配 Servlet、Spring Cloud、Dubbo、gRPC 等 Servlet、Spring Cloud Netflix

從對比的表格可以看到,Sentinel比Hystrix在功能性上還要強大一些,本文讓我們一起來了解下Sentinel的原始碼,揭開Sentinel的神祕面紗。

專案結構

將Sentinel的原始碼fork到自己的github庫中,接著把原始碼clone到本地,然後開始原始碼閱讀之旅吧。

首先我們看一下Sentinel專案的整個結構:

sentinel-project-structure.png

  • sentinel-core 核心模組,限流、降級、系統保護等都在這裡實現
  • sentinel-dashboard 控制檯模組,可以對連線上的sentinel客戶端實現視覺化的管理
  • sentinel-transport 傳輸模組,提供了基本的監控服務端和客戶端的API介面,以及一些基於不同庫的實現
  • sentinel-extension 擴充套件模組,主要對DataSource進行了部分擴充套件實現
  • sentinel-adapter 介面卡模組,主要實現了對一些常見框架的適配
  • sentinel-demo 樣例模組,可參考怎麼使用sentinel進行限流、降級等
  • sentinel-benchmark 基準測試模組,對核心程式碼的精確性提供基準測試

執行樣例

基本上每個框架都會帶有樣例模組,有的叫example,有的叫demo,sentinel也不例外。

那我們從sentinel的demo中找一個例子執行下看看大致的情況吧,上面說過了sentinel主要的核心功能是做限流、降級和系統保護,那我們就從“限流”開始看sentinel的實現原理吧。

sentinel-basic-demo-flow-qps.png

可以看到sentinel-demo模組中有很多不同的樣例,我們找到basic模組下的flow包,這個包下面就是對應的限流的樣例,但是限流也有很多種型別的限流,我們就找根據qps限流的類看吧,其他的限流方式原理上都大差不差。

public class FlowQpsDemo {

    private static final String KEY = "abc";

    private static AtomicInteger pass = new AtomicInteger();
    private static AtomicInteger block = new AtomicInteger();
    private static AtomicInteger total = new AtomicInteger();

    private static volatile boolean stop = false;

    private static final int threadCount = 32;

    private static int seconds = 30;

    public static void main(String[] args) throws Exception {
        initFlowQpsRule();

        tick();
        // first make the system run on a very low condition
        simulateTraffic();

        System.out.println("===== begin to do flow control");
        System.out.println("only 20 requests per second can pass");

    }

    private static void initFlowQpsRule() {
        List<FlowRule> rules = new ArrayList<FlowRule>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource(KEY);
        // set limit qps to 20
        rule1.setCount(20);
        // 設定限流型別:根據qps
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        // 載入限流的規則
        FlowRuleManager.loadRules(rules);
    }

    private static void simulateTraffic() {
        for (int i = 0; i < threadCount; i++) {
            Thread t = new Thread(new RunTask());
            t.setName("simulate-traffic-Task");
            t.start();
        }
    }

    private static void tick() {
        Thread timer = new Thread(new TimerTask());
        timer.setName("sentinel-timer-task");
        timer.start();
    }

    static class TimerTask implements Runnable {

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            System.out.println("begin to statistic!!!");

            long oldTotal = 0;
            long oldPass = 0;
            long oldBlock = 0;
            while (!stop) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
                long globalTotal = total.get();
                long oneSecondTotal = globalTotal - oldTotal;
                oldTotal = globalTotal;

                long globalPass = pass.get();
                long oneSecondPass = globalPass - oldPass;
                oldPass = globalPass;

                long globalBlock = block.get();
                long oneSecondBlock = globalBlock - oldBlock;
                oldBlock = globalBlock;

                System.out.println(seconds + " send qps is: " + oneSecondTotal);
                System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal
                    + ", pass:" + oneSecondPass
                    + ", block:" + oneSecondBlock);

                if (seconds-- <= 0) {
                    stop = true;
                }
            }

            long cost = System.currentTimeMillis() - start;
            System.out.println("time cost: " + cost + " ms");
            System.out.println("total:" + total.get() + ", pass:" + pass.get()
                + ", block:" + block.get());
            System.exit(0);
        }
    }

    static class RunTask implements Runnable {
        @Override
        public void run() {
            while (!stop) {
                Entry entry = null;

                try {
                    entry = SphU.entry(KEY);
                    // token acquired, means pass
                    pass.addAndGet(1);
                } catch (BlockException e1) {
                    block.incrementAndGet();
                } catch (Exception e2) {
                    // biz exception
                } finally {
                    total.incrementAndGet();
                    if (entry != null) {
                        entry.exit();
                    }
                }

                Random random2 = new Random();
                try {
                    TimeUnit.MILLISECONDS.sleep(random2.nextInt(50));
                } catch (InterruptedException e) {
                    // ignore
                }
            }
        }
    }
}
複製程式碼

執行上面的程式碼後,列印出如下的結果:

sentinel-basic-demo-flow-qps-result.png

可以看到,上面的結果中,pass的數量和我們的預期並不相同,我們預期的是每秒允許pass的請求數是20個,但是目前有很多pass的請求數是超過20個的。

原因是,我們這裡測試的程式碼使用了多執行緒,注意看 threadCount 的值,一共有32個執行緒來模擬,而在RunTask的run方法中執行資源保護時,即在 SphU.entry 的內部是沒有加鎖的,所以就會導致在高併發下,pass的數量會高於20。

可以用下面這個模型來描述下,有一個TimeTicker執行緒在做統計,每1秒鐘做一次。有N個RunTask執行緒在模擬請求,被訪問的business code被資源key保護著,根據規則,每秒只允許20個請求通過。

由於pass、block、total等計數器是全域性共享的,而多個RunTask執行緒在執行SphU.entry申請獲取entry時,內部沒有鎖保護,所以會存在pass的個數超過設定的閾值。

sentinel-basic-demo-flow-qps-module.png

那為了證明在單執行緒下限流的正確性與可靠性,那我們的模型就應該變成了這樣:

sentinel-basic-demo-flow-qps-single-thread-module.png

那接下來我把 threadCount 的值改為1,只有一個執行緒來執行這個方法,看下具體的限流結果,執行上面的程式碼後列印的結果如下:

sentinel-basic-demo-single-thread-flow-qps-result.png

可以看到pass數基本上維持在20,但是第一次統計的pass值還是超過了20。這又是什麼原因導致的呢?

其實仔細看下Demo中的程式碼可以發現,模擬請求是用的一個執行緒,統計結果是用的另外一個執行緒,統計執行緒每1秒鐘統計一次結果,這兩個執行緒之間是有時間上的誤差的。從TimeTicker執行緒列印出來的時間戳可以看出來,雖然每隔一秒進行統計,但是當前列印時的時間和上一次的時間還是有誤差的,不完全是1000ms的間隔。

要真正驗證每秒限制20個請求,保證資料的精準性,需要做基準測試,這個不是本篇文章的重點,有興趣的同學可以去了解下jmh,sentinel中的基準測試也是通過jmh做的。

深入原理

通過一個簡單的示例程式,我們瞭解了sentinel可以對請求進行限流,除了限流外,還有降級和系統保護等功能。那現在我們就撥開雲霧,深入原始碼內部去一窺sentinel的實現原理吧。

首先從入口開始:SphU.entry() 。這個方法會去申請一個entry,如果能夠申請成功,則說明沒有被限流,否則會丟擲BlockException,表面已經被限流了。

SphU.entry() 方法往下執行會進入到 Sph.entry() ,Sph的預設實現類是 CtSph ,在CtSph中最終會執行到 entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException 這個方法。

我們來看一下這個方法的具體實現:

public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
    Context context = ContextUtil.getContext();
    if (context instanceof NullContext) {
        // Init the entry only. No rule checking will occur.
        return new CtEntry(resourceWrapper, null, context);
    }

    if (context == null) {
        context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
    }

    // Global switch is close, no rule checking will do.
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }

    // 獲取該資源對應的SlotChain
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

    /*
     * Means processor cache size exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE}, so no
     * rule checking will be done.
     */
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }

    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
    	// 執行Slot的entry方法
        chain.entry(context, resourceWrapper, null, count, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        // 丟擲BlockExecption
        throw e1;
    } catch (Throwable e1) {
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}
複製程式碼

這個方法可以分為以下幾個部分:

  • 1.對引數和全域性配置項做檢測,如果不符合要求就直接返回了一個CtEntry物件,不會再進行後面的限流檢測,否則進入下面的檢測流程。
  • 2.根據包裝過的資源物件獲取對應的SlotChain
  • 3.執行SlotChain的entry方法
    • 3.1.如果SlotChain的entry方法丟擲了BlockException,則將該異常繼續向上丟擲
    • 3.2.如果SlotChain的entry方法正常執行了,則最後會將該entry物件返回
  • 4.如果上層方法捕獲了BlockException,則說明請求被限流了,否則請求能正常執行

其中比較重要的是第2、3兩個步驟,我們來分解一下這兩個步驟。

建立SlotChain

首先看一下lookProcessChain的方法實現:

private ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    if (chain == null) {
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // Entry size limit.
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    return null;
                }

                // 具體構造chain的方法
                chain = Env.slotsChainbuilder.build();
                Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}
複製程式碼

該方法使用了一個HashMap做了快取,key是資源物件。這裡加了鎖,並且做了 double check 。具體構造chain的方法是通過: Env.slotsChainbuilder.build() 這句程式碼建立的。那就進入這個方法看看吧。

public ProcessorSlotChain build() {
    ProcessorSlotChain chain = new DefaultProcessorSlotChain();
    chain.addLast(new NodeSelectorSlot());
    chain.addLast(new ClusterBuilderSlot());
    chain.addLast(new LogSlot());
    chain.addLast(new StatisticSlot());
    chain.addLast(new SystemSlot());
    chain.addLast(new AuthoritySlot());
    chain.addLast(new FlowSlot());
    chain.addLast(new DegradeSlot());

    return chain;
}
複製程式碼

Chain是鏈條的意思,從build的方法可看出,ProcessorSlotChain是一個連結串列,裡面新增了很多個Slot。具體的實現需要到DefaultProcessorSlotChain中去看。

public class DefaultProcessorSlotChain extends ProcessorSlotChain {

    AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
        @Override
        public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)
            throws Throwable {
            super.fireEntry(context, resourceWrapper, t, count, args);
        }
        @Override
        public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
            super.fireExit(context, resourceWrapper, count, args);
        }
    };
    
    AbstractLinkedProcessorSlot<?> end = first;

    @Override
    public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
        if (end == first) {
            end = protocolProcessor;
        }
    }

    @Override
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }
}
複製程式碼

DefaultProcessorSlotChain中有兩個AbstractLinkedProcessorSlot型別的變數:first和end,這就是連結串列的頭結點和尾節點。

建立DefaultProcessorSlotChain物件時,首先建立了首節點,然後把首節點賦值給了尾節點,可以用下圖表示:

slot-chain-1.png

將第一個節點新增到連結串列中後,整個連結串列的結構變成了如下圖這樣:

slot-chain-2.png

將所有的節點都加入到連結串列中後,整個連結串列的結構變成了如下圖所示:

slot-chain-3.png

這樣就將所有的Slot物件新增到了連結串列中去了,每一個Slot都是繼承自AbstractLinkedProcessorSlot。而AbstractLinkedProcessorSlot是一種責任鏈的設計,每個物件中都有一個next屬性,指向的是另一個AbstractLinkedProcessorSlot物件。其實責任鏈模式在很多框架中都有,比如Netty中是通過pipeline來實現的。

知道了SlotChain是如何建立的了,那接下來就要看下是如何執行Slot的entry方法的了。

執行SlotChain的entry方法

lookProcessChain方法獲得的ProcessorSlotChain的例項是DefaultProcessorSlotChain,那麼執行chain.entry方法,就會執行DefaultProcessorSlotChain的entry方法,而DefaultProcessorSlotChain的entry方法是這樣的:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)
    throws Throwable {
    first.transformEntry(context, resourceWrapper, t, count, args);
}
複製程式碼

也就是說,DefaultProcessorSlotChain的entry實際是執行的first屬性的transformEntry方法。

而transformEntry方法會執行當前節點的entry方法,在DefaultProcessorSlotChain中first節點重寫了entry方法,具體如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)
    throws Throwable {
    super.fireEntry(context, resourceWrapper, t, count, args);
}
複製程式碼

first節點的entry方法,實際又是執行的super的fireEntry方法,那繼續把目光轉移到fireEntry方法,具體如下:

@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)
    throws Throwable {
    if (next != null) {
        next.transformEntry(context, resourceWrapper, obj, count, args);
    }
}
複製程式碼

從這裡可以看到,從fireEntry方法中就開始傳遞執行entry了,這裡會執行當前節點的下一個節點transformEntry方法,上面已經分析過了,transformEntry方法會觸發當前節點的entry,也就是說fireEntry方法實際是觸發了下一個節點的entry方法。具體的流程如下圖所示:

slot-chain-entry-process.png

從圖中可以看出,從最初的呼叫Chain的entry()方法,轉變成了呼叫SlotChain中Slot的entry()方法。從上面的分析可以知道,SlotChain中的第一個Slot節點是NodeSelectorSlot。

執行Slot的entry方法

現在可以把目光轉移到SlotChain中的第一個節點NodeSelectorSlot的entry方法中去了,具體的程式碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)
    throws Throwable {
    
    DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
                HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                cacheMap.putAll(map);
                cacheMap.put(context.getName(), node);
                map = cacheMap;
            }
            // Build invocation tree
            ((DefaultNode)context.getLastNode()).addChild(node);
        }
    }

    context.setCurNode(node);
    // 由此觸發下一個節點的entry方法
    fireEntry(context, resourceWrapper, node, count, args);
}
複製程式碼

從程式碼中可以看到,NodeSelectorSlot節點做了一些自己的業務邏輯處理,具體的大家可以深入原始碼繼續追蹤,這裡大概的介紹下每種Slot的功能職責:

  • NodeSelectorSlot 負責收集資源的路徑,並將這些資源的呼叫路徑,以樹狀結構儲存起來,用於根據呼叫路徑來限流降級;
  • ClusterBuilderSlot 則用於儲存資源的統計資訊以及呼叫者資訊,例如該資源的 RT, QPS, thread count 等等,這些資訊將用作為多維度限流,降級的依據;
  • StatistcSlot 則用於記錄,統計不同緯度的 runtime 資訊;
  • FlowSlot 則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流;
  • AuthorizationSlot 則根據黑白名單,來做黑白名單控制;
  • DegradeSlot 則通過統計資訊,以及預設的規則,來做熔斷降級;
  • SystemSlot 則通過系統的狀態,例如 load1 等,來控制總的入口流量;

執行完業務邏輯處理後,呼叫了fireEntry()方法,由此觸發了下一個節點的entry方法。此時我們就知道了sentinel的責任鏈就是這樣傳遞的:每個Slot節點執行完自己的業務後,會呼叫fireEntry來觸發下一個節點的entry方法。

所以可以將上面的圖完整了,具體如下:

slot-chain-entry-whole-process.png

至此就通過SlotChain完成了對每個節點的entry()方法的呼叫,每個節點會根據建立的規則,進行自己的邏輯處理,當統計的結果達到設定的閾值時,就會觸發限流、降級等事件,具體是丟擲BlockException異常。

總結

sentinel主要是基於7種不同的Slot形成了一個連結串列,每個Slot都各司其職,自己做完分內的事之後,會把請求傳遞給下一個Slot,直到在某一個Slot中命中規則後丟擲BlockException而終止。

前三個Slot負責做統計,後面的Slot負責根據統計的結果結合配置的規則進行具體的控制,是Block該請求還是放行。

控制的型別也有很多可選項:根據qps、執行緒數、冷啟動等等。

然後基於這個核心的方法,衍生出了很多其他的功能:

  • 1、dashboard控制檯,可以視覺化的對每個連線過來的sentinel客戶端 (通過傳送heartbeat訊息)進行控制,dashboard和客戶端之間通過http協議進行通訊。
  • 2、規則的持久化,通過實現DataSource介面,可以通過不同的方式對配置的規則進行持久化,預設規則是在記憶體中的
  • 3、對主流的框架進行適配,包括servlet,dubbo,rRpc等

Dashboard控制檯

sentinel-dashboard是一個單獨的應用,通過spring-boot進行啟動,主要提供一個輕量級的控制檯,它提供機器發現、單機資源實時監控、叢集資源彙總,以及規則管理的功能。

我們只需要對應用進行簡單的配置,就可以使用這些功能。

1 啟動控制檯

1.1 下載程式碼並編譯控制檯

  • 下載 控制檯 工程
  • 使用以下命令將程式碼打包成一個 fat jar: mvn clean package

1.2 啟動

使用如下命令啟動編譯後的控制檯:

$ java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -jar target/sentinel-dashboard.jar
複製程式碼

上述命令中我們指定了一個JVM引數,-Dserver.port=8080 用於指定 Spring Boot 啟動埠為 8080

2 客戶端接入控制檯

控制檯啟動後,客戶端需要按照以下步驟接入到控制檯。

2.1 引入客戶端jar包

通過 pom.xml 引入 jar 包:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>x.y.z</version>
</dependency>
複製程式碼

2.2 配置啟動引數

啟動時加入 JVM 引數 -Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制檯地址和埠。若啟動多個應用,則需要通過 -Dcsp.sentinel.api.port=xxxx 指定客戶端監控 API 的埠(預設是 8719)。

除了修改 JVM 引數,也可以通過配置檔案取得同樣的效果。更詳細的資訊可以參考 啟動配置項

2.3 觸發客戶端初始化

確保客戶端有訪問量,Sentinel 會在客戶端首次呼叫的時候進行初始化,開始向控制檯傳送心跳包。

sentinel-dashboard是一個獨立的web應用,可以接受客戶端的連線,然後與客戶端之間進行通訊,他們之間使用http協議進行通訊。他們之間的關係如下圖所示:

dashboard-client-transport.png

dashboard

dashboard啟動後會等待客戶端的連線,具體的做法是在 MachineRegistryController 中有一個 receiveHeartBeat 的方法,客戶端傳送心跳訊息,就是通過http請求這個方法。

dashboard接收到客戶端的心跳訊息後,會把客戶端的傳遞過來的ip、port等資訊封裝成一個 MachineInfo 物件,然後將該物件通過 MachineDiscovery 介面的 addMachine 方法新增到一個ConcurrentHashMap中儲存起來。

這裡會有問題,因為客戶端的資訊是儲存在dashboard的記憶體中的,所以當dashboard應用重啟後,之前已經傳送過來的客戶端資訊都會丟失掉。

client

client在啟動時,會通過CommandCenterInitFunc選擇一個,並且只選擇一個CommandCenter進行啟動。

啟動之前會通過spi的方式掃描獲取到所有的CommandHandler的實現類,然後將所有的CommandHandler註冊到一個HashMap中去,待後期使用。

PS:考慮一下,為什麼CommandHandler不需要做持久化,而是直接儲存在記憶體中。

註冊完CommandHandler之後,緊接著就啟動CommandCenter了,目前CommandCenter有兩個實現類:

  • SimpleHttpCommandCenter 通過ServerSocket啟動一個服務端,接受socket連線
  • NettyHttpCommandCenter 通過Netty啟動一個服務端,接受channel連線

CommandCenter啟動後,就等待dashboard傳送訊息過來了,當接收到訊息後,會把訊息通過具體的CommandHandler進行處理,然後將處理的結果返回給dashboard。

這裡需要注意的是,dashboard給client傳送訊息是通過非同步的httpClient進行傳送的,在HttpHelper類中。

但是詭異的是,既然通過非同步傳送了,又通過一個CountDownLatch來等待訊息的返回,然後獲取結果,那這樣不就失去了非同步的意義的嗎?具體的程式碼如下:

private String httpGetContent(String url) {
    final HttpGet httpGet = new HttpGet(url);
    final CountDownLatch latch = new CountDownLatch(1);
    final AtomicReference<String> reference = new AtomicReference<>();
    httpclient.execute(httpGet, new FutureCallback<HttpResponse>() {
        @Override
        public void completed(final HttpResponse response) {
            try {
                reference.set(getBody(response));
            } catch (Exception e) {
                logger.info("httpGetContent " + url + " error:", e);
            } finally {
                latch.countDown();
            }
        }

        @Override
        public void failed(final Exception ex) {
            latch.countDown();
            logger.info("httpGetContent " + url + " failed:", ex);
        }

        @Override
        public void cancelled() {
            latch.countDown();
        }
    });
    try {
        latch.await(5, TimeUnit.SECONDS);
    } catch (Exception e) {
        logger.info("wait http client error:", e);
    }
    return reference.get();
}

複製程式碼

主流框架的適配

sentinel也對一些主流的框架進行了適配,使得在使用主流框架時,也可以享受到sentinel的保護。目前已經支援的介面卡包括以下這些:

  • Web Servlet
  • Dubbo
  • Spring Boot / Spring Cloud
  • gRPC
  • Apache RocketMQ

其實做適配就是通過那些主流框架的擴充套件點,然後在擴充套件點上加入sentinel限流降級的程式碼即可。拿Servlet的適配程式碼看一下,具體的程式碼是:

public class CommonFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        HttpServletRequest sRequest = (HttpServletRequest)request;
        Entry entry = null;

        try {
        	// 根據請求生成的資源
            String target = FilterUtil.filterTarget(sRequest);
            target = WebCallbackManager.getUrlCleaner().clean(target);

            // “申請”該資源
            ContextUtil.enter(target);
            entry = SphU.entry(target, EntryType.IN);

            // 如果能成功“申請”到資源,則說明未被限流
            // 則將請求放行
            chain.doFilter(request, response);
        } catch (BlockException e) {
        	// 否則如果捕獲了BlockException異常,說明請求被限流了
        	// 則將請求重定向到一個預設的頁面
            HttpServletResponse sResponse = (HttpServletResponse)response;
            WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse);
        } catch (IOException e2) {
            // 省略部分程式碼
        } finally {
            if (entry != null) {
                entry.exit();
            }
            ContextUtil.exit();
        }
    }

    @Override
    public void destroy() {

    }
}
複製程式碼

通過Servlet的Filter進行擴充套件,實現一個Filter,然後在doFilter方法中對請求進行限流控制,如果請求被限流則將請求重定向到一個預設頁面,否則將請求放行給下一個Filter。

規則持久化,動態化

Sentinel 的理念是開發者只需要關注資源的定義,當資源定義成功,可以動態增加各種流控降級規則。

Sentinel 提供兩種方式修改規則:

  • 通過 API 直接修改 (loadRules)
  • 通過DataSource適配不同資料來源修改

通過 API 修改比較直觀,可以通過以下三個 API 修改不同的規則:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規則
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級規則
SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系統規則
複製程式碼

DataSource 擴充套件

上述 loadRules() 方法只接受記憶體態的規則物件,但應用重啟後記憶體中的規則就會丟失,更多的時候規則最好能夠儲存在檔案、資料庫或者配置中心中。

DataSource 介面給我們提供了對接任意配置源的能力。相比直接通過 API 修改規則,實現 DataSource 介面是更加可靠的做法。

官方推薦通過控制檯設定規則後將規則推送到統一的規則中心,使用者只需要實現 DataSource 介面,來監聽規則中心的規則變化,以實時獲取變更的規則

DataSource 擴充常見的實現方式有:

  • 拉模式:客戶端主動向某個規則管理中心定期輪詢拉取規則,這個規則中心可以是 SQL、檔案,甚至是 VCS 等。這樣做的方式是簡單,缺點是無法及時獲取變更;
  • 推模式:規則中心統一推送,客戶端通過註冊監聽器的方式時刻監聽變化,比如使用 Nacos、Zookeeper 等配置中心。這種方式有更好的實時性和一致性保證。

至此,sentinel的基本情況都已經分析了,更加詳細的內容,可以繼續閱讀原始碼來研究。

更多原創好文,請關注「逅弈逐碼」

更多原創好文,請關注公眾號「逅弈逐碼」

相關文章