雙劍合璧 Nacos 結合 Sentinel 實現流量安全控制

Alibaba Sentinel 是一款高性能且輕量級的流量控制、熔斷降級解決方案。是面向分佈式服務架構的高可用流量控制組件。

Sentinel 官網:https://sentinelguard.io/zh-cn/

Github:https://github.com/alibaba/Sentinel


Sentinel 是什麼


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

Sentinel 具有以下特徵:

  • 豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、消息削峯填谷、集羣流量控制、實時熔斷下游不可用應用等。
  • 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制檯中看到接入應用的單臺機器秒級數據,甚至 500 臺以下規模的集羣的彙總運行情況。
  • 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
  • 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定製邏輯。例如定製規則管理、適配動態數據源等。

Sentinel 主要特徵

Sentinel 開源生態

Sentinel 目前已經針對 Servlet、Dubbo、Spring Boot/Spring Cloud、gRPC 等進行了適配,用戶只需引入相應依賴並進行簡單配置即可非常方便地享受 Sentinel 的高可用流量防護能力。Sentinel 還爲 Service Mesh 提供了集羣流量防護的能力。未來 Sentinel 還會對更多常用框架進行適配。

Sentinel 分爲兩個部分:

  • 核心庫(Java 客戶端)不依賴任何框架/庫,能夠運行於所有 Java 運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持。
  • 控制檯(Dashboard)基於 Spring Boot 開發,打包後可以直接運行,不需要額外的 Tomcat 等應用容器。

Sentinel 的歷史

  • 2012 年,Sentinel 誕生,主要功能爲入口流量控制。
  • 2013-2017 年,Sentinel 在阿里巴巴集團內部迅速發展,成爲基礎技術模塊,覆蓋了所有的核心場景。Sentinel 也因此積累了大量的流量歸整場景以及生產實踐。
  • 2018 年,Sentinel 開源,並持續演進。
  • 2019 年,Sentinel 朝着多語言擴展的方向不斷探索,推出 C++ 原生版本,同時針對 Service Mesh 場景也推出了 Envoy 集羣流量控制支持,以解決 Service Mesh 架構下多語言限流的問題。
  • 2020 年,推出 Sentinel Go 版本,繼續朝着雲原生方向演進。

Sentinel 核心

Sentinel 的使用可以分爲兩個部分:

  • 核心庫(Java 客戶端):不依賴任何框架/庫,能夠運行於 Java 7 及以上的版本的運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持(見 主流框架適配)。
  • 控制檯(Dashboard):控制檯主要負責管理推送規則、監控、集羣限流分配管理、機器發現等。

Sentinel 控制檯

Sentinel 提供一個輕量級的開源控制檯,它提供機器發現以及健康情況管理、監控(單機和集羣),規則管理和推送的功能。

官網文檔:https://github.com/alibaba/Sentinel/wiki/控制檯

獲取控制檯

您可以從 release 頁面 下載最新版本的控制檯 jar 包。

您也可以從最新版本的源碼自行構建 Sentinel 控制檯:

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

啓動控制檯

啓動命令如下,本文使用的是目前最新 1.7.2 版本:

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

注意:啓動 Sentinel 控制檯需要 JDK 版本爲 1.8 及以上版本。

其中 -Dserver.port=8080 用於指定 Sentinel 控制檯端口爲 8080

從 Sentinel 1.6.0 起,Sentinel 控制檯引入基本的登錄功能,默認用戶名和密碼都是 sentinel。可以參考 鑑權模塊文檔 配置用戶名和密碼。

注:若您的應用爲 Spring Boot 或 Spring Cloud 應用,您可以通過 Spring 配置文件來指定配置,詳情請參考 Spring Cloud Alibaba Sentinel 文檔

爲了方便啓動,可以編寫一個啓動腳本 run.bat

java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.2.jar
pause

訪問

訪問:http://localhost:8080/

輸入默認用戶名和密碼 sentinel 點擊登錄。至此控制檯就安裝完成了。

環境準備

sentinel-demo 聚合工程。SpringBoot 2.3.0.RELEASESpring Cloud Hoxton.SR4

  • Nacos 註冊中心
  • product-service:商品服務,提供了 /product/{id} 接口
  • order-service-rest:訂單服務,基於 Ribbon 通過 RestTemplate 調用商品服務
  • order-server-feign:訂單服務,基於 Feign 通過聲明式服務調用商品服務

客戶端接入控制檯

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

  • 添加依賴
  • 定義資源
  • 定義規則

先把可能需要保護的資源定義好,之後再配置規則。也可以理解爲,只要有了資源,我們就可以在任何時候靈活地定義各種流量控制規則。在編碼的時候,只需要考慮這個代碼是否需要保護,如果需要保護,就將之定義爲一個資源。

由於我們的項目是 Spring Cloud 項目,所以可以藉助官方文檔來進行學習。

Spring 官網文檔:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

Github 文檔:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel

添加依賴

父工程需要添加如下依賴:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子工程需要添加如下依賴:

<!-- spring cloud alibaba sentinel 依賴 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

配置文件

客戶端需要啓動 Transport 模塊來與 Sentinel 控制檯進行通信。

order-service-rest 的 application.yml

spring:
  cloud:
    # 配置 Sentinel
    sentinel:
      transport:
        port: 8719
        dashboard: localhost:8080

這裏的 spring.cloud.sentinel.transport.port 端口配置會在應用對應的機器上啓動一個 Http Server,該 Server 會與 Sentinel 控制檯做交互。比如 Sentinel 控制檯添加了一個限流規則,會把規則數據 push 給這個 Http Server 接收,Http Server 再將規則註冊到 Sentinel 中。

初始化客戶端

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

簡單的理解就是:訪問一次客戶端,Sentinel 即可完成客戶端初始化操作,並持續向控制檯發送心跳包。

訪問

多次訪問:http://localhost:9090/order/1 然後查看控制檯實時監控結果如下:

定義資源

資源 是 Sentinel 中的核心概念之一。我們說的資源,可以是任何東西,服務,服務裏的方法,甚至是一段代碼。最常用的資源是我們代碼中的 Java 方法。Sentinel 提供了 @SentinelResource 註解用於定義資源,並提供了 AspectJ 的擴展用於自動定義資源、處理 BlockException 等。

只要通過 Sentinel API 定義的代碼,就是資源,能夠被 Sentinel 保護起來。大部分情況下,可以使用方法簽名,URL,甚至服務名稱作爲資源名來標示資源。

官網文檔:https://github.com/alibaba/Sentinel/wiki/如何使用#定義資源

註解支持

官網文檔:https://github.com/alibaba/Sentinel/wiki/註解支持

OrderServiceImpl.java

package com.example.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ProductService productService;

    /**
     * 根據主鍵和訂單編號查詢訂單
     *
     * @param id
     * @param orderNo
     * @return
     */
    @Override
    @SentinelResource(value = "selectOrderByIdAndOrderNo",
            blockHandler = "selectOrderByIdAndOrderNoBlockHandler",
            fallback = "selectOrderByIdAndOrderNoFallback")
    public Order selectOrderByIdAndOrderNo(Integer id, String orderNo) {
        return new Order(id, orderNo, "中國", 2666D,
                Arrays.asList(productService.selectProductById(1)));
    }

    // 服務流量控制處理,參數最後多一個 BlockException,其餘與原函數一致。
    public Order selectOrderByIdAndOrderNoBlockHandler(Integer id, String orderNo,
                                                       BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return new Order(id, "服務流量控制處理-託底數據", "中國", 2666D,
                Arrays.asList(productService.selectProductById(1)));
    }

    // 服務熔斷降級處理,函數簽名與原函數一致或加一個 Throwable 類型的參數
    public Order selectOrderByIdAndOrderNoFallback(Integer id, String orderNo,
                                                   Throwable throwable) {
        System.out.println("order-service 服務的 selectOrderById 方法出現異常,異常信息如下:"
                + throwable);
        return new Order(id, "服務熔斷降級處理-託底數據", "中國", 2666D,
                Arrays.asList(productService.selectProductById(1)));
    }

}

ProductServiceImpl.java

package com.example.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * 商品管理
 */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @SentinelResource(value = "selectProductById",
            blockHandler = "selectProductByIdBlockHandler", fallback = "selectProductByIdFallback")
    @Override
    public Product selectProductById(Integer id) {
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

    // 服務流量控制處理,參數最後多一個 BlockException,其餘與原函數一致。
    public Product selectProductByIdBlockHandler(Integer id, BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return new Product(id, "服務流量控制處理-託底數據", 1, 2666D);
    }

    // 服務熔斷降級處理,函數簽名與原函數一致或加一個 Throwable 類型的參數
    public Product selectProductByIdFallback(Integer id, Throwable throwable) {
        System.out.println("product-service 服務的 selectProductById 方法出現異常,異常信息如下:"
                + throwable);
        return new Product(id, "服務熔斷降級處理-託底數據", 1, 2666D);
    }

}

注意:註解方式埋點不支持 private 方法。

@SentinelResource 用於定義資源,並提供可選的異常處理和 fallback 配置項。 @SentinelResource 註解包含以下屬性:

  • value:資源名稱,必需項(不能爲空)
  • entryType:entry 類型,可選項(默認爲 EntryType.OUT
  • blockHandler / blockHandlerClass: blockHandler對應處理 BlockException 的函數名稱,可選項。blockHandler 函數訪問範圍需要是 public,返回類型需要與原方法相匹配,參數類型需要和原方法相匹配並且最後加一個額外的參數,類型爲 BlockException。blockHandler 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 blockHandlerClass 爲對應的類的 Class 對象,注意對應的函數必需爲 static 函數,否則無法解析。
  • fallback:fallback 函數名稱,可選項,用於在拋出異常的時候提供 fallback 處理邏輯。fallback 函數可以針對所有類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。fallback 函數簽名和位置要求:
    • 返回值類型必須與原函數返回值類型一致;
    • 方法參數列表需要和原函數一致,或者可以額外多一個 Throwable 類型的參數用於接收對應的異常。
    • fallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass 爲對應的類的 Class 對象,注意對應的函數必需爲 static 函數,否則無法解析。
  • defaultFallback(since 1.6.0):默認的 fallback 函數名稱,可選項,通常用於通用的 fallback 邏輯(即可以用於很多服務或方法)。默認 fallback 函數可以針對所有類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,則只有 fallback 會生效。defaultFallback 函數簽名要求:
    • 返回值類型必須與原函數返回值類型一致;
    • 方法參數列表需要爲空,或者可以額外多一個 Throwable 類型的參數用於接收對應的異常。
    • defaultFallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass 爲對應的類的 Class 對象,注意對應的函數必需爲 static 函數,否則無法解析。
  • exceptionsToIgnore(since 1.6.0):用於指定哪些異常被排除掉,不會計入異常統計中,也不會進入 fallback 邏輯中,而是會原樣拋出。

注:1.6.0 之前的版本 fallback 函數只針對降級異常(DegradeException)進行處理,不能針對業務異常進行處理

特別地,若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出 BlockException 時只會進入 blockHandler 處理邏輯。若未配置 blockHandlerfallbackdefaultFallback,則被限流降級時會將 BlockException 直接拋出(若方法本身未定義 throws BlockException 則會被 JVM 包裝一層 UndeclaredThrowableException)。

從 1.4.0 版本開始,註解方式定義資源支持自動統計業務異常,無需手動調用 Tracer.trace(ex) 來記錄業務異常。Sentinel 1.4.0 以前的版本需要自行調用 Tracer.trace(ex) 來記錄業務異常。

定義規則

Sentinel 的所有規則都可以在內存態中動態地查詢及修改,修改之後立即生效。同時 Sentinel 也提供相關 API,供您來定製自己的規則策略。

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

官網文檔:https://github.com/alibaba/Sentinel/wiki/如何使用#規則的種類

流量控制規則

添加流量控制規則

選擇 簇點鏈路 找到定義好的資源 selectProductById 並點擊對應的規則按鈕進行設置。

比如我們設置一個流量控制規則,定義資源訪問的 QPS 爲 1(每秒能處理查詢數目)。

測試

快速刷新頁面多次訪問:http://localhost:9090/order/idAndOrderNo?id=1&orderNo=order-001 結果如下:

熔斷降級規則

模擬服務出錯

修改 order-service-rest 項目中的核心代碼,模擬服務出錯。

package com.example.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * 商品管理
 */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @SentinelResource(value = "selectProductById",
            blockHandler = "selectProductByIdBlockHandler", fallback = "selectProductByIdFallback")
    @Override
    public Product selectProductById(Integer id, String productName) {
        // 模擬查詢主鍵爲 1 的商品信息會導致異常
        if (1 == id)
            throw new RuntimeException("查詢主鍵爲 1 的商品信息導致異常");
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

    // 服務流量控制處理,參數最後多一個 BlockException,其餘與原函數一致。
    public Product selectProductByIdBlockHandler(Integer id, BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return new Product(id, "服務流量控制處理-託底數據", 1, 2666D);
    }

    // 服務熔斷降級處理,函數簽名與原函數一致或加一個 Throwable 類型的參數
    public Product selectProductByIdFallback(Integer id, Throwable throwable) {
        System.out.println("product-service 服務的 selectProductById 方法出現異常,異常信息如下:"
                + throwable);
        return new Product(id, "服務熔斷降級處理-託底數據", 1, 2666D);
    }

}

添加熔斷降級規則

熔斷降級規則支持相應時間、異常比例、異常數三種方式。

測試

訪問:http://localhost:9090/order/idAndOrderNo?id=1&orderNo=order-001 結果如下:

熱點參數規則

熱點參數規則是一種更細粒度的流控規則,它允許將規則具體到參數上。比如 selectOrderByIdAndOrderNo 方法有兩個參數,我們對第一個參數進行限流,對第二個參數不限流。

添加熱點參數規則

選擇 簇點鏈路 找到定義好的資源 selectOrderByIdAndOrderNo 並點擊對應的規則按鈕進行設置。

設置熱點參數規則,定義對資源的第一個參數的 QPS 爲 1(每秒能處理查詢數目)。

測試

分別用兩個參數訪問,會發現只對第一個參數限流了。

快速刷新頁面多次訪問:http://localhost:9090/order/idAndOrderNo?id=1 被限流。

快速刷新頁面多次訪問:http://localhost:9090/order/idAndOrderNo?orderNo=order-001 正常訪問。

授權規則

很多時候,我們需要根據調用來源來判斷該次請求是否被允許,這時候可以使用 Sentinel 的來源訪問控制的功能。來源訪問控制根據資源的請求來源(origin)限制資源是否通過。

Sentinel 提供了 RequestOriginParser 接口來處理來源。一旦 Sentinel 保護的接口資源被訪問,Sentinel 就會調用 RequestOriginParser 的實現類去解析訪問來源。

自定義來源處理規則

package com.example.sentinel;

import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 自定義來源處理規則
 */
@Component
public class MyRequestOriginParser implements RequestOriginParser {

    @Override
    public String parseOrigin(HttpServletRequest request) {
        return request.getParameter("userName");
    }

}

新增授權規則

下圖配置的意思是資源 selectOrderByIdAndOrderNo 只有 userName=zhangsan 的用戶無法訪問(黑名單)

測試

快速刷新頁面多次訪問:http://localhost:9090/order/idAndOrderNo?id=1&userName=zhangsan 被限流。

快速刷新頁面多次訪問:http://localhost:9090/order/idAndOrderNo?id=1&userName=lisi 正常訪問。

系統保護規則

系統保護規則是從應用級別的入口流量進行控制,從單臺機器的總體 LOAD、RT、線程數、入口 QPS 和 CPU 使用率五個維度監控應用數據,讓系統儘可能跑在最大吞吐量的同時保證系統整體的穩定性。

系統保護規則是應用整體維度的,而不是資源維度的,並且僅對入口流量(進入應用的流量)生效。

  • Load(僅對 Linux/Unix-like 機器生效):當系統 load 超過閾值,且系統當前的併發線程數超過系統容量時纔會觸發系統保護。系統容量由系統的 maxQps * minRt 計算得出。設定參考值一般是 CPU cores * 2.5。
  • RT:當單臺機器上所有入口流量的平均 RT 達到閾值即觸發系統保護,單位是毫秒。
  • 線程數:當單臺機器上所有入口流量的併發線程數達到閾值即觸發系統保護。
  • 入口 QPS:當單臺機器上所有入口流量的 QPS 達到閾值即觸發系統保護。
  • CPU使用率:當單臺機器上所有入口流量的 CPU 使用率達到閾值即觸發系統保護。

動態規則擴展

官網文檔:

SentinelProperties 內部提供了 TreeMap 類型的 datasource 屬性用於配置數據源信息。支持:

  • 文件配置規則
  • Nacos 配置規則
  • ZooKeeper 配置規則
  • Apollo 配置規則
  • Redis 配置規則

文件配置規則

Sentinel 支持通過本地文件加載規則配置,使用方式如下(限流規則作爲演示):

spring:
  cloud:
    # 配置 Sentinel
    sentinel:
      datasource:
        ds1:
          file:
            file: classpath:flowRule.json
            data-type: json
            rule-type: flow

flowRule.json 對應 com.alibaba.csp.sentinel.slots.block.RuleConstant 各屬性。

[
  {
    "resource": "selectProductList",
    "count": 1,
    "grade": 1,
    "limitApp": "default",
    "strategy": 0,
    "controlBehavior": 0
  }
]

重要屬性:

Field 說明 默認值
resource 資源名,資源名是限流規則的作用對象
count 限流閾值
grade 限流閾值類型,QPS 模式(1)或併發線程數模式(0) QPS 模式
limitApp 流控針對的調用來源 default,代表不區分調用來源
strategy 調用關係限流策略:直接、鏈路、關聯 根據資源本身(直接)
controlBehavior 流控效果(直接拒絕 / 排隊等待 / 慢啓動模式),不支持按調用關係限流 直接拒絕
clusterMode 是否集羣限流

訪問客戶端以後,刷新控制檯,查看流控規則如下:

RestTemplate 支持

Spring Cloud Alibaba Sentinel 支持對 RestTemplate 調用的服務進行服務保護。需要在構造 RestTemplate Bean 時添加 @SentinelRestTemplate 註解。

啓動類

OrderServiceRestApplication.java

package com.example;

import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate;
import com.example.exception.ExceptionUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class OrderServiceRestApplication {

    @Bean
    @LoadBalanced
    @SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class,
            fallback = "fallback", fallbackClass = ExceptionUtil.class)
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceRestApplication.class, args);
    }

}

服務熔斷處理類

ExceptionUtil.java 必須使用靜態方法。

package com.example.exception;

import com.alibaba.cloud.sentinel.rest.SentinelClientHttpResponse;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.example.pojo.Product;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;

public class ExceptionUtil {

    // 服務流量控制處理
    public static ClientHttpResponse handleException(HttpRequest request,
                                                     byte[] body,
                                                     ClientHttpRequestExecution execution,
                                                     BlockException exception) {
        exception.printStackTrace();
        return new SentinelClientHttpResponse(
                JSON.toJSONString(new Product(1, "服務流量控制處理-託底數據", 1, 2666D)));
    }

    // 服務熔斷降級處理
    public static ClientHttpResponse fallback(HttpRequest request,
                                                    byte[] body,
                                                    ClientHttpRequestExecution execution,
                                                    BlockException exception) {
        exception.printStackTrace();
        return new SentinelClientHttpResponse(
                JSON.toJSONString(new Product(1, "服務熔斷降級處理-託底數據", 1, 2666D)));
    }

}

訪問

控制檯設置流量控制規則,定義資源訪問的 QPS 爲 1(每秒能處理查詢數目)。

快速刷新頁面多次訪問:http://localhost:9090/order/1 結果如下:

OpenFeign 支持

添加依賴

<!-- spring cloud alibaba sentinel 依賴 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- spring cloud openfeign 依賴 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

開啓 Sentinel

server:
  port: 9091 # 端口

spring:
  application:
    name: order-service-feign # 應用名稱
  cloud:
    # 配置 Nacos 註冊中心
    nacos:
      discovery:
        enabled: true # 如果不想使用 Nacos 進行服務註冊和發現,設置爲 false 即可
        server-addr: 127.0.0.1:8848 # Nacos 服務器地址
    # 配置 Sentinel
    sentinel:
      transport:
        port: 8719
        dashboard: localhost:8080

# feign 開啓 sentinel 支持
feign:
  sentinel:
    enabled: true

熔斷降級

ProductServiceFallback.java

package com.example.fallback;

import com.example.pojo.Product;
import com.example.service.ProductService;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 服務熔斷降級處理可以捕獲異常
 */
@Slf4j
@Component
public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> {

    @Override
    public ProductService create(Throwable throwable) {
        return new ProductService() {
            @Override
            public Product selectProductById(Integer id) {
                // 獲取日誌,在需要捕獲異常的方法中進行處理
                log.error("product-service 服務的 selectProductById 方法出現異常,異常信息如下:"
                        + throwable);
                return new Product(id, "託底數據", 1, 2666D);
            }
        };
    }

}

消費服務

ProductService.java

package com.example.service;

import com.example.fallback.ProductServiceFallbackFactory;
import com.example.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

// 聲明需要調用的服務
@FeignClient(value = "product-service", fallbackFactory = ProductServiceFallbackFactory.class)
public interface ProductService {

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @GetMapping("/product/{id}")
    Product selectProductById(@PathVariable("id") Integer id);

}

OrderServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ProductService productService;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order selectOrderById(Integer id) {
        return new Order(id, "order-001", "中國", 2666D,
                Arrays.asList(productService.selectProductById(1)));
    }

}

控制層

package com.example.controller;

import com.example.pojo.Order;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Order selectOrderById(@PathVariable("id") Integer id) {
        return orderService.selectOrderById(id);
    }

}

啓動類

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

// 開啓 FeignClients 註解
@EnableFeignClients
// 開啓 @EnableDiscoveryClient 註解,當前版本默認會開啓該註解
//@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceFeignApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceFeignApplication.class, args);
    }

}

測試

控制檯信息如下:

添加流量控制規則,定義資源訪問的 QPS 爲 1(每秒能處理查詢數目)。

快速刷新頁面多次訪問:http://localhost:9091/order/1 結果如下:

或者關閉服務提供者,訪問:http://localhost:9091/order/1 結果如下:

Gateway 支持

Sentinel 支持對 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 進行限流。

官網文檔:

創建項目

創建 gateway-server-sentinel 項目。

添加依賴

單獨使用添加 sentinel-spring-cloud-gateway-adapter 依賴即可。

若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依賴來讓 spring-cloud-alibaba-sentinel-gateway 模塊裏的 Spring Cloud Gateway 自動化配置類生效。

同時請將 spring.cloud.sentinel.filter.enabled 配置項置爲 false(若在網關流控控制檯上看到了 URL 資源,就是此配置項沒有置爲 false)。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- 繼承父依賴 -->
    <parent>
        <artifactId>gateway-demo</artifactId>
        <groupId>com.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway-server-sentinel</artifactId>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- spring cloud gateway 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- spring cloud alibaba nacos discovery 依賴 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 單獨使用 -->
        <!-- sentinel gateway adapter 依賴 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
        </dependency>
        <!-- 和 Sentinel Starter 配合使用 -->
        <!--
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        -->
    </dependencies>

</project>

配置文件

server:
  port: 9001 # 端口

spring:
  application:
    name: gateway-server-sentinel # 應用名稱
  cloud:
    sentinel:
      filter:
        enabled: false
    gateway:
      discovery:
        locator:
          # 是否與服務發現組件進行結合,通過 serviceId 轉發到具體服務實例。
          enabled: true                  # 是否開啓基於服務發現的路由規則
          lower-case-service-id: true    # 是否將服務名稱轉小寫
      # 路由規則
      routes:
        - id: order-service           # 路由 ID,唯一
          uri: lb://order-service     # 目標 URI,lb:// 根據服務名稱從註冊中心獲取服務請求地址
          predicates:                 # 斷言(判斷條件)
            # 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
            - Path=/order/**

限流規則配置類

使用時只需注入對應的 SentinelGatewayFilter 實例以及 SentinelGatewayBlockExceptionHandler 實例即可。

GatewayConfiguration.java

package com.example.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 限流規則配置類
 */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 構造器
     *
     * @param viewResolversProvider
     * @param serverCodecConfigurer
     */
    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 限流異常處理器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 限流過濾器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 加載網關限流規則
        initGatewayRules();
    }

    /**
     * 網關限流規則
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:資源名稱,可以是網關中的 route 名稱或者用戶自定義的 API 分組名稱
            count:限流閾值
            intervalSec:統計時間窗口,單位是秒,默認是 1 秒
         */
        rules.add(new GatewayFlowRule("order-service")
                .setCount(3) // 限流閾值
                .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
        // 加載網關限流規則
        GatewayRuleManager.loadRules(rules);
    }

}

啓動類

GatewayServerSentinelApplication.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// 開啓 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,默認會開啓該註解
//@EnableEurekaClient
@SpringBootApplication
public class GatewayServerSentinelApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayServerSentinelApplication.class, args);
    }

}

訪問

多次訪問:http://localhost:9001/order/1 結果如下:

接口 BlockRequestHandler 的默認實現爲 DefaultBlockRequestHandler,當觸發限流時會返回默認的錯誤信息:Blocked by Sentinel: FlowException。我們可以通過 GatewayCallbackManager 定製異常提示信息。

自定義異常提示

GatewayCallbackManagersetBlockHandler 註冊函數用於實現自定義的邏輯,處理被限流的請求。

package com.example.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * 限流規則配置類
 */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 構造器
     *
     * @param viewResolversProvider
     * @param serverCodecConfigurer
     */
    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 限流異常處理器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 限流過濾器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 加載網關限流規則
        initGatewayRules();
        // 加載自定義限流異常處理器
        initBlockHandler();
    }

    /**
     * 網關限流規則
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:資源名稱,可以是網關中的 route 名稱或者用戶自定義的 API 分組名稱
            count:限流閾值
            intervalSec:統計時間窗口,單位是秒,默認是 1 秒
         */
        rules.add(new GatewayFlowRule("order-service")
                .setCount(3) // 限流閾值
                .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
        // 加載網關限流規則
        GatewayRuleManager.loadRules(rules);
    }

    /**
     * 自定義限流異常處理器
     */
    private void initBlockHandler() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map<String, String> result = new HashMap<>();
                result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
                result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                result.put("route", "order-service");
                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(result));
            }
        };

        // 加載自定義限流異常處理器
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }

}

訪問

多次訪問:http://localhost:9001/order/1 結果如下:

分組限流

package com.example.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * 限流規則配置類
 */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 構造器
     *
     * @param viewResolversProvider
     * @param serverCodecConfigurer
     */
    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 限流異常處理器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 限流過濾器
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 加載網關限流規則
        initGatewayRules();
        // 加載自定義限流異常處理器
        initBlockHandler();
    }

    /**
     * 網關限流規則
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:資源名稱,可以是網關中的 route 名稱或者用戶自定義的 API 分組名稱
            count:限流閾值
            intervalSec:統計時間窗口,單位是秒,默認是 1 秒
         */
        // rules.add(new GatewayFlowRule("order-service")
        //         .setCount(3) // 限流閾值
        //         .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
        // --------------------限流分組----------start----------
        rules.add(new GatewayFlowRule("product-api")
                .setCount(3) // 限流閾值
                .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
        rules.add(new GatewayFlowRule("order-api")
                .setCount(5) // 限流閾值
                .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
        // --------------------限流分組-----------end-----------
        // 加載網關限流規則
        GatewayRuleManager.loadRules(rules);
        // 加載限流分組
        initCustomizedApis();
    }

    /**
     * 自定義限流異常處理器
     */
    private void initBlockHandler() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map<String, String> result = new HashMap<>();
                result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
                result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                result.put("route", "order-service");
                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(result));
            }
        };

        // 加載自定義限流異常處理器
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }

    /**
     * 限流分組
     */
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        // product-api 組
        ApiDefinition api1 = new ApiDefinition("product-api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    // 匹配 /product-service/product 以及其子路徑的所有請求
                    add(new ApiPathPredicateItem().setPattern("/product-service/product/**")
                            .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});

        // order-api 組
        ApiDefinition api2 = new ApiDefinition("order-api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    // 只匹配 /order-service/order/index
                    add(new ApiPathPredicateItem().setPattern("/order-service/order/index"));
                }});
        definitions.add(api1);
        definitions.add(api2);
        // 加載限流分組
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

}

訪問

訪問:http://localhost:9001/product-service/product/1 觸發限流

訪問:http://localhost:9001/order-service/order/index 觸發限流

訪問:http://localhost:9001/order-service/order/1 不會觸發限流

至此 Sentinel 服務哨兵知識點就講解結束了。

本文采用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議

大家可以通過 分類 查看更多關於 Spring Cloud 的文章。


🤗 您的點贊轉發是對我最大的支持。

📢 掃碼關注 哈嘍沃德先生「文檔 + 視頻」每篇文章都配有專門視頻講解,學習更輕鬆噢 ~

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