分佈式系統流控、熔斷:Sentinel的使用

前言

隨着微服務的流行,服務和服務之間的穩定性變得越來越重要。Sentinel 以流量爲切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。

Sentinel 具有以下特徵:

  • 豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、消息削峯填谷、集羣流量控制、實時熔斷下游不可用應用等。

  • 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制檯中看到接入應用的單臺機器秒級數據,甚至 500 臺以下規模的集羣的彙總運行情況。

  • 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。

  • 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定製邏輯。例如定製規則管理、適配動態數據源等。

以上內容引自 Sentinel 官方介紹。在本文中,筆者將從實際應用的角度,來學習Sentinel的使用。

一、初識Sentinel

首先,我們需要引入Sentinel的依賴。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.7.2</version>
</dependency>

Sentinel 支持以下幾種規則:流量控制規則、熔斷降級規則、系統保護規則、來源訪問控制規則 和 熱點參數規則。

在這裏,我們來展示一個流量控制和熔斷降級的示例。

1、流量控制

流量控制,其原理是監控應用流量的 QPS 或併發線程數等指標,當達到指定的閾值時對流量進行控制,以避免被瞬時的流量高峯沖垮,從而保障應用的高可用性。

我們以 QPS 爲例,先來定義它的規則,相關屬性含義見註釋。

/**
 * 加載限流規則
 * @param resource
 */
public static void loadFlowRules(String resource){
    FlowRule rule = new FlowRule();
    //資源名稱,可以是任意字符串
    rule.setResource(resource);
    //限流閾值
    rule.setCount(5);
    //限流閾值類型,設置爲QPS。即每秒QPS大於5時,觸發限流
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    //針對的調用來源
    rule.setLimitApp("default");
    //調用關係限流策略,默認按照資源本身
    rule.setStrategy(RuleConstant.STRATEGY_DIRECT);
    //限流效果,默認直接拒絕
    rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
    //是否集羣限流
    rule.setClusterMode(false);
    FlowRuleManager.loadRules(Collections.singletonList(rule));
}

如上代碼,當每秒的請求數達到 5 之後,就會直接拒絕當前時間窗口的後續請求。

接下來,我們把需要控制流量的代碼用 Sentinel API SphU.entry("resource") 和 entry.exit() 包圍起來即可。

public static void main(String[] args) throws InterruptedException {
    loadFlowRules("orderService");
    while (!stop){
        count.incrementAndGet();
        Entry entry = null;
        try {
            entry = SphU.entry(resource);
            logger.info("業務操作...{}",count.get());
        } catch (BlockException e) {
            logger.error("請求被限流...{}",count.get());
            Thread.sleep(1000);
        } finally {
            if (entry != null) {
                entry.exit();
            }
            if (count.get()>=20){
                stop = true;
            }
        }
    }
}

如上代碼,我們先通過loadFlowRules()方法加載限流規則。然後將業務操作用Sentinel API包圍起來。

我們定義的限流閾值是5,這裏一共有20個請求。觸發限流之後,我們的線程停頓1秒,以便度過當前的時間窗口,所以會有3個請求被限流。

運行代碼,我們可以得到以下結果:

14:38:00.463  - 業務操作...1
14:38:00.465  - 業務操作...2
14:38:00.465  - 業務操作...3
14:38:00.465  - 業務操作...4
14:38:00.465  - 業務操作...5
14:38:00.494  - 請求被限流...6
14:38:01.494  - 業務操作...7
14:38:01.494  - 業務操作...8
14:38:01.495  - 業務操作...9
14:38:01.495  - 業務操作...10
14:38:01.495  - 業務操作...11
14:38:01.496  - 請求被限流...12
14:38:02.497  - 業務操作...13
14:38:02.497  - 業務操作...14
14:38:02.497  - 業務操作...15
14:38:02.497  - 業務操作...16
14:38:02.497  - 業務操作...17
14:38:02.497  - 請求被限流...18
14:38:03.498  - 業務操作...19
14:38:03.498  - 業務操作...20

2、熔斷

除了流量控制以外,對調用鏈路中不穩定的資源進行熔斷降級也是保障高可用的重要措施之一。

Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。

那怎麼來衡量資源是否穩定呢?

Sentinel提供了三種方式,平均響應時間、異常比例和異常數。

我們拿平均響應時間爲例,先來定義它的規則。

/**
 * 1秒內的5個請求,平均響應時間大於10ms,接下來的3秒內都會自動熔斷。
 * @param resourceName
 */
public static void loadDegradeRule(String resourceName){
    List<DegradeRule> rules = new ArrayList<>();
    DegradeRule rule = new DegradeRule();
    //資源名稱
    rule.setResource(resourceName);
    //閾值 - 10ms
    rule.setCount(10);
    //熔斷策略 - RT模式
    rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
    //時間窗口 - 3s
    rule.setTimeWindow(3);
    //RT模式下,1秒內連續多少個請求的平均RT超出閾值,纔可以觸發熔斷
    rule.setRtSlowRequestAmount(5);
    rules.add(rule);
    DegradeRuleManager.loadRules(rules);
}

如上代碼,我們定義了熔斷的規則,屬性的含義見註釋內容,然後來看測試用例。

public static void main(String[] args)throws InterruptedException {
    loadDegradeRule(resource);
    while (!stop){
        count.incrementAndGet();
        Entry entry = null;
        try {
            entry = SphU.entry(resource);
            logger.info("業務操作...{}",count.get());
            Thread.sleep(15);
        } catch (BlockException e) {
            if (e instanceof DegradeException){
                logger.error("觸發熔斷機制...{}",count.get());
                Thread.sleep(500);
            }
        } finally {
            if (entry != null) {
                entry.exit();
            }
            if (count.get()>=20){
                stop = true;
            }
        }
    }
    logger.info("----------------------------");
}

在上面的代碼中,我們一共有20個請求。我們讓線程停頓15ms使平均RT超過閾值,也就是超過10ms。

我們定義的規則裏面是1秒內連續5個請求的平均RT超出閾值,就可以觸發熔斷,所以當第6個請求到達時,就會觸發熔斷。

熔斷多久呢?就在3秒的時間窗口。

上面的測試代碼中,在觸發熔斷之後,我們又手動讓線程停頓了 1000ms ,所以每次熔斷的請求會有3個。

是不是這樣,我們運行代碼,看下結果:

10:56:20.022 [main] INFO orderService - 業務操作...1
10:56:20.040 [main] INFO orderService - 業務操作...2
10:56:20.056 [main] INFO orderService - 業務操作...3
10:56:20.072 [main] INFO orderService - 業務操作...4
10:56:20.088 [main] INFO orderService - 業務操作...5
10:56:20.127 [main] ERROR orderService - 觸發熔斷機制...6
10:56:21.128 [main] ERROR orderService - 觸發熔斷機制...7
10:56:22.128 [main] ERROR orderService - 觸發熔斷機制...8
10:56:23.129 [main] INFO orderService - 業務操作...9
10:56:23.145 [main] INFO orderService - 業務操作...10
10:56:23.160 [main] INFO orderService - 業務操作...11
10:56:23.176 [main] INFO orderService - 業務操作...12
10:56:23.192 [main] INFO orderService - 業務操作...13
10:56:23.207 [main] ERROR orderService - 觸發熔斷機制...14
10:56:24.208 [main] ERROR orderService - 觸發熔斷機制...15
10:56:25.208 [main] ERROR orderService - 觸發熔斷機制...16
10:56:26.209 [main] INFO orderService - 業務操作...17
10:56:26.224 [main] INFO orderService - 業務操作...18
10:56:26.240 [main] INFO orderService - 業務操作...19
10:56:26.255 [main] INFO orderService - 業務操作...20
10:56:26.271 [main] INFO orderService - ----------------------------

至此,我們就可以說,Sentinel 能夠正常工作了。

二、系統集成

上面只是一個很簡單的Demo示例,如果我們希望在我們的SpringBoot項目中使用Sentinel,還需要一些工作。

1、Sentinel 控制檯

Sentinel 提供一個輕量級的開源控制檯,它是使用SpringBoot開發的。

它提供機器發現以及健康情況管理、監控(單機和集羣),規則管理和推送的功能。

所以,我們先把這個控制檯運行起來。

第一步,需要在https://github.com/alibaba/Sentinel/releases這個地址,下載最新版本的控制檯 jar 包。

第二步,使用命令啓動控制檯程序,其中 -Dserver.port=9080 用於指定 Sentinel 控制檯端口。

java -Dserver.port=9080 -Dcsp.sentinel.dashboard.server=localhost:9080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

第三步,我們的業務系統引入 Transport 模塊來與 Sentinel 控制檯進行通信。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.7.2</version>
</dependency>

第四步,在我們的業務系統中,設置JVM啓動參數,用來指明Sentinel控制檯的地址。

-Dcsp.sentinel.dashboard.server=127.0.0.1:9080

最後,啓動我們的業務系統,然後打開Sentinel控制檯,如果可以看到機器列表就可以了。


2、定義規則

在定義規則之前,我們需要規劃好資源範圍。

什麼意思呢?比如我們拿一個訂單業務來說,是不是所有的訂單操作都算一個資源?還是拆分開來看,創建訂單算一個資源,訂單查詢算另外一個資源。

所以,我們可以先把希望流控的資源名稱定義出來。

public final class ResourceConstants {
    public static final String ORDER_SERVICE = OrderService.class.getName();
    public static final String ORDER_SERVICE_ORDERS = ORDER_SERVICE+".orders";
    public static final String ORDER_SERVICE_CREATE = ORDER_SERVICE+".create";
}

由於是一個SpringBoot項目,我們可以在系統啓動的時候,來加載流控規則。

@Component
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        initFlowRule(ResourceConstants.ORDER_SERVICE,5);
        initFlowRule(ResourceConstants.ORDER_SERVICE_ORDERS,5);
    }
    public void initFlowRule(String resourceName,int count) {
        FlowRule flowRule = new FlowRule(resourceName)
                .setCount(count)
                .setGrade(RuleConstant.FLOW_GRADE_QPS);
        List<FlowRule> list = new ArrayList<>();
        list.add(flowRule);
        FlowRuleManager.loadRules(list);
    }
}

然後,我們在Controller加入Sentinel的代碼,來達到流控的效果。

@RequestMapping("/getOrders")
public ResponseEntity getOrders(){
    Entry entry = null;
    try {
        entry = SphU.entry(ResourceConstants.ORDER_SERVICE_ORDERS);
        return ResponseEntity.ok(orderService.orders());
    } catch (BlockException e) {
        logger.error("請求被限流...{}",e.getRule().getResource());
        return ResponseEntity.badRequest().body(e.getRule());
    } finally {
        if (entry != null) {
            entry.exit();
        }
    }
}

現在,我們拿JMeter來測試一下,啓動10個線程來請求這個接口。只會通過5個請求,拒絕5個請求。

至此,我們已經可以在SpringBoot項目中簡單使用Sentinel了,不過此時還有兩個很明顯的問題。

  • 在每個需要流控的地方,通過API硬編碼,侵入性太強而且也不方便;
  • 流控規則只保留在內存中,系統重啓就沒了,沒有持久化規則數據。

接下來,我們來解決上述的兩個問題。

三、框架適配

得益於廣泛的開源生態,Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊。我們只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。

我們希望可以對 Web 請求進行流量控制,那麼需要引入Sentinel 提供與 Servlet 的整合。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-web-servlet</artifactId>
    <version>1.7.2</version>
</dependency>

1、Filter配置

因爲是SpringBoot應用,我們通過Configuration進行配置。

@Configuration
public class SentinelFilterConfig {
    @Bean
    public FilterRegistrationBean sentinelFilterRegistration() {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new CommonFilter());
        registration.addUrlPatterns("/*");
        registration.setName("sentinelFilter");
        registration.setOrder(1);
        return registration;
    }
}

在我們自己的業務代碼中,就可以免去Sentinel API部分了。

@RequestMapping("/getOrders")
public ResponseEntity getOrders(){
    return ResponseEntity.ok(orderService.orders());
}

在流控規則不變的情況下,我們拿JMeter啓動10個線程來請求這個接口。同樣的只會通過5個請求,拒絕5個請求。

2、UrlBlockHandler

默認情況下,當請求被限流時會返回默認的提示頁面。

我們可以在代碼中調用WebServletConfig.setBlockPage(blockPage) 方法設定自定義的跳轉 URL,當請求被限流時會自動跳轉至設定好的 URL。

如果不打算讓它跳轉頁面,我們也可以實現 UrlBlockHandler 接口並編寫定製化的限流處理邏輯。

比如像下面這樣,限流或熔斷之後,會向客戶端返回一個異常的HTTP狀態碼和提示信息。

public class SentinelUrlBlockHandler implements UrlBlockHandler {

    public static final String flowMsg = "觸發流控機制~";
    public static final String degradeMsg = "觸發熔斷機制~";
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex){
        logger.error("熔斷限流...{}",ex.getRule());
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        PrintWriter out = response.getWriter();
        if (ex instanceof FlowException){
            out.print(flowMsg);
        }else if (ex instanceof DegradeException){
            out.print(degradeMsg);
        }
        out.flush();
        out.close();
    }
}

然後將其註冊至 WebCallbackManager 中。

WebCallbackManager.setUrlBlockHandler(new SentinelUrlBlockHandler());

3、UrlCleaner

Sentinel Web Filter 會將每個到來的不同的 URL 都作爲不同的資源處理。

比如訂單業務中的,創建訂單、訂單查詢、訂單刪除等等,因爲URL的不同,都會被當作不同的資源。

如果我們希望將這些操作都歸到訂單資源下/order/*,就需要實現 UrlCleaner 接口清洗一下資源。

比如像下面這樣,將資源歸類。比如/order/getOrders和/order/createOrder,都會變成/order/*

public class SentinelUrlClean implements UrlCleaner {
    @Override
    public String clean(String originUrl) {
        if (originUrl == null || originUrl.isEmpty()) {
            return originUrl;
        }
        int lastSlashIndex = originUrl.lastIndexOf("/");
        if (lastSlashIndex >= 0) {
            originUrl = originUrl.substring(0, lastSlashIndex) + "/*";
        }
        return originUrl;
    }
}

然後將其註冊至 WebCallbackManager 中。

WebCallbackManager.setUrlCleaner(new SentinelUrlClean());

當時,更絕對一些,如果整個系統都採用一個資源,那麼這裏只返回一個固定的url也可以。

四、最佳實踐

上面我們說到,現在的Sentinel規則數據都只保留在內存中,沒辦法做到集中管理和推送規則,不具備生產環境可用性。

規則管理及推送,一般有三種方式。

  • 原始模式

將規則推送至客戶端並直接更新到內存中。重啓即消失,不建議在生產環境中使用。

  • Pull 模式

客戶端主動向某個規則管理中心定期輪詢拉取規則,這個規則中心可以是 RDBMS、文件 等。不保證實時性,拉取過於頻繁可能會導致性能問題。

  • Push 模式

規則中心統一推送,客戶端通過註冊監聽器的方式時刻監聽變化,比如使用 Nacos、Zookeeper 等配置中心,有更好的實時性和一致性。生產環境下一般採用 push 模式的數據源。

生產環境下一般更常用的是 push 模式的數據源。對於 push 模式的數據源,如遠程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不應由 Sentinel 客戶端進行,而應該經控制檯統一進行管理,直接進行推送,數據源僅負責獲取配置中心推送的配置並更新到本地。因此推送規則正確做法應該是 ** 配置中心控制檯/Sentinel 控制檯 → 配置中心 → Sentinel 數據源 → Sentinel **,而不是經 Sentinel 數據源推送至配置中心。

接下來我們來實現由Nacos配置中心統一管理數據。

1、啓動Nacos

關於Nacos本文不再多說,下載一個啓動就好了。

2、引入依賴

NacosDataSource,官方已經提供了,我們引入相關依賴即可。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-extension</artifactId>
    <version>1.7.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.7.2</version>
</dependency>

3、從數據源中讀取規則數據

在初始化NacosDataSource的時候,我們要指定Nacos的服務地址,groupId和dataId。

然後根據這些信息連接Nacos,去讀取裏面的數據。並且註冊監聽器,在Nacos配置中心的規則數據發生變化後,通知到客戶端。

說起來可能比較複雜,但是作爲客戶端使用的話,其實比較簡單。我們搞一個類,去連接它就可以了。

@Component
public class DataSourceRuleManager {

    private static final String remoteAddress = "localhost:8848";
    private static final String groupId = "sentinel.group";
    private static final String flowDataId = "flow.rule";

    @PostConstruct
    public void loadFlowRules() {
        FlowConverter converter = new FlowConverter();
        //連接Nacos,讀取配置信息並通過converter將內容轉換爲對象
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource =
                new NacosDataSource<>(remoteAddress,groupId,flowDataId,converter);
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }
    //轉換器 從Nacos配置中心讀取到的數據轉換爲對象
    public class FlowConverter implements Converter {
        @Override
        public Object convert(Object source) {
            return JSON.parseArray(source.toString(),FlowRule.class);
        }
    }
}

配置完之後,我們就可以啓動業務系統了。

4、從Nacos配置中心添加規則數據

現在就可以通過Nacos控制檯,向配置中心添加規則數據了。

有一點需要注意的是,由於我們的轉換器是通過JSON解析FlowRule類型的數組對象,所以配置內容裏面的格式和屬性名稱要對應起來,否則解析會失敗。


通過擴展讀數據源的方式,當我們在Nacos配置中心發佈新的內容後,相應的我們業務系統裏面的規則也會更新,Sentinel控制檯裏面的規則也一樣會同步更新,就實現了規則中心統一推送和持久化。

還有一種方式是直接通過 Sentinel 控制檯 → 配置中心,這樣的話需要修改dashboard的實現,過程雖然不難但比較複雜,由於篇幅有限,本文就不再贅述。感興趣的朋友可以留言交流~

總結

本文簡單介紹了分佈式系統熔斷、限流組件Sentinel的使用。爲了達到生產環境的基本可用,包含了 Sentinel 與 Servlet 的整合和規則中心統一推送和持久化。

本文只是Sentinel生態中的一小部分,更多內容如多種策略的流控和熔斷機制、黑白名單控制、框架適配、實現原理等內容,有時間後續分享~

原創不易,客官們點個贊再走嘛,這將是筆者持續寫作的動力~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章