天天看點

Alibaba Sentinel 學習筆記Sentinel

Sentinel

推薦使用 GitCodeTree 插件浏覽網頁項目

第一章:Sentinel 簡介

Sentinel 是面向分布式服務架構的流量控制元件,主要以流量為切入點,從流量控制、熔斷降級、系統自适應保護等多個次元來保障微服務的穩定性。

1.1 Sentinel 基本概念

1.1.1 資源

​ 資源是 Sentinel 的關鍵概念。它可以是 Java 應用程式中的任何内容,例如,由應用程式提供的服務,或由應用程式調用的其它應用提供的服務,甚至可以是一段代碼。隻要通過 Sentinel API 定義的代碼,就是資源,能夠被 Sentinel 保護起來。大部分情況下,可以使用方法簽名,URL,服務名稱作為資源名來标示資源。

1.1.2 規則

​ 圍繞資源的實時狀态設定的規則,可以包括流量控制規則、熔斷降級規則以及系統保護規則。所有規則可以動态實時調整。

1.2 Sentinel 功能和設計理念

1.2.1 流量控制

流量控制在網絡傳輸中是一個常用的概念,它用于調整網絡包的發送資料。Sentinel 作為一個調配器,可以根據系統的處理能力把随機的請求調整成合适的形狀。
Alibaba Sentinel 學習筆記Sentinel

流量控制調整角度

  • 資源的調用關系,例如資源的調用鍊路,資源和資源之間的關系;
  • 運作名額,例如 QPS、線程池、系統負載等;
  • 控制的效果,例如直接限流、冷啟動、排隊等。

1.2.2 熔斷降級

由于調用關系的複雜性,如果調用鍊路中的某個資源出現了不穩定,最終會導緻請求發生堆積。是以

Sentinel

同時也被設計用來降低調用鍊路中的不穩定資源。

Sentinel 和 Hystrix 的原則是一緻的: 當調用鍊路中某個資源出現不穩定,例如,表現為 timeout,異常比例升高的時候,則對這個資源的調用進行限制,并讓請求快速失敗,避免影響到其它的資源,最終産生雪崩的效果。

熔斷降級設計理念
  • Hystrix: 采用線程池(benefits-of-thread-pools)的方式,來對依賴進行了隔離
    • 優點:資源和資源之間做到了最徹底的隔離
    • 缺點:增加了線程切換的成本,需要預先給各個資源做線程池大小的配置設定
  • Sentinel:
    • 通過并發線程數進行限制
      • Sentinel 通過限制資源并發線程的數量,來減少不穩定資源對其他資源的影響。減少線程切換的損耗,也不需要您預先配置設定線程池的大小。
        當某個資源出現不穩定的情況下,例如響應時間變長,對資源的直接影響就是會造成線程數的逐漸堆積。當線程數在特定資源上堆積到一定的數量之後,對該資源的新請求就會被拒絕。堆積的線程完成任務後才開始繼續接收請求。
    • 通過響應時間對資源進行降級
      • Sentinel 可以通過響應時間來快速降級不穩定的資源
        當依賴的資源出現響應時間過長後,所有對該資源的通路都會被直接拒絕,直到過了指定的時間視窗之後才重新恢複。

1.2.3 系統負載保護

為防止雪崩事件發生,Sentinel 提供了系統次元的系統自适應保護。

​ 當系統負載較高的時候,如果還持續讓請求進入,可能會導緻系統崩潰,無法響應。在叢集環境下,網絡負載均衡會把本應這台機器承載的流量轉發到其它的機器上去。如果這個時候其它的機器也處在一個邊緣狀态的時候,這個增加的流量就會導緻這台機器也崩潰,最後導緻整個叢集不可用。這個時候,Sentinel 能讓系統的入口流量和系統的負載達到一個平衡,保證系統在能力範圍之内處理最多的請求。

1.2.4. Sentinel 工作機制

  • 對主流架構提供适配或者顯示的 API,來定義需要保護的資源,并提供設施對資源進行實時統計和調用鍊路分析。
  • 根據預設的規則,結合對資源的實時統計資訊,對流量進行控制。同時,Sentinel 提供開放的接口,友善您定義及改變規則。
  • Sentinel 提供實時的監控系統,友善您快速了解目前系統的狀态。

第二章:Sentinel 開發環境的搭建

2.1. Server 端 (單機)

2.1.1 Docker 搭建

擷取最新版 sentinel 的 jar 包
sudo mkdir -p /usr/local/soft/sentinel/
cd /usr/local/soft/sentinel/
wget https://github.com/alibaba/Sentinel/releases/download/v1.8.0/sentinel-dashboard-1.8.0.jar
           
編寫 Dockerfile 檔案
# 指定 java 版本,最低 JDK 1.8
FROM java:8
# 挂載docker卷
VOLUME /tmp
# 前者主要操作的是 jar 包 後者定義的jar包名稱
ADD sentinel-dashboard-1.8.0.jar sentinel-dashboard-1.8.0.jar
# 定義時區參數
ENV TZ=Asia/Shanghai
# 設定時區
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone
# 指定暴露端口
EXPOSE 8858
# username 和 password 寫自己的賬戶密碼,最後一個雙引号内需要和上面自定義jar包名一緻
ENTRYPOINT ["java","-Dserver.port=8858","-Dsentinel.dashboard.auth.username=root","-Dsentinel.dashboard.auth.password=123456","-Dcsp.sentinel.dashboard.server=localhost:8858","Dproject.name=sentinel-dashboard","-jar","/sentinel-dashboard-1.8.0.jar"]
           

具體啟動參數配置,參考 sentinel 啟動配置項

# docker建構 docker build -f Dockerfile -t 定義鏡像名稱:版本名 .
docker build -f Dockerfile -t xxx-sentinel:1.0 .
# 檢視 docker 鏡像是中存在的 sentinel 鏡像
docker images
           
使用 docker-compose 運作 docker sentinel 鏡像

docker-compose-sentinel.yml

version: '3'
services:
  sentinel-dashboard:
    image: xxx-sentinel:1.0
    container_name: sentinel-dashboard
    restart: always
    ports:
      - "8858:8858"
           

運作 sentinel 鏡像

docker-compose -f docker-compose-sentinel.yml up -d
           

2.1.2 Zip 搭建

擷取資源
  • 擷取最新 Sentinel Jar 資源包
  • 或者使用 git clone 源碼進行

    mvn clean package

    打包
啟動
# JDK 版本在 JDK 1.8 及以上
# 預設使用者名和密碼都是 sentinel
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
           
驗證

浏覽器輸入

http://localhost:8080/#/login

打開 Sentinel 控制台界面。輸入

sentinel

賬号和密碼進行登入

Alibaba Sentinel 學習筆記Sentinel

2.1.3 啟動參數配置

觸發用戶端初始化

Sentinel 采用懶附加元件進行應用監控。

是以 確定用戶端有通路量,Sentinel 會在用戶端首次調用的時候進行初始化,開始向控制台發送心跳包。

監控

"簇點鍊路"中顯示剛剛調用的資源(單機)

​ 簇點鍊路(單機調用鍊路)頁面實時的去拉取指定用戶端資源的運作情況。它一共提供兩種展示模式:一種用樹狀結構展示資源的調用鍊路,另外一種則不區分調用鍊路展示資源的運作情況。

Alibaba Sentinel 學習筆記Sentinel
Alibaba Sentinel 學習筆記Sentinel

注意: 簇點監控是記憶體态的資訊,它僅展示啟動後調用過的資源。

"實時監控"彙總資源資訊(叢集聚合)

​ 同時,同一個服務下的所有機器的簇點資訊會被彙總,并且秒級地展示在"實時監控"下。

Alibaba Sentinel 學習筆記Sentinel

注意:

  • 實時監控僅存儲 5 分鐘以内的資料,如果需要持久化,需要通過調用實時監控接口來定制。
  • 確定 Sentinel 控制台所在的機器時間與自己應用的機器時間保持一緻,否則會導緻拉不到實時的監控資料。
2.1.4 規則管理及推送
Sentinel 控制台同時提供簡單的規則管理以及推送的功能。規則推送分為 3 種模式,包括 “原始模式”、“Pull 模式” 和"Push 模式"。

規則管理

​ 在控制台通過接入端暴露的 HTTP API 來查詢規則。

規則推送

​ 目前控制台的規則推送也是通過 規則查詢更改 HTTP API 來更改規則。這也意味着這些規則僅在記憶體态生效,應用重新開機之後,該規則會丢失。

以上是原始模式。當了解了原始模式之後,我們非常鼓勵您通過 動态規則 并結合各種外部存儲來定制自己的規則源。我們推薦通過動态配置源的控制台來進行規則寫入和推送,而不是通過 Sentinel 用戶端直接寫入到動态配置源中。在生産環境中,我們推薦 push 模式,具體可以參考:在生産環境使用 Sentinel。

注:若要使用叢集流控功能,則必須對接動态規則源,否則無法正常使用。

Sentinel 同時還提供應用次元規則推送的示例頁面(流控規則頁面,前端路由為

/v2/flow

),使用者改造控制台對接配置中心後可直接通過 v2 頁面推送規則至配置中心。Sentinel 抽取了通用接口用于向遠端配置中心推送規則以及拉取規則:

  • DynamicRuleProvider<T>

    : 拉取規則(應用次元)
  • DynamicRulePublisher<T>

    : 推送規則(應用次元)

使用者隻需實作

DynamicRuleProvider

DynamicRulePublisher

接口,并在 v2 的 controller 中通過

@Qualifier

注解替換相應的 bean 即可實作應用次元推送。我們提供了 Nacos 和 Apollo 的示例,改造詳情可參考 應用次元規則推送示例。

2.1.5 控制台配置項
控制台的一些特性可以通過配置項來進行配置,配置項主要有兩個來源:

System.getProperty()

System.getenv()

,同時存在時後者可以覆寫前者。
通過環境變量進行配置時,因為不支援

.

是以需要将其更換為

_

配置項 類型 預設值 最小值 描述
auth.enabled boolean true - 是否開啟登入鑒權,僅用于日常測試,生産上不建議關閉
sentinel.dashboard.auth.username String sentinel - 登入控制台的使用者名,預設為

sentinel

sentinel.dashboard.auth.password String sentinel - 登入控制台的密碼,預設為

sentinel

sentinel.dashboard.app.hideAppNoMachineMillis Integer 60000 是否隐藏無健康節點的應用,距離最近一次主機心跳時間的毫秒數,預設關閉
sentinel.dashboard.removeAppNoMachineMillis Integer 120000 是否自動删除無健康節點的應用,距離最近一次其下節點的心跳時間毫秒數,預設關閉
sentinel.dashboard.unhealthyMachineMillis Integer 60000 30000 主機失聯判定,不可關閉
sentinel.dashboard.autoRemoveMachineMillis Integer 300000 距離最近心跳時間超過指定時間是否自動删除失聯節點,預設關閉
sentinel.dashboard.unhealthyMachineMillis Integer 60000 30000 主機失聯判定,不可關閉
server.servlet.session.cookie.name String sentinel_dashboard_cookie - 控制台應用的 cookie 名稱,可單獨設定避免同一域名下 cookie 名沖突

配置示例:

  • 指令行方式:
java -Dsentinel.dashboard.app.hideAppNoMachineMillis=60000
           
  • Java 方式:
  • 環境變量方式:
sentinel_dashboard_app_hideAppNoMachineMillis=60000
           
2.1.6 Sentinel 控制台鑒權
  • Sentinel 預設使用者名和密碼均為

    sentinel

  • -Dsentinel.dashboard.auth.username=sentinel

    用于指定控制台登入使用者名為

    sentinel

  • -Dsentinel.dashboard.auth.password=123456

    用于指定控制台的登入密碼為

    123456

  • -Dserver.servlet.session.timeout=7200

    用于指定 Spring Boot 服務端 session 的過期時間,預設機關為 s;指定

    60m

    則為60分鐘。預設為30分鐘;

2.2. Client 端

官方 Sentinel Demo 集合

Spring Cloud 整合 Damo 集合

2.2.1 微服務體系

Spring Boot/Spring Cloud
Alibaba Sentinel 學習筆記Sentinel

POM 依賴

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

Sentinel 簡例

SentinelSpringCloudApplication

@SpringBootApplication
public class SentinelSpringCloudApplication {

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

HelloService

public interface HelloService {
    String hello(String name);
}
           

HelloServiceImpl

@Service
public class HelloServiceImpl implements HelloService {

    //@SentinelResource 注解用來辨別資源屬性,資源名稱、是否被限流、降級等。fallback 用于表示限流或降級的操作。若不配置 blockHandler、fallback 等函數,則被流控降級時方法會直接抛出對應的 BlockException;若方法未定義 throws BlockException 則會被 JVM 包裝一層 UndeclaredThrowableException。
    @SentinelResource(value = "hello")
    @Override
    public String hello(String name) {
        return "hello " + name +"!";
    }
}
           

HelloController

@RestController
public class HelloController {
    @Autowired
    private HelloService helloService;

    @GetMapping(value = "/hello/{name}")
    public String hello(@PathVariable(value = "name") String name){
        return helloService.hello(name);
    }
}
           

application 配置項

server:
  port: 8081
spring:
  application:
    name: spring-cloud-alibaba-sentinel-springboot-example
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8791
management:
  endpoints:
    web:
      exposure:
        exclude: '*'
           

RestTemplate 支援

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

@SentinelRestTemplate

注解的屬性支援限流(

blockHandler

,

blockHandlerClass

)和降級(

fallback

,

fallbackClass

)的處理。其中

blockHandler

fallback

屬性對應的方法必須是對應

blockHandlerClass

fallbackClass

屬性中的靜态方法。該方法的參數跟傳回值跟

org.springframework.http.client.ClientHttpRequestInterceptor#interceptor

方法一緻,其中參數多出了一個

BlockException

參數用于擷取 Sentinel 捕獲的異常。

Quarkus
定位為GraalVM和OpenJDK HotSpot量身定制的一個Kurbernetes Native Java架構。雖然開源時間較短,但是生态方面也已經達到可用的狀态,自身包含擴充架構,已經支援像Netty、Undertow、Hibernate、JWT等架構,足以用于開發企業級應用,使用者也可以基于擴充架構自行擴充。

2.2.2 Web 體系

Web Servlet
Spring Web

2.2.3 RPC 體系

Apache Dubbo
gRPC
Feign
Feign 對應的接口中的資源名政策定義:httpmethod:protocol:/requesturl。

@FeignClient

注解中的所有屬性,Sentinel 都做了相容。

POM 依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
           

Application 配置

# 配置檔案打開 Sentinel 對 Feign 的支援
feign.sentinel.enabled=true
           

簡單示例

@FeignClient(name = "service-provider", fallback = EchoServiceFallback.class, configuration = FeignConfiguration.class)
public interface EchoService {
    @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
    String echo(@PathVariable("str") String str);
}

class FeignConfiguration {
    @Bean
    public EchoServiceFallback echoServiceFallback() {
        return new EchoServiceFallback();
    }
}

class EchoServiceFallback implements EchoService {
    @Override
    public String echo(@PathVariable("str") String str) {
        return "echo fallback";
    }
}
           
SOFARPC

2.2.4 HTTP Client 體系

Apache HttpClient
OkHttp

2.2.5 Reactor

2.2.6 Spring Cloud Gateway

示例

2.2.7 Apache RocketMQ

理念

2.3 Sentinel 控制台

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

2.3.1 概述

  • 檢視機器清單以及健康情況:收集 Sentinel 用戶端發送的心跳包,用于判斷機器是否線上。
  • 監控 (單機和叢集聚合):通過 Sentinel 用戶端暴露的監控 API,定期拉取并且聚合應用監控資訊,最終可以實作秒級的實時監控。
  • 規則管理和推送:統一管理推送規則。
  • 鑒權:生産環境中鑒權非常重要。這裡每個開發者需要根據自己的實際情況進行定制。

2.3.2 啟動控制台

現階段 Sentinel 核心庫和控制台(Dashboard)整合為一個控制端。

控制台的搭建參見 [Sentinel Server 端搭建]( # Sentinel_Server)

2.3.3 client 接入控制台(Dashboard)

JAR 包依賴
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>x.y.z</version>
</dependency>
           
配置啟動參數

JVM 啟動參數配置:

  • -Dcsp.sentinel.dashboard.server=consoleIp:port

    指定控制台位址和端口
  • -Dcsp.sentinel.api.port=xxxx

    指定客戶度監控 API 的端口(預設為 8719),友善監控多個應用
觸發用戶端初始化

Sentinel 采用懶加載機制,當有流量通路時才會初始化監控

檢視機器清單及健康狀況

${user.home}/logs/csp/sentinel-record.log.xxx

檢視日志來檢視啟動狀态

2.4 Sentinel 叢集搭建(待完善)

第三章:Sentinel 工作流程簡析

3.1 概述

在 Sentinel 裡面,所有的資源都對應一個資源名稱以及一個 Entry。Entry 可以通過對主流架構的适配自動建立,也可以通過注解的方式或調用 API 顯式建立;每一個 Entry 建立的時候,同時也會建立一系列功能插槽(slot chain)

Sentinel 主要插槽總覽:

  • NodeSelectorSlot

    負責收集資源的路徑,并将這些資源的調用路徑,以樹狀結構存儲起來,用于根據調用路徑來限流降級;
  • ClusterBuilderSlot

    則用于存儲資源的統計資訊以及調用者資訊,例如該資源的 RT, QPS, thread count 等等,這些資訊将用作為多元度限流,降級的依據;
  • StatisticSlot

    則用于記錄、統計不同緯度的 runtime 名額監控資訊;
  • FlowSlot

    則用于根據預設的限流規則以及前面 slot 統計的狀态,來進行流量控制;
  • AuthoritySlot

    則根據配置的黑白名單和調用來源資訊,來做黑白名單控制;
  • DegradeSlot

    則通過統計資訊以及預設的規則,來做熔斷降級;
  • SystemSlot

    則通過系統的狀态,例如 load1 等,來控制總的入口流量;

3.2 架構

Alibaba Sentinel 學習筆記Sentinel

3.3 插槽

3.3.1 NodeSelectorSlot

主要負責收集資源的路徑,并将這些資源的調用路徑,以樹狀結構存儲起來,用于根據調用路徑來限流降級。

3.3.2 ClusterBuilderSlot

用于建構資源的

ClusterNode

以及調用來源節點。

ClusterNode

保持資源運作統計資訊(響應時間、QPS、block 數目、線程數、異常數等)以及原始調用者統計資訊清單。來源調用者的名字由

ContextUtil.enter(contextName,origin)

中的

origin

标記。

3.3.3 StatisticSlot

用于統計實時的調用資料。
  • clusterNode

    :資源唯一辨別的 ClusterNode 的 runtime 統計
  • origin

    :根據來自不同調用者的統計資訊
  • defaultnode

    : 根據上下文條目名稱和資源 ID 的 runtime 統計
  • 入口的統計

底層采用高性能的滑動視窗資料結構

LeapArray

來統計實時的秒級名額資料,可以很好地支撐寫多于讀的高并發場景。

Alibaba Sentinel 學習筆記Sentinel

3.3.4 FlowSlot

​ 主要根據預設的資源的統計資訊,按照固定的次序,依次生效。如果一個資源對應兩條或者多條流控規則,則會根據如下次序依次檢驗,直到全部通過或者有一個規則生效為止:

  • 指定應用生效的規則,即針對調用方限流的;
  • 調用方為 other 的規則;
  • 調用方為 default 的規則。

3.3.5 DegradeSlot

主要針對資源的平均響應時間(RT)以及異常比率,來決定資源是否在接下來的時間被自動熔斷掉。

3.3.6 SystemSlot

​ 會根據對于目前系統的整體情況,對入口資源的調用進行動态調配。其原理是讓入口的流量和目前系統的預計容量達到一個動态平衡。注意系統規則隻對入口流量起作用(調用類型為

EntryType.IN

),對出口流量無效。可通過

SphU.entry(res, entryType)

指定調用類型,如果不指定,預設是

EntryType.OUT

3.3.7 自定義 Slot 的實作

Sentinel 将

ProcessorSlot

作為 SPI 接口進行擴充(1.7.2 版本以前

SlotChainBuilder

作為 SPI),使得 Slot Chain 具備了擴充的能力。您可以自行加入自定義的 slot 并編排 slot 間的順序,進而可以給 Sentinel 添加自定義的功能。
Alibaba Sentinel 學習筆記Sentinel

3.4 sentinel核心類

3.4.1 ProcessorSlotChain

Sentinel 的核心骨架,将不同的 Slot 按照順序串在一起(責任鍊模式),進而将不同的功能(限流、降級、系統保護)組合在一起。

slot chain 可以分為兩個主要部分:

  • 統計資料建構部分(statistic)
  • 判斷部分(rule checking)

3.4.2 Context

Context 代表調用鍊路上下文,貫穿一次調用鍊路中的所有

Entry

。Context 維持着入口節點(

entranceNode

)、本次調用鍊路的 curNode、調用來源(

origin

)等資訊。Context 名稱即為調用鍊路入口名稱。

Context 維持的方式:通過 ThreadLocal 傳遞,隻有在入口

enter

的時候生效。由于 Context 是通過 ThreadLocal 傳遞的,是以對于異步調用鍊路,線程切換的時候會丢掉 Context,是以需要手動通過

ContextUtil.runOnContext(context, f)

來變換 context

3.4.3 Entry

每一次資源調用都會建立一個

Entry

Entry

包含了資源名、curNode(目前統計節點)、originNode(來源統計節點)等資訊。

3.4.4 Node

Sentinel 裡面的各種種類的統計節點:

  • StatisticNode

    :最為基礎的統計節點,包含秒級和分鐘級兩個滑動視窗結構。
  • DefaultNode

    :鍊路節點,用于統計調用鍊路上某個資源的資料,維持樹狀結構。
  • ClusterNode

    :簇點,用于統計每個資源全局的資料(不區分調用鍊路),以及存放該資源的按來源區分的調用資料(類型為

    StatisticNode

    )。特别地,

    Constants.ENTRY_NODE

    節點用于統計全局的入口資源資料。
  • EntranceNode

    :入口節點,特殊的鍊路節點,對應某個 Context 入口的所有調用資料。

    Constants.ROOT

    節點也是入口節點。

建構的時機:

  • EntranceNode

    ContextUtil.enter(xxx)

    的時候就建立了,然後塞到 Context 裡面。
  • NodeSelectorSlot

    :根據 context 建立

    DefaultNode

    ,然後 set curNode to context。
  • ClusterBuilderSlot

    :首先根據 resourceName 建立

    ClusterNode

    ,并且 set clusterNode to defaultNode;然後再根據 origin 建立來源節點(類型為

    StatisticNode

    ),并且 set originNode to curEntry。

幾種 Node 的次元(數目):

  • ClusterNode

    的次元是 resource
  • DefaultNode

    的次元是 resource * context,存在每個 NodeSelectorSlot 的

    map

    裡面
  • EntranceNode

    的次元是 context,存在 ContextUtil 類的

    contextNameNodeMap

    裡面
  • 來源節點(類型為

    StatisticNode

    )的次元是 resource * origin,存在每個 ClusterNode 的

    originCountMap

    裡面

3.4.5 StatisticSlot

用于根據規則判斷結果進行相應的統計操作。

​ entry 的時候:依次執行後面的判斷 slot。每個 slot 觸發流控的話會抛出異常(

BlockException

的子類)。若有

BlockException

抛出,則記錄 block 資料;若無異常抛出則算作可通過(pass),記錄 pass 資料。

​ exit 的時候:若無 error(無論是業務異常還是流控異常),記錄 complete(success)以及 RT,線程數-1。

​ 記錄資料的次元:線程數+1、記錄目前 DefaultNode 資料、記錄對應的 originNode 資料(若存在 origin)、累計 IN 統計資料(若流量類型為 IN)。

第四章:資源與規則

4.1 概述

Sentinel 可以簡單的分為 Sentinel 核心庫和 Dashboard。核心庫不依賴 Dashboard,但是結合 Dashboard 可以取得最好的效果。

使用 Sentinel 進行資源保護,主要步驟:

  1. 定義資源
  2. 定義規則
  3. 檢驗規則是否生效

4.2 定義資源

4.2.1 方式一:主流架構的預設适配

參見:Sentinel 官方主流架構的适配

4.2.2 方式二:抛出異常的方式定義資源

Sphu 包含了 try-catch 風格的 API 。當資源發生了限流之後會抛出 BlockException 。這個時候就可以進行異常的捕獲了,進行限流之後的邏輯處理。
// 1.5.0 版本開始可以利用 try-with-resources 特性
// 資源名可使用任意有業務語義的字元串,比如方法名、接口名或其它可唯一辨別的字元串。
try (Entry entry = SphU.entry("resourceName")) {
  // 被保護的業務邏輯
  // do something here...
} catch (BlockException ex) {
  // 資源通路阻止,被限流或被降級
  // 在此處進行相應的處理操作
}
           

​ 若 entry 的時候傳入了熱點參數,那麼 exit 的時候也一定要帶上對應的參數(

exit(count, args)

),否則可能會有統計錯誤。這個時候不能使用 try-with-resources 的方式。另外通過

Tracer.trace(ex)

來統計異常資訊時,由于 try-with-resources 文法中 catch 調用順序的問題,會導緻無法正确統計異常數,是以統計異常資訊時也不能在 try-with-resources 的 catch 塊中調用

Tracer.trace(ex)

// 1.5.0 之前的版本的示例:

Entry entry = null;
// 務必保證finally會被執行
try {
  // 資源名可使用任意有業務語義的字元串
  entry = SphU.entry("自定義資源名");
  // 被保護的業務邏輯
  // do something...
} catch (BlockException e1) {
  // 資源通路阻止,被限流或被降級
  // 進行相應的處理操作
} finally {
  if (entry != null) {
    // SphU.entry(xxx) 需要與 entry.exit() 方法成對出現,比對調用,否則會導緻調用鍊記錄異常,抛出 ErrorEntryFreeException 異常。
    entry.exit();
  }
}
           

4.2.3 方式三:傳回布爾值方式定義資源

SphO

提供 if-else 風格的 API。用這種方式,當資源發生了限流之後會傳回

false

,這個時候可以根據傳回值,進行限流之後的邏輯處理。
// 資源名可使用任意有業務語義的字元串
  if (SphO.entry("自定義資源名")) {
    // 務必保證finally會被執行
    try {
      /**
      * 被保護的業務邏輯
      */
    } finally {
      SphO.exit();
    }
  } else {
    // 資源通路阻止,被限流或被降級
    // 進行相應的處理操作
  }
           

4.2.4 方式四:注解方式定義資源

@SentinelResource

注解定義資源并配置

blockHandler

fallback

函數來進行限流之後的處理。具體參見: Sentinel 注解埋點
// 原本的業務方法.
@SentinelResource(blockHandler = "blockHandlerForGetUser")
public User getUserById(String id) {
    throw new RuntimeException("getUserById command failed");
}

// blockHandler 函數,原方法調用被限流/降級/系統保護的時候調用
public User blockHandlerForGetUser(String id, BlockException ex) {
    return new User("admin");
}
           

blockHandler

函數會在原方法被限流/降級/系統保護的時候調用,而

fallback

函數會針對所有類型的異常。

4.2.5 方式五:異步調用支援

在異步調用中,需要通過

SphU.asyncEntry(xxx)

方法定義資源,并通常需要在異步的回調函數中調用

exit

方法。
try {
    AsyncEntry entry = SphU.asyncEntry(resourceName);

    // 異步調用.
    doAsync(userId, result -> {
        try {
            // 在此處處理異步調用的結果.
        } finally {
            // 在回調結束後 exit.
            entry.exit();
        }
    });
} catch (BlockException ex) {
    // Request blocked.
    // Handle the exception (e.g. retry or fallback).
}
           

SphU.asyncEntry(xxx)

不會影響目前(調用線程)的 Context,是以以下兩個 entry 在調用鍊上是平級關系(處于同一層),而不是嵌套關系:

// 調用鍊類似于:
// -parent
// ---asyncResource
// ---syncResource
asyncEntry = SphU.asyncEntry(asyncResource);
entry = SphU.entry(normalResource);
           

若在異步回調中需要嵌套其它的資源調用(無論是

entry

還是

asyncEntry

),隻需要借助 Sentinel 提供的上下文切換功能,在對應的地方通過

ContextUtil.runOnContext(context, f)

進行 Context 變換,将對應資源調用處的 Context 切換為生成的異步 Context,即可維持正确的調用鍊路關系。

public void handleResult(String result) {
    Entry entry = null;
    try {
        entry = SphU.entry("handleResultForAsync");
        // Handle your result here.
    } catch (BlockException ex) {
        // Blocked for the result handler.
    } finally {
        if (entry != null) {
            entry.exit();
        }
    }
}

public void someAsync() {
    try {
        AsyncEntry entry = SphU.asyncEntry(resourceName);

        // Asynchronous invocation.
        doAsync(userId, result -> {
            // 在異步回調中進行上下文變換,通過 AsyncEntry 的 getAsyncContext 方法擷取異步 Context
            ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
                try {
                    // 此處嵌套正常的資源調用.
                    handleResult(result);
                } finally {
                    entry.exit();
                }
            });
        });
    } catch (BlockException ex) {
        // Request blocked.
        // Handle the exception (e.g. retry or fallback).
    }
}



// 此時的鍊路調用就類似于
-parent
---asyncInvocation
-----handleResultForAsync
           

示例 Demo:

public class AsyncEntryDemo {

    private void invoke(String arg, Consumer<String> handler) {
        CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                String resp = arg + ": " + System.currentTimeMillis();
                handler.accept(resp);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });
    }

    private void anotherAsync() {
        try {
            final AsyncEntry entry = SphU.asyncEntry("test-another-async");

            CompletableFuture.runAsync(() -> {
                ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                        // Normal entry nested in asynchronous entry.
                        anotherSyncInAsync();

                        System.out.println("Async result: 666");
                    } catch (InterruptedException e) {
                        // Ignore.
                    } finally {
                        entry.exit();
                    }
                });
            });
        } catch (BlockException ex) {
            ex.printStackTrace();
        }
    }

    private void fetchSync() {
        Entry entry = null;
        try {
            entry = SphU.entry("test-sync");
        } catch (BlockException ex) {
            ex.printStackTrace();
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }

    private void fetchSyncInAsync() {
        Entry entry = null;
        try {
            entry = SphU.entry("test-sync-in-async");
        } catch (BlockException ex) {
            ex.printStackTrace();
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }

    private void anotherSyncInAsync() {
        Entry entry = null;
        try {
            entry = SphU.entry("test-another-sync-in-async");
        } catch (BlockException ex) {
            ex.printStackTrace();
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }

    private void directlyAsync() {
        try {
            final AsyncEntry entry = SphU.asyncEntry("test-async-not-nested");

            this.invoke("abc", result -> {
                // If no nested entry later, we don't have to wrap in `ContextUtil.runOnContext()`.
                try {
                    // Here to handle the async result (without other entry).
                } finally {
                    // Exit the async entry.
                    entry.exit();
                }
            });
        } catch (BlockException e) {
            // Request blocked, handle the exception.
            e.printStackTrace();
        }
    }

    private void doAsyncThenSync() {
        try {
            // First we call an asynchronous resource.
            final AsyncEntry entry = SphU.asyncEntry("test-async");
            this.invoke("abc", resp -> {
                // The thread is different from original caller thread for async entry.
                // So we need to wrap in the async context so that nested invocation entry
                // can be linked to the parent asynchronous entry.
                ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
                    try {
                        // In the callback, we do another async invocation several times under the async context.
                        for (int i = 0; i < 7; i++) {
                            anotherAsync();
                        }

                        System.out.println(resp);

                        // Then we do a sync (normal) entry under current async context.
                        fetchSyncInAsync();
                    } finally {
                        // Exit the async entry.
                        entry.exit();
                    }
                });
            });
            // Then we call a sync resource.
            fetchSync();
        } catch (BlockException ex) {
            // Request blocked, handle the exception.
            ex.printStackTrace();
        }
    }

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

        AsyncEntryDemo service = new AsyncEntryDemo();

        // Expected invocation chain:
        //
        // EntranceNode: machine-root
        // -EntranceNode: async-context
        // --test-top
        // ---test-sync
        // ---test-async
        // ----test-another-async
        // -----test-another-sync-in-async
        // ----test-sync-in-async
        ContextUtil.enter("async-context", "originA");
        Entry entry = null;
        try {
            entry = SphU.entry("test-top");
            System.out.println("Do something...");
            service.doAsyncThenSync();
        } catch (BlockException ex) {
            // Request blocked, handle the exception.
            ex.printStackTrace();
        } finally {
            if (entry != null) {
                entry.exit();
            }
            ContextUtil.exit();
        }

        TimeUnit.SECONDS.sleep(20);
    }

    private static void initFlowRule() {
        // Rule 1 won't take effect as the limitApp doesn't match.
        FlowRule rule1 = new FlowRule()
            .setResource("test-another-sync-in-async")
            .setLimitApp("originB")
            .as(FlowRule.class)
            .setCount(4)
            .setGrade(RuleConstant.FLOW_GRADE_QPS);
        // Rule 2 will take effect.
        FlowRule rule2 = new FlowRule()
            .setResource("test-another-async")
            .setLimitApp("default")
            .as(FlowRule.class)
            .setCount(5)
            .setGrade(RuleConstant.FLOW_GRADE_QPS);
        List<FlowRule> ruleList = Arrays.asList(rule1, rule2);
        FlowRuleManager.loadRules(ruleList);
    }
}
           

4.3 規則的簡介及操作

Sentinel 的所有規則都可以在記憶體态中動态地查詢及修改,修改之後立即生效。同時 Sentinel 也提供相關 API,來快速的定制自己的規則政策

4.3.1 流量控制規則 (FlowRule)

同一個資源可以同時有多個限流規則。
流量規則重要屬性
Field 說明 預設值
resource 資源名,資源名是限流規則的作用對象
count 限流門檻值
grade 限流門檻值類型,QPS 或線程數模式 QPS 模式
limitApp 流控針對的調用來源

default

,代表不區分調用來源
strategy 調用關系限流政策:直接、鍊路、關聯 根據資源本身(直接)
controlBehavior 流控效果(直接拒絕 / 排隊等待 / 慢啟動模式),不支援按調用關系限流 直接拒絕
通過代碼定義流量控制規則
private static void initFlowQpsRule() {
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule1 = new FlowRule();
    rule1.setResource(resource);
    // Set max qps to 20
    rule1.setCount(20);
    rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule1.setLimitApp("default");
    rules.add(rule1);
    // FlowRuleManager.loadRules() 采用寫死的方式定義流量控制規則
    FlowRuleManager.loadRules(rules);
}
           

4.3.2 熔斷降級規則 (DegradeRule)

同一個資源可以同時有多個降級規則。
熔斷降級規則重要屬性
Field 說明 預設值
resource 資源名,即規則的作用對象
grade 熔斷政策,支援慢調用比例/異常比例/異常數政策 慢調用比例
count 慢調用比例模式下為慢調用臨界 RT(超出該值計為慢調用);異常比例/異常數模式下為對應的門檻值
timeWindow 熔斷時長,機關為 s
minRequestAmount 熔斷觸發的最小請求數,請求數小于該值時即使異常比率超出門檻值也不會熔斷(1.7.0 引入) 5
statIntervalMs 統計時長(機關為 ms),如 60*1000 代表分鐘級(1.8.0 引入) 1000 ms
slowRatioThreshold 慢調用比例門檻值,僅慢調用比例模式有效(1.8.0 引入)
通過代碼定義熔斷降級規則
private static void initDegradeRule() {
    List<DegradeRule> rules = new ArrayList<>();
    DegradeRule rule = new DegradeRule(resource);
        .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType());
        .setCount(0.7); // Threshold is 70% error ratio
        .setMinRequestAmount(100)
        .setStatIntervalMs(30000) // 30s
        .setTimeWindow(10);
    rules.add(rule);
    // DegradeRuleManager.loadRules() 方法來用寫死的方式定義熔斷降級規則
    DegradeRuleManager.loadRules(rules);
}
           

4.3.3 系統保護規則 (SystemRule)

Sentinel 系統自适應限流從整體次元對應用入口流量進行控制,結合應用的 Load、CPU 使用率、總體平均 RT、入口 QPS 和并發線程數等幾個次元的監控名額,通過自适應的流控政策,讓系統的入口流量和系統的負載達到一個平衡,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。
熔斷降級規則重要屬性
Field 說明 預設值
highestSystemLoad

load1

觸發值,用于觸發自适應控制階段
-1 (不生效)
avgRt 所有入口流量的平均響應時間 -1 (不生效)
maxThread 入口流量的最大并發數 -1 (不生效)
qps 所有入口資源的 QPS -1 (不生效)
highestCpuUsage 目前系統的 CPU 使用率(0.0-1.0) -1 (不生效)
通過代碼定義熔斷降級規則
private void initSystemProtectionRule() {
  List<SystemRule> rules = new ArrayList<>();
  SystemRule rule = new SystemRule();
  rule.setHighestSystemLoad(10);
  rules.add(rule);
  // 系統保護規則
  SystemRuleManager.loadRules(rules);
}
           

4.3.4 通路控制規則 (AuthorityRule)

很多時候,我們需要根據調用方來限制資源是否通過,這時候可以使用 Sentinel 的通路控制(黑白名單)的功能。黑白名單根據資源的請求來源(

origin

)限制資源是否通過,若配置白名單則隻有請求來源位于白名單内時才可通過;若配置黑名單則請求來源位于黑名單時不通過,其餘的請求通過。

授權規則,即黑白名單規則(

AuthorityRule

):
  • resource

    :資源名,即限流規則的作用對象
  • limitApp

    :對應的黑名單/白名單,不同 origin 用

    ,

    分隔,如

    appA,appB

  • strategy

    :限制模式,

    AUTHORITY_WHITE

    為白名單模式,

    AUTHORITY_BLACK

    為黑名單模式,預設為白名單模式
通過代碼定義熔斷降級規則
調用方資訊通過

ContextUtil.enter(resourceName, origin)

方法中的

origin

參數傳入。
AuthorityRule rule = new AuthorityRule();
// 設定 test 資源
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
// 設定資源通路規則 隻有 appA 和 appB 的請求才能通過
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
           

4.3.5 熱點規則 (ParamFlowRule)

何為熱點?熱點即經常通路的資料。很多時候我們希望統計某個熱點資料中通路頻次最高的 Top K 資料,并對其通路進行限制。熱點參數限流會統計傳入參數中的熱點參數,并根據配置的限流門檻值與模式,對包含熱點參數的資源調用進行限流。熱點參數限流可以看做是一種特殊的流量控制,僅對包含熱點參數的資源調用生效。

Sentinel 利用 LRU 政策統計最近最常通路的熱點參數,結合令牌桶算法來進行參數級别的流控。

示例:

<!-- 熱點參數限流需要引入的 Jar -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-parameter-flow-control</artifactId>
    <version>x.y.z</version>
</dependency>
           
  1. 對應的資源配置熱點參數限流規則,并在

    entry

    的時候傳入相應的參數,即可使熱點參數限流生效。
    若自行擴充并注冊了自己實作的

    SlotChainBuilder

    ,并希望使用熱點參數限流功能,則可以在 chain 裡面合适的地方插入

    ParamFlowSlot

    // 通過 SphU 類裡面幾個 entry 重載方法來傳入對應的參數
    public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException
    
    public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException
               
    注意:
    1. 若 entry 的時候傳入了熱點參數,那麼 exit 的時候也一定要帶上對應的參數(

      exit(count, args)

      )。例如:

      entry.exit(1, paramA, paramB)

    2. @SentinelResource

      注解方式定義的資源,若注解作用的方法上有參數,Sentinel 會将它們作為參數傳入

      SphU.entry(res, args)

    // uid 和 type 會分别作為第一個和第二個參數傳入 Sentinel API,進而可以用于熱點規則判斷
       @SentinelResource("myMethod")
       public Result doSomething(String uid, int type) {
         // some logic here...
       }
               
熱點參數規則重要屬性
屬性 說明 預設值
resource 資源名,必填
count 限流門檻值,必填
grade 限流模式 QPS 模式
durationInSec 統計視窗時間長度(機關為秒),1.6.0 版本開始支援 1s
controlBehavior 流控效果(支援快速失敗和勻速排隊模式),1.6.0 版本開始支援 快速失敗
maxQueueingTimeMs 最大排隊等待時長(僅在勻速排隊模式生效),1.6.0 版本開始支援 0ms
paramIdx 熱點參數的索引,必填,對應

SphU.entry(xxx, args)

中的參數索引位置
paramFlowItemList 參數例外項,可以針對指定的參數值單獨設定限流門檻值,不受前面

count

門檻值的限制。僅支援基本類型和字元串類型
clusterMode 是否是叢集參數流控規則

false

clusterConfig 叢集流控相關配置
通過代碼定義熱點參數規則
ParamFlowRule rule = new ParamFlowRule(resourceName)
    .setParamIdx(0)
    .setCount(5);
// 針對 int 類型的參數 PARAM_B,單獨設定限流 QPS 門檻值為 10,而不是全局的門檻值 5.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
    .setClassType(int.class.getName())
    .setCount(10);
rule.setParamFlowItemList(Collections.singletonList(item));

// 通過 ParamFlowRuleManager 的 loadRules 方法更新熱點參數規則
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
           

4.3.6 查詢更改規則

  1. transport 子產品後,可以通過以下的 HTTP API 來擷取所有已加載的規則:
http://localhost:8719/getRules?type=<XXXX>
           

type=flow

以 JSON 格式傳回現有的限流規則,degrade 傳回現有生效的降級規則清單,system 則傳回系統保護規則。

  1. 擷取所有熱點規則:
http://localhost:8719/getParamRules
           

​ 其中,type 可以輸入

flow

degrade

等方式來制定更改的規則種類,

data

則是對應的 JSON 格式的規則。

4.3.7 定制自己的持久化規則

初始化時系統的規則配置,都是存在記憶體中的,如果應用重新開機,這個規則就會失效。可以通過實作外部開放的

DataSource

接口的方式,來自定義規則的存儲資料源。實作規則的持久化存儲:

  • 整合動态配置系統,如 ZooKeeper、Nacos 等,動态地實時重新整理配置規則
  • 結合 RDBMS、NoSQL、VCS 等來實作該規則
  • 配合 Sentinel Dashboard 使用

具體使用參見:動态規則配置

4.3.8 規則生效的效果

判斷限流降級異常
// 代碼層面判斷 Sentinel 的流控降級異常
BlockException.isBlockException(Throwable t);
           

或者

  • 暴露的 HTTP 接口:通過運作下面指令

    curl http://localhost:8719/cnode?id=<資源名稱>

    ,觀察傳回的資料。如果規則生效,在傳回的資料欄中的

    block

    以及

    block(m)

    中會有顯示
  • 日志:Sentinel 提供秒級的資源運作日志以及限流日志,詳情可以參考 日志文檔
block 事件

通過

StatisticSlotCallbackRegistry

StatisticSlot

注冊回調函數。利用這些回調接口來實作報警等功能,實時的監控資訊可以從

ClusterNode

中實時擷取:

  • ProcessorSlotEntryCallback

    : callback when resource entry passed (

    onPass

    ) or blocked (

    onBlocked

    )
  • ProcessorSlotExitCallback

    : callback when resource entry successfully completed (

    onExit

    )

4.4 其它 API

4.4.1 業務異常統計 Tracer

業務異常記錄類

Tracer

用于記錄業務異常。

相關方法:

  • trace(Throwable e)

    :記錄業務異常(非

    BlockException

    異常),對應的資源為目前線程 context 下 entry 對應的資源。
  • trace(Throwable e, int count)

    :記錄業務異常(非

    BlockException

    異常),異常數目為傳入的

    count

  • traceEntry(Throwable, int, Entry)

    :向傳入 entry 對應的資源記錄業務異常(非

    BlockException

    異常),異常數目為傳入的

    count

如果使用者通過

SphU

SphO

手動定義資源,則 Sentinel 不能感覺上層業務的異常,需要手動調用

Tracer.trace(ex)

來記錄業務異常,否則對應的異常不會統計到 Sentinel 異常計數中。注意不要在 try-with-resources 形式的

SphU.entry(xxx)

中使用,否則會統計不上。

Tips: 從 1.3.1 版本開始,注解方式定義資源支援自動統計業務異常,無需手動調用

Tracer.trace(ex)

來記錄業務異常。Sentinel 1.3.1 以前的版本需要手動記錄。

4.4.2 上下文工具類 ContextUtil

辨別進入調用鍊入口(上下文):

以下靜态方法用于辨別調用鍊路入口,用于區分不同的調用鍊路:

  • public static Context enter(String contextName)

  • public static Context enter(String contextName, String origin)

其中

contextName

代表調用鍊路入口名稱(上下文名稱),

origin

代表調用來源名稱。預設調用來源為空。傳回值類型為

Context

,即生成的調用鍊路上下文對象。

注意:

ContextUtil.enter(xxx)

方法僅在調用鍊路入口處生效,即僅在目前線程的初次調用生效,後面再調用不會覆寫目前線程的調用鍊路,直到 exit。

Context

存于 ThreadLocal 中,是以切換線程時可能會丢掉,如果需要跨線程使用可以結合

runOnContext

方法使用。

流控規則中若選擇“流控方式”為“鍊路”方式,則入口資源名即為上面的

contextName

退出調用鍊(清空上下文):

  • public static void exit()

    :該方法用于退出調用鍊,清理目前線程的上下文。

擷取目前線程的調用鍊上下文:

  • public static Context getContext()

    :擷取目前線程的調用鍊路上下文對象。

在某個調用鍊上下文中執行代碼:

  • public static void runOnContext(Context context, Runnable f)

    :常用于異步調用鍊路中 context 的變換。

4.4.3名額統計配置

Sentinel 底層采用高性能的滑動視窗資料結構來統計實時的秒級名額資料,并支援對滑動視窗進行配置。主要有以下兩個配置:

  • windowIntervalMs

    :滑動視窗的總的時間長度,預設為 1000 ms
  • sampleCount

    :滑動視窗劃分的格子數目,預設為 2;格子越多則精度越高,但是記憶體占用也會越多
Alibaba Sentinel 學習筆記Sentinel

Tips: 我們可以通過

SampleCountProperty

來動态地變更滑動視窗的格子數目,通過

IntervalProperty

來動态地變更滑動視窗的總時間長度。注意這兩個配置都是全局生效的,會影響所有資源的所有名額統計。

第五章:流量控制

5.1 概述

FlowSlot

會根據預設的規則,結合前面

NodeSelectorSlot

ClusterNodeBuilderSlot

StatistcSlot

統計出來的實時資訊進行流量控制。

​ 限流的直接表現是在執行

Entry nodeA = SphU.entry(資源名字)

的時候抛出

FlowException

異常。

FlowException

BlockException

的子類,您可以捕捉

BlockException

來自定義被限流之後的處理邏輯。

​ 同一個資源可以對應多條限流規則。

FlowSlot

會對該資源的所有限流規則依次周遊,直到有規則觸發限流或者所有規則周遊完畢。

一條限流規則主要由下面幾個因素組成:

  • resource

    :資源名,即限流規則的作用對象
  • count

    : 限流門檻值
  • grade

    : 限流門檻值類型,QPS 或線程數
  • strategy

    : 根據調用關系選擇政策

5.2 基于QPS/并發數的流量控制

流量控制主要有兩種統計類型,一種是統計線程數,另外一種則是統計 QPS。類型由

FlowRule.grade

字段來定義。其中,0 代表根據并發數量來限流,1 代表根據 QPS 來進行流量控制。其中線程數、QPS 值,都是由

StatisticSlot

實時統計擷取的。
curl http://localhost:8719/cnode?id=resourceName

輸出内容格式:
idx id   thread  pass  blocked   success  total Rt   1m-pass   1m-block   1m-all   exeption
2   abc647 0     46     0           46     46   1       2763      0         2763     0
           
  • thread: 代表目前處理該資源的線程數;
  • pass: 代表一秒内到來到的請求;
  • blocked: 代表一秒内被流量控制的請求數量;
  • success: 代表一秒内成功處理完的請求;
  • total: 代表到一秒内到來的請求以及被阻止的請求總和;
  • RT: 代表一秒内該資源的平均響應時間;
  • 1m-pass: 則是一分鐘内到來的請求;
  • 1m-block: 則是一分鐘内被阻止的請求;
  • 1m-all: 則是一分鐘内到來的請求和被阻止的請求的總和;
  • exception: 則是一秒内業務本身異常的總和。

5.2.1 并發線程數流量控制

Sentinel線程數限流不負責建立和管理線程池,而是簡單統計目前請求上下文的線程個數,如果超出門檻值,新的請求會被立即拒絕。

​ 線程數限流用于保護業務線程數不被耗盡。例如,當應用所依賴的下遊應用由于某種原因導緻服務不穩定、響應延遲增加,對于調用者來說,意味着吞吐量下降和更多的線程數占用,極端情況下甚至導緻線程池耗盡。為應對高線程占用的情況,業内有使用隔離的方案,比如通過不同業務邏輯使用不同線程池來隔離業務自身之間的資源争搶(線程池隔離),或者使用信号量來控制同時請求的個數(信号量隔離)。這種隔離方案雖然能夠控制線程數量,但無法控制請求排隊時間。當請求過多時排隊也是無益的,直接拒絕能夠迅速降低系統壓力。

public class FlowThreadDemo {

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

    private static volatile boolean stop = false;
    private static final int threadCount = 100;

    private static int seconds = 60 + 40;
    private static volatile int methodBRunningTime = 2000;

    public static void main(String[] args) throws Exception {
        System.out.println(
            "MethodA will call methodB. After running for a while, methodB becomes fast, "
                + "which make methodA also become fast ");
        tick();
        initFlowRule();

        for (int i = 0; i < threadCount; i++) {
            Thread entryThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        Entry methodA = null;
                        try {
                            TimeUnit.MILLISECONDS.sleep(5);
                            methodA = SphU.entry("methodA");
                            activeThread.incrementAndGet();
                            Entry methodB = SphU.entry("methodB");
                            TimeUnit.MILLISECONDS.sleep(methodBRunningTime);
                            methodB.exit();
                            pass.addAndGet(1);
                        } catch (BlockException e1) {
                            block.incrementAndGet();
                        } catch (Exception e2) {
                            // biz exception
                        } finally {
                            total.incrementAndGet();
                            if (methodA != null) {
                                methodA.exit();
                                activeThread.decrementAndGet();
                            }
                        }
                    }
                }
            });
            entryThread.setName("working thread");
            entryThread.start();
        }
    }

    private static void initFlowRule() {
        List<FlowRule> rules = new ArrayList<FlowRule>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource("methodA");
        // set limit concurrent thread for 'methodA' to 20
        rule1.setCount(20);
        rule1.setGrade(RuleConstant.FLOW_GRADE_THREAD);
        rule1.setLimitApp("default");

        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
    }

    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 + " total qps is: " + oneSecondTotal);
                System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal
                    + ", pass:" + oneSecondPass
                    + ", block:" + oneSecondBlock
                    + " activeThread:" + activeThread.get());
                if (seconds-- <= 0) {
                    stop = true;
                }
                if (seconds == 40) {
                    System.out.println("method B is running much faster; more requests are allowed to pass");
                    methodBRunningTime = 20;
                }
            }

            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);
        }
    }
}
           

5.2.2 QPS流量控制

QPS:每秒通路量

當 QPS 超過某個門檻值的時候,則采取措施進行流量控制。流量控制的手段包括下面 3 種,對應

FlowRule

中的

controlBehavior

字段:

  1. 直接拒絕(

    RuleConstant.CONTROL_BEHAVIOR_DEFAULT

    )方式。該方式是預設的流量控制方式,當QPS超過任意規則的門檻值後,新的請求就會被立即拒絕,拒絕方式為抛出

    FlowException

    。這種方式适用于對系統處理能力确切已知的情況下,比如通過壓測确定了系統的準确水位時。具體的例子參見 FlowqpsDemo。
  2. 冷啟動(

    RuleConstant.CONTROL_BEHAVIOR_WARM_UP

    )方式。該方式主要用于系統長期處于低水位的情況下,當流量突然增加時,直接把系統拉升到高水位可能瞬間把系統壓垮。通過"冷啟動",讓通過的流量緩慢增加,在一定時間内逐漸增加到門檻值上限,給冷系統一個預熱的時間,避免冷系統被壓垮的情況。具體的例子參見 WarmUpFlowDemo。
  3. 勻速器(

    RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER

    )方式。這種方式嚴格控制了請求通過的間隔時間,也即是讓請求以均勻的速度通過,對應的是漏桶算法。具體的例子參見 PaceFlowDemo。
    主要用于處理間隔性突發的流量,例如消息隊列。在某一秒有大量的請求到來,而接下來的幾秒則處于空閑狀态,我們希望系統能夠在接下來的空閑期間逐漸處理這些請求,而不是在第一秒直接拒絕多餘的請求。

5.3 基于調用關系的流量控制

調用關系包括調用方、被調用方;方法又可能會調用其它方法,形成一個調用鍊路的層次關系。Sentinel 通過

NodeSelectorSlot

建立不同資源間的調用的關系,并且通過

ClusterNodeBuilderSlot

記錄每個資源的實時統計資訊。

5.3.1 根據調用方限流

ContextUtil.enter(resourceName, origin)

方法中的

origin

參數标明了調用方身份。這些資訊會在

ClusterBuilderSlot

中被統計。
# 可通過以下指令來展示不同的調用方對同一個資源的調用資料:
curl http://localhost:8719/origin/id=nodeA

# 資料示例:
id: nodeA
idx origin  threadNum passedQps blockedQps totalQps aRt   1m-passed 1m-blocked 1m-total 
1   caller1 0         0         0          0        0     0         0          0
2   caller2 0         0         0          0        0     0         0          0
           

限流規則中的

limitApp

字段用于根據調用方進行流量控制。該字段的值有以下三種選項,分别對應不同的場景:

  • default

    :表示不區分調用者,來自任何調用者的請求都将進行限流統計。如果這個資源名的調用總和超過了這條規則定義的門檻值,則觸發限流。
  • {some_origin_name}

    :表示針對特定的調用者,隻有來自這個調用者的請求才會進行流量控制。例如

    NodeA

    配置了一條針對調用者

    caller1

    的規則,那麼當且僅當來自

    caller1

    NodeA

    的請求才會觸發流量控制。
  • other

    :表示針對除

    {some_origin_name}

    以外的其餘調用方的流量進行流量控制。例如,資源

    NodeA

    配置了一條針對調用者

    caller1

    的限流規則,同時又配置了一條調用者為

    other

    的規則,那麼任意來自非

    caller1

    NodeA

    的調用,都不能超過

    other

    這條規則定義的門檻值。

同一個資源名可以配置多條規則,規則的生效順序為:{some_origin_name} > other > default

5.3.2 根據調用鍊路入口限流:鍊路限流

NodeSelectorSlot

中記錄了資源之間的調用鍊路,這些資源通過調用關系,互相之間構成一棵調用樹。這棵樹的根節點是一個名字為

machine-root

的虛拟節點,調用鍊的入口都是這個虛節點的子節點。
machine-root
                    /       \
                   /         \
             Entrance1     Entrance2
                /             \
               /               \
      DefaultNode(nodeA)   DefaultNode(nodeA)
           

來自入口

Entrance1

Entrance2

的請求都調用到了資源

NodeA

,Sentinel 允許隻根據某個入口的統計資訊對資源限流。比如我們可以設定

FlowRule.strategy

RuleConstant.CHAIN

,同時設定

FlowRule.ref_identity

Entrance1

來表示隻有從入口

Entrance1

的調用才會記錄到

NodeA

的限流統計當中,而對來自

Entrance2

的調用漠不關心。

調用鍊的入口是通過 API 方法

ContextUtil.enter(name)

定義的。

5.3.3 具有關系的資源流量控制:關聯流量控制

​ 當兩個資源之間具有資源争搶或者依賴關系的時候,這兩個資源便具有了關聯。比如對資料庫同一個字段的讀操作和寫操作存在争搶,讀的速度過高會影響寫得速度,寫的速度過高會影響讀的速度。如果放任讀寫操作争搶資源,則争搶本身帶來的開銷會降低整體的吞吐量。可使用關聯限流來避免具有關聯關系的資源之間過度的争搶,舉例來說,

read_db

write_db

這兩個資源分别代表資料庫讀寫,我們可以給

read_db

設定限流規則來達到寫優先的目的:設定

FlowRule.strategy

RuleConstant.RELATE

同時設定

FlowRule.ref_identity

write_db

。這樣當寫庫操作過于頻繁時,讀資料的請求會被限流。

第六章:熔斷降級

6.1 概述

,對調用鍊路中不穩定的資源進行熔斷降級也是保障高可用的重要措施之一。一個服務常常會調用别的子產品,可能是另外的一個遠端服務、資料庫,或者第三方 API 等。然而,這個被依賴服務的穩定性是不能保證的。如果依賴的服務出現了不穩定的情況,請求的響應時間變長,那麼調用服務的方法的響應時間也會變長,線程會産生堆積,最終可能耗盡業務自身的線程池,服務本身也變得不可用。

現代微服務架構都是分布式的,由非常多的服務組成。不同服務之間互相調用,組成複雜的調用鍊路。以上的問題在鍊路調用中會産生放大的效果。複雜鍊路上的某一環不穩定,就可能會層層級聯,最終導緻整個鍊路都不可用。是以我們需要對不穩定的弱依賴服務調用進行熔斷降級,暫時切斷不穩定調用,避免局部不穩定因素導緻整體的雪崩。熔斷降級作為保護自身的手段,通常在用戶端(調用端)進行配置。

6.1 熔斷政策

Sentinel 提供以下幾種熔斷政策:

  • 慢調用比例 (

    SLOW_REQUEST_RATIO

    ):選擇以慢調用比例作為門檻值,需要設定允許的慢調用 RT(即最大的響應時間),請求的響應時間大于該值則統計為慢調用。當機關統計時長(

    statIntervalMs

    )内請求數目大于設定的最小請求數目,并且慢調用的比例大于門檻值,則接下來的熔斷時長内請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢複狀态(HALF-OPEN 狀态),若接下來的一個請求響應時間小于設定的慢調用 RT 則結束熔斷,若大于設定的慢調用 RT 則會再次被熔斷。
  • 異常比例 (

    ERROR_RATIO

    ):當機關統計時長(

    statIntervalMs

    )内請求數目大于設定的最小請求數目,并且異常的比例大于門檻值,則接下來的熔斷時長内請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢複狀态(HALF-OPEN 狀态),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。異常比率的門檻值範圍是

    [0.0, 1.0]

    ,代表 0% - 100%。
  • 異常數 (

    ERROR_COUNT

    ):當機關統計時長内的異常數目超過門檻值之後會自動進行熔斷。經過熔斷時長後熔斷器會進入探測恢複狀态(HALF-OPEN 狀态),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。

Tips: 異常降級僅針對業務異常,對 Sentinel 限流降級本身的異常(

BlockException

)不生效。為了統計異常比例或異常數,需要通過

Tracer.trace(ex)

記錄業務異常。

Entry entry = null;
try {
  entry = SphU.entry(key, EntryType.IN, key);

  // Write your biz code here.
  // <<BIZ CODE>>
} catch (Throwable t) {
  if (!BlockException.isBlockException(t)) {
    Tracer.trace(t);
  }
} finally {
  if (entry != null) {
    entry.exit();
  }
}
           

6.2 熔斷器事件監聽

Sentinel 支援注冊自定義的事件監聽器監聽熔斷器狀态變換事件(state change event)。
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
    (prevState, newState, rule, snapshotValue) -> {
        if (newState == State.OPEN) {
            // 變換至 OPEN state 時會攜帶觸發時的值
            System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
                TimeUtil.currentTimeMillis(), snapshotValue));
        } else {
            System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
                TimeUtil.currentTimeMillis()));
        }
    });
           

6.4 示例

public class SlowRatioCircuitBreakerDemo {

    private static final String KEY = "some_method";

    private static volatile boolean stop = false;
    private static int seconds = 120;

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

    public static void main(String[] args) throws Exception {
        initDegradeRule();
        registerStateChangeObserver();
        startTick();

        int concurrency = 8;
        for (int i = 0; i < concurrency; i++) {
            Thread entryThread = new Thread(() -> {
                while (true) {
                    Entry entry = null;
                    try {
                        entry = SphU.entry(KEY);
                        pass.incrementAndGet();
                        // RT: [40ms, 60ms)
                        sleep(ThreadLocalRandom.current().nextInt(40, 60));
                    } catch (BlockException e) {
                        block.incrementAndGet();
                        sleep(ThreadLocalRandom.current().nextInt(5, 10));
                    } finally {
                        total.incrementAndGet();
                        if (entry != null) {
                            entry.exit();
                        }
                    }
                }
            });
            entryThread.setName("sentinel-simulate-traffic-task-" + i);
            entryThread.start();
        }
    }

    private static void registerStateChangeObserver() {
        EventObserverRegistry.getInstance().addStateChangeObserver("logging",
            (prevState, newState, rule, snapshotValue) -> {
                if (newState == State.OPEN) {
                    System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
                        TimeUtil.currentTimeMillis(), snapshotValue));
                } else {
                    System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
                        TimeUtil.currentTimeMillis()));
                }
            });
    }

    private static void initDegradeRule() {
        List<DegradeRule> rules = new ArrayList<>();
        DegradeRule rule = new DegradeRule(KEY)
            .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
            // Max allowed response time
            .setCount(50)
            // Retry timeout (in second)
            .setTimeWindow(10)
            // Circuit breaker opens when slow request ratio > 60%
            .setSlowRatioThreshold(0.6)
            .setMinRequestAmount(100)
            .setStatIntervalMs(20000);
        rules.add(rule);

        DegradeRuleManager.loadRules(rules);
        System.out.println("Degrade rule loaded: " + rules);
    }

    private static void sleep(int timeMs) {
        try {
            TimeUnit.MILLISECONDS.sleep(timeMs);
        } catch (InterruptedException e) {
            // ignore
        }
    }

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

    static class TimerTask implements Runnable {
        @Override
        public void run() {
            long start = System.currentTimeMillis();
            System.out.println("Begin to run! Go go go!");
            System.out.println("See corresponding metrics.log for accurate statistic data");

            long oldTotal = 0;
            long oldPass = 0;
            long oldBlock = 0;

            while (!stop) {
                sleep(1000);

                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(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);
        }
    }
}
           

第七章:系統自适應保護

Sentinel 系統自适應保護從整體次元對應用入口流量進行控制,結合應用的 Load、總體平均 RT、入口 QPS 和線程數等幾個次元的監控名額,讓系統的入口流量和系統的負載達到一個平衡,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。

7.1 概述

​ 系統自适應保護的思路是根據硬名額,即系統的負載 (load1) 來做系統過載保護。當系統負載高于某個門檻值,就禁止或者減少流量的進入;當 load 開始好轉,則恢複流量的進入。這個思路給我們帶來了不可避免的兩個問題:

  • load 是一個“果”,如果根據 load 的情況來調節流量的通過率,那麼就始終有延遲性。也就意味着通過率的任何調整,都會過一段時間才能看到效果。目前通過率是使 load 惡化的一個動作,那麼也至少要過 1 秒之後才能觀測到;同理,如果目前通過率調整是讓 load 好轉的一個動作,也需要 1 秒之後才能繼續調整,這樣就浪費了系統的處理能力。是以我們看到的曲線,總是會有抖動。
  • 恢複慢。想象一下這樣的一個場景(真實),出現了這樣一個問題,下遊應用不可靠,導緻應用 RT 很高,進而 load 到了一個很高的點。過了一段時間之後下遊應用恢複了,應用 RT 也相應減少。這個時候,其實應該大幅度增大流量的通過率;但是由于這個時候 load 仍然很高,通過率的恢複仍然不高。

​ 根據 TCP BBR 的思想,根據系統能夠處理的請求,和允許進來的請求,來做平衡,而不是根據一個間接的名額(系統 load)來做限流。最終實作的是 在系統不被拖垮的情況下,提高系統的吞吐率,而不是 load 一定要到低于某個門檻值。

​ 如果我們還是按照固有的思維,超過特定的 load 就禁止流量進入,系統 load 恢複就放開流量,這樣做的結果是無論我們怎麼調參數,調比例,都是按照果來調節因,都無法取得良好的效果。

​ Sentinel 在系統自适應保護的做法是,用 load1 作為啟動控制流量的值,而允許通過的流量由處理請求的能力,即請求的響應時間以及目前系統正在處理的請求速率來決定。

7.2 系統規則

系統保護規則是從應用級别的入口流量進行控制,從單台機器的總體 Load、RT、入口 QPS 和線程數四個次元監控應用資料,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。

系統保護規則是應用整體次元的,而不是資源次元的,并且僅對入口流量生效。入口流量指的是進入應用的流量(

EntryType.IN

),比如 Web 服務或 Dubbo 服務端接收的請求,都屬于入口流量。

系統規則支援以下的門檻值類型:

  • Load(僅對 Linux/Unix-like 機器生效):當系統 load1 超過門檻值,且系統目前的并發線程數超過系統容量時才會觸發系統保護。系統容量由系統的

    maxQps * minRt

    計算得出。設定參考值一般是

    CPU cores * 2.5

  • CPU usage(1.5.0+ 版本):當系統 CPU 使用率超過門檻值即觸發系統保護(取值範圍 0.0-1.0)。
  • RT:當單台機器上所有入口流量的平均 RT 達到門檻值即觸發系統保護,機關是毫秒。
  • 線程數:當單台機器上所有入口流量的并發線程數達到門檻值即觸發系統保護。
  • 入口 QPS:當單台機器上所有入口流量的 QPS 達到門檻值即觸發系統保護。

7.3 原理

Alibaba Sentinel 學習筆記Sentinel

我們把系統處理請求的過程想象為一個水管,到來的請求是往這個水管灌水,當系統處理順暢的時候,請求不需要排隊,直接從水管中穿過,這個請求的RT是最短的;反之,當請求堆積的時候,那麼處理請求的時間則會變為:排隊時間 + 最短處理時間。

  • 推論一: 如果我們能夠保證水管裡的水量,能夠讓水順暢的流動,則不會增加排隊的請求;也就是說,這個時候的系統負載不會進一步惡化。

我們用 T 來表示(水管内部的水量),用RT來表示請求的處理時間,用P來表示進來的請求數,那麼一個請求從進入水管道到從水管出來,這個水管會存在

P * RT

 個請求。換一句話來說,當

T ≈ QPS * Avg(RT)

的時候,我們可以認為系統的處理能力和允許進入的請求個數達到了平衡,系統的負載不會進一步惡化。

接下來的問題是,水管的水位是可以達到了一個平衡點,但是這個平衡點隻能保證水管的水位不再繼續增高,但是還面臨一個問題,就是在達到平衡點之前,這個水管裡已經堆積了多少水。如果之前水管的水已經在一個量級了,那麼這個時候系統允許通過的水量可能隻能緩慢通過,RT會大,之前堆積在水管裡的水會滞留;反之,如果之前的水管水位偏低,那麼又會浪費了系統的處理能力。

  • 推論二: 當保持入口的流量是水管出來的流量的最大的值的時候,可以最大利用水管的處理能力。

然而,和 TCP BBR 的不一樣的地方在于,還需要用一個系統負載的值(load1)來激發這套機制啟動。

注:這種系統自适應算法對于低 load 的請求,它的效果是一個“兜底”的角色。對于不是應用本身造成的 load 高的情況(如其它程序導緻的不穩定的情況),效果不明顯。

7.4 示例

public class SystemGuardDemo {

    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 = 100;

    private static int seconds = 60 + 40;

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

        tick();
        initSystemRule();

        for (int i = 0; i < threadCount; i++) {
            Thread entryThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        Entry entry = null;
                        try {
                            entry = SphU.entry("methodA", EntryType.IN);
                            pass.incrementAndGet();
                            try {
                                TimeUnit.MILLISECONDS.sleep(20);
                            } catch (InterruptedException e) {
                                // ignore
                            }
                        } catch (BlockException e1) {
                            block.incrementAndGet();
                            try {
                                TimeUnit.MILLISECONDS.sleep(20);
                            } catch (InterruptedException e) {
                                // ignore
                            }
                        } catch (Exception e2) {
                            // biz exception
                        } finally {
                            total.incrementAndGet();
                            if (entry != null) {
                                entry.exit();
                            }
                        }
                    }
                }

            });
            entryThread.setName("working-thread");
            entryThread.start();
        }
    }

    private static void initSystemRule() {
        List<SystemRule> rules = new ArrayList<SystemRule>();
        SystemRule rule = new SystemRule();
        // max load is 3
        rule.setHighestSystemLoad(3.0);
        // max cpu usage is 60%
        rule.setHighestCpuUsage(0.6);
        // max avg rt of all request is 10 ms
        rule.setAvgRt(10);
        // max total qps is 20
        rule.setQps(20);
        // max parallel working thread is 10
        rule.setMaxThread(10);

        rules.add(rule);
        SystemRuleManager.loadRules(Collections.singletonList(rule));
    }

    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() {
            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 + ", " + TimeUtil.currentTimeMillis() + ", total:"
                    + oneSecondTotal + ", pass:"
                    + oneSecondPass + ", block:" + oneSecondBlock);
                if (seconds-- <= 0) {
                    stop = true;
                }
            }
            System.exit(0);
        }
    }
}
           

第八章:叢集流量控制

8.1 概述

**叢集流控最基礎的方式:**假設我們希望給某個使用者限制調用某個 API 的總 QPS 為 50,但機器數可能很多(比如有 100 台)。這時候我們很自然地就想到,找一個 server 來專門來統計總的調用量,其它的執行個體都與這台 server 通信來判斷是否可以調用。

另外叢集流控還可以解決流量不均勻導緻總體限流效果不佳的問題。假設叢集中有 10 台機器,我們給每台機器設定單機限流門檻值為 10 QPS,理想情況下整個叢集的限流門檻值就為 100 QPS。不過實際情況下流量到每台機器可能會不均勻,會導緻總量沒有到的情況下某些機器就開始限流。是以僅靠單機次元去限制的話會無法精确地限制總體流量。而叢集流控可以精确地控制整個叢集的調用總量,結合單機限流兜底,可以更好地發揮流量控制的效果。

叢集流控兩種身份:

  • Token Client:叢集流控用戶端,用于向所屬 Token Server 通信請求 token。叢集限流服務端會傳回給用戶端結果,決定是否限流。
  • Token Server:即叢集流控服務端,處理來自 Token Client 的請求,根據配置的叢集規則判斷是否應該發放 token(是否允許通過)
Alibaba Sentinel 學習筆記Sentinel

8.2 子產品結構

叢集流控子產品需求的版本為 JDK 1.7+
  • sentinel-cluster-common-default

    : 公共子產品,包含公共接口和實體
  • sentinel-cluster-client-default

    : 預設叢集流控 client 子產品,使用 Netty 進行通信,提供接口友善序列化協定擴充
  • sentinel-cluster-server-default

    : 預設叢集流控 server 子產品,使用 Netty 進行通信,提供接口友善序列化協定擴充;同時提供擴充接口對接規則判斷的具體實作(

    TokenService

    ),預設實作是複用

    sentinel-core

    的相關邏輯

8.3 叢集流控規則

8.3.1 規則

FlowRule

添加了兩個字段用于叢集限流相關配置:

private boolean clusterMode; // 辨別是否為叢集限流配置
private ClusterFlowConfig clusterConfig; // 叢集限流相關配置項
           

其中 用一個專門的

ClusterFlowConfig

代表叢集限流相關配置項,以與現有規則配置項分開:

// 全局唯一的規則 ID,由叢集限流管控端配置設定.
private Long flowId;

// 門檻值模式,預設(0)為單機均攤,1 為全局門檻值.
private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL;

private int strategy = ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL;

// 在 client 連接配接失敗或通信失敗時,是否退化到本地的限流模式
private boolean fallbackToLocalWhenFail = true;
           
  • flowId

    代表全局唯一的規則 ID,Sentinel 叢集限流服務端通過此 ID 來區分各個規則,是以務必保持全局唯一。一般 flowId 由統一的管控端進行配置設定,或寫入至 DB 時生成。
  • thresholdType

    代表叢集限流門檻值模式。其中單機均攤模式下配置的門檻值等同于單機能夠承受的限額,token server 會根據用戶端對應的 namespace(預設為

    project.name

    定義的應用名)下的連接配接數來計算總的門檻值(比如獨立模式下有 3 個 client 連接配接到了 token server,然後配的單機均攤門檻值為 10,則計算出的叢集總量就為 30);而全局模式下配置的門檻值等同于整個叢集的總門檻值。

8.3.2 配置方式

建議使用動态規則源來動态地管理規則。

TokenClient

ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser);
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
           

TokenServer

由于叢集限流服務端有作用域(namespace)的概念,是以我們需要注冊一個自動根據 namespace 生成動态規則源的 PropertySupplier
// Supplier 類型:接受 namespace,傳回生成的動态規則源,類型為 SentinelProperty<List<FlowRule>>
// ClusterFlowRuleManager 針對叢集限流規則,ClusterParamFlowRuleManager 針對叢集熱點規則,配置方式類似
ClusterFlowRuleManager.setPropertySupplier(namespace -> {
    return new SomeDataSource(namespace).getProperty();
});
           

​ 每當叢集限流服務端 namespace set 産生變更時,Sentinel 會自動針對新加入的 namespace 生成動态規則源并進行自動監聽,并删除舊的不需要的規則源。

8.4 叢集限流用戶端

  1. 引入 jar 包
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-cluster-client-default</artifactId>
        <version>1.7.1</version>
    </dependency>
               
  2. 可以通過 API 将目前模式置為用戶端模式:

    http://<ip>:<port>/setClusterMode?mode=<xxx>

    mode

    為 0 代表

    client

    ,1 代表

    server

    。設定成功後,若已有用戶端的配置,叢集限流用戶端将會開啟并連接配接遠端的 token server。我們可以在

    sentinel-record.log

    日志中檢視連接配接的相關日志。
  3. 若叢集限流用戶端未進行配置,則使用者需要對用戶端進行基本的配置,比如指定叢集限流

    token server

    http://<ip>:<port>/cluster/client/modifyConfig?data=<config>

    其中 data 是 JSON 格式的

    ClusterClientConfig

    ,對應的配置項:
    • serverHost

      : token server host
    • serverPort

      : token server 端口
    • requestTimeout

      : 請求的逾時時間(預設為 20 ms)
  4. 也可以通過

    ClusterClientConfigManager

    register2Property

    方法注冊動态配置源。配置源注冊的相關邏輯可以置于

    InitFunc

    實作類中,并通過 SPI 注冊,在 Sentinel 初始化時即可自動進行配置源加載監聽。
  5. 若使用者未引入叢集限流 client 相關依賴,或者 client 未開啟/連接配接失敗/通信失敗,則對于開啟了叢集模式的規則:
    • 叢集熱點限流預設直接通過
    • 普通叢集限流會退化到 local 模式的限流,即在本地按照單機門檻值執行限流檢查
  6. 當 token client 與 server 之間的連接配接意外斷開時,token client 會不斷進行重試,每次重試的間隔時間以

    n * 2000 ms

    的形式遞增。

8.5 叢集限流服務端

8.5.1 引入 jar 包

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-cluster-server-default</artifactId>
    <version>1.7.1</version>
</dependency>
           

8.5.2 啟動方式

Sentinel 叢集限流服務端有兩種啟動方式

  • 獨立模式(Alone),即作為獨立的 token server 程序啟動,獨立部署,隔離性好,但是需要額外的部署操作。獨立模式适合作為 Global Rate Limiter 給叢集提供流控服務。
  • 嵌入模式(Embedded),即作為内置的 token server 與服務在同一程序中啟動。在此模式下,叢集中各個執行個體都是對等的,token server 和 client 可以随時進行轉變,是以無需單獨部署,靈活性比較好。但是隔離性不佳,需要限制 token server 的總 QPS,防止影響應用本身。嵌入模式适合某個應用叢集内部的流控。

8.5.3 啟動操作

  1. 轉換叢集流控身份:

    http://<ip>:<port>/setClusterMode?mode=<xxx>

    其中 mode 為 代表 client,

    1

    代表 server,

    -1

    代表關閉。注意應用端需要引入叢集限流用戶端或服務端的相應依賴。
  2. 在獨立模式下,我們可以直接建立對應的

    ClusterTokenServer

    執行個體并在 main 函數中通過

    start

    方法啟動 Token Server。

8.5.2 規則配置

8.5.3 屬性配置

推薦給叢集限流服務端注冊動态配置源來動态地進行配置.

配置類型:

  • namespace set: 叢集限流服務端服務的作用域(命名空間),可以設定為自己服務的應用名。叢集限流 client 在連接配接到 token server 後會上報自己的命名空間(預設為

    project.name

    配置的應用名),token server 會根據上報的命名空間名稱統計連接配接數。
  • transport config: 叢集限流服務端通信相關配置,如 server port
  • flow config: 叢集限流服務端限流相關配置,如滑動視窗統計時長、格子數目、最大允許總 QPS等

Tips:

  1. 可以通過

    ClusterServerConfigManager

    的各個

    registerXxxProperty

    方法來注冊相關的配置源。
  2. 從 1.4.1 版本開始,Sentinel 支援給 token server 配置最大允許的總 QPS(

    maxAllowedQps

    ),來對 token server 的資源使用進行限制,防止在嵌入模式下影響應用本身。

8.6 Token Server 配置設定配置

Alibaba Sentinel 學習筆記Sentinel

8.7 示例

8.7.1 獨立模式

Alibaba Sentinel 學習筆記Sentinel
public class ClusterServerDemo {

    public static void main(String[] args) throws Exception {
        // Not embedded mode by default (alone mode).
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();

        // A sample for manually load config for cluster server.
        // It's recommended to use dynamic data source to cluster manage config and rules.
        // See the sample in DemoClusterServerInitFunc for detail.
        ClusterServerConfigManager.loadGlobalTransportConfig(new ServerTransportConfig()
            .setIdleSeconds(600)
            .setPort(11111));
        ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton(DemoConstants.APP_NAME));

        // Start the server.
        tokenServer.start();
    }
}
           

8.7.2 嵌入模式

注意:若在本地啟動多個 Demo 示例,需要加上

-Dcsp.sentinel.log.use.pid=true

參數,否則控制台顯示監控會不準确。

sentinel-demo-cluster-embedded

Alibaba Sentinel 學習筆記Sentinel
POM 檔案依賴
<groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-transport-simple-http</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-parameter-flow-control</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-cluster-client-default</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-cluster-server-default</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- Nacos for dynamic data source -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

        <!-- for a real web demo -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-annotation-aspectj</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    </dependencies>
           

8.8 叢集限流控制台

開源版本控制台 。使用叢集限流功能需要對 Sentinel 控制台進行相關的改造,推送規則時直接推送至配置中心,接入端引入 push 模式的動态資料源。

8.9 擴充接口設計

8.9.1 整體擴充架構

Alibaba Sentinel 學習筆記Sentinel

8.9.2 通用擴充接口

以下通用接口位于

sentinel-core

中:

  • TokenService

    : 叢集限流功能接口,server / client 均可複用
  • ClusterTokenClient

    : 叢集限流功能用戶端
  • ClusterTokenServer

    : 叢集限流服務端接口
  • EmbeddedClusterTokenServer

    : 叢集限流服務端接口(embedded 模式)

以下通用接口位于

sentinel-cluster-common-default

:

  • EntityWriter

  • EntityDecoder

8.9.3 Client 擴充接口

叢集流控 Client 端通信相關擴充接口:

  • ClusterTransportClient

    :叢集限流通信用戶端
  • RequestEntityWriter

  • ResponseEntityDecoder

8.9.4 Server 擴充接口

叢集流控 Server 端通信相關擴充接口:

  • ResponseEntityWriter

  • RequestEntityDecoder

叢集流控 Server 端請求處理擴充接口:

  • RequestProcessor

    : 請求處理接口 (request -> response)

第九章:網關流量控制

Alibaba Sentinel 學習筆記Sentinel

9.1 概述

Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 子產品,此子產品中包含網關限流的規則和自定義 API 的實體和管理邏輯:

  • GatewayFlowRule

    :網關限流規則,針對 API Gateway 的場景定制的限流規則,可以針對不同 route 或自定義的 API 分組進行限流,支援針對請求中的參數、Header、來源 IP 等進行定制化的限流。
  • ApiDefinition

    :使用者自定義的 API 定義分組,可以看做是一些 URL 比對的組合。比如我們可以定義一個 API 叫

    my_api

    ,請求 path 模式為

    /foo/**

    /baz/**

    的都歸到

    my_api

    這個 API 分組下面。限流的時候可以針對這個自定義的 API 分組次元進行限流。

其中網關限流規則

GatewayFlowRule

的字段解釋如下:

  • resource

    :資源名稱,可以是網關中的 route 名稱或者使用者自定義的 API 分組名稱。
  • resourceMode

    :規則是針對 API Gateway 的 route(

    RESOURCE_MODE_ROUTE_ID

    )還是使用者在 Sentinel 中定義的 API 分組(

    RESOURCE_MODE_CUSTOM_API_NAME

    ),預設是 route。
  • grade

    :限流名額次元,同限流規則的

    grade

    字段。
  • count

    :限流門檻值
  • intervalSec

    :統計時間視窗,機關是秒,預設是 1 秒。
  • controlBehavior

    :流量整形的控制效果,同限流規則的

    controlBehavior

    字段,目前支援快速失敗和勻速排隊兩種模式,預設是快速失敗。
  • burst

    :應對突發請求時額外允許的請求數目。
  • maxQueueingTimeoutMs

    :勻速排隊模式下的最長排隊時間,機關是毫秒,僅在勻速排隊模式下生效。
  • paramItem
               
    :參數限流配置。若不提供,則代表不針對參數進行限流,該網關規則将會被轉換成普通流控規則;否則會轉換成熱點規則。其中的字段:
    • parseStrategy

      :從請求中提取參數的政策,目前支援提取來源 IP(

      PARAM_PARSE_STRATEGY_CLIENT_IP

      )、Host(

      PARAM_PARSE_STRATEGY_HOST

      )、任意 Header(

      PARAM_PARSE_STRATEGY_HEADER

      )和任意 URL 參數(

      PARAM_PARSE_STRATEGY_URL_PARAM

      )四種模式。
    • fieldName

      :若提取政策選擇 Header 模式或 URL 參數模式,則需要指定對應的 header 名稱或 URL 參數名稱。
    • pattern

      :參數值的比對模式,隻有比對該模式的請求屬性值會納入統計和流控;若為空則統計該請求屬性的所有值。(1.6.2 版本開始支援)
    • matchStrategy

      :參數值的比對政策,目前支援精确比對(

      PARAM_MATCH_STRATEGY_EXACT

      )、子串比對(

      PARAM_MATCH_STRATEGY_CONTAINS

      )和正則比對(

      PARAM_MATCH_STRATEGY_REGEX

      )。(1.6.2 版本開始支援)

使用者可以通過

GatewayRuleManager.loadRules(rules)

手動加載網關規則,或通過

GatewayRuleManager.register2Property(property)

注冊動态規則源動态推送(推薦方式)。

9.2 Spring Cloud Gateway

Sentinel 提供了 Spring Cloud Gateway 的适配子產品,提供兩種資源次元的限流:

  • **route 次元:**即在 Spring 配置檔案中配置的路由條目,資源名為對應的 routeId
  • **自定義 API 次元:**使用者可以利用 Sentinel 提供的 API 來自定義一些 API 分組

POM 依賴

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    <version>x.y.z</version>
</dependency>
           

Java 使用

使用時隻需注入對應的

SentinelGatewayFilter

執行個體以及

SentinelGatewayBlockExceptionHandler

執行個體即可。示例:
初始化配置
@Configuration
public class GatewayConfiguration {

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

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }
}
           
Application 配置
server:
  port: 8090
spring:
  application:
    name: spring-cloud-gateway
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          lower-case-service-id: true
      routes:
        # Add your routes here.
        - id: product_route
          uri: lb://product
          predicates:
            - Path=/product/**
        - id: httpbin_route
          uri: https://httpbin.org
          predicates:
            - Path=/httpbin/**
          filters:
            - RewritePath=/httpbin/(?<segment>.*), /$\{segment}
           
資源分組配置
private void initCustomizedApis() {
    Set<ApiDefinition> definitions = new HashSet<>();
    ApiDefinition api1 = new ApiDefinition("some_customized_api")
        .setPredicateItems(new HashSet<ApiPredicateItem>() {{
            add(new ApiPathPredicateItem().setPattern("/product/baz"));
            add(new ApiPathPredicateItem().setPattern("/product/foo/**")
                .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX));
        }});
    ApiDefinition api2 = new ApiDefinition("another_customized_api")
        .setPredicateItems(new HashSet<ApiPredicateItem>() {{
            add(new ApiPathPredicateItem().setPattern("/ahas"));
        }});
    definitions.add(api1);
    definitions.add(api2);
    GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
           

如上:那麼這裡面的 route ID(如

product_route

)和 API name(如

some_customized_api

)都會被辨別為 Sentinel 的資源。比如通路網關的 URL 為

http://localhost:8090/product/foo/22

的時候,對應的統計會加到

product_route

some_customized_api

這兩個資源上面,而

http://localhost:8090/httpbin/json

隻會對應到

httpbin_route

資源上面。:

您可以在

GatewayCallbackManager

注冊回調進行定制:

  • setBlockHandler

    :注冊函數用于實作自定義的邏輯處理被限流的請求,對應接口為

    BlockRequestHandler

    。預設實作為

    DefaultBlockRequestHandler

    ,當被限流時會傳回類似于下面的錯誤資訊:

    Blocked by Sentinel: FlowException

9.3 網關流控實作原理

  1. 當通過

    GatewayRuleManager

    加載網關流控規則(

    GatewayFlowRule

    )時,無論是否針對請求屬性進行限流,Sentinel 底層都會将網關流控規則轉化為熱點參數規則(

    ParamFlowRule

    ),存儲在

    GatewayRuleManager

    中,與正常的熱點參數規則相隔離。轉換時 Sentinel 會根據請求屬性配置,為網關流控規則設定參數索引(

    idx

    ),并同步到生成的熱點參數規則中。
  2. 外部請求進入 API Gateway 時會經過 Sentinel 實作的 filter,其中會依次進行 路由/API 分組比對、請求屬性解析和參數組裝。Sentinel 會根據配置的網關流控規則來解析請求屬性,并依照參數索引順序組裝參數數組,最終傳入

    SphU.entry(res, args)

    中。Sentinel API Gateway Adapter Common 子產品向 Slot Chain 中添加了一個

    GatewayFlowSlot

    ,專門用來做網關規則的檢查。

    GatewayFlowSlot

    會從

    GatewayRuleManager

    中提取生成的熱點參數規則,根據傳入的參數依次進行規則檢查。若某條規則不針對請求屬性,則會在參數最後一個位置置入預設的常量,達到普通流控的效果。

9.4 網關流控控制台

使用者可以直接在 Sentinel 控制台上檢視 API Gateway 實時的 route 和自定義 API 分組監控,管理網關規則和 API 分組配置。

在 API Gateway 端,使用者隻需要在原有啟動參數的基礎上添加如下啟動參數即可标記應用為 API Gateway 類型:

# 注:通過 Spring Cloud Alibaba Sentinel 自動接入的 API Gateway 整合則無需此參數
-Dcsp.sentinel.app.type=1
           

第十章:注解支援(注解埋點支援)

12.1 概述

Sentinel 提供了

@SentinelResource

注解用于定義資源,并提供了 AspectJ 的擴充用于自動定義資源、處理

BlockException

等。使用 Sentinel Annotation AspectJ Extension 的時候需要引入以下依賴>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>x.y.z</version>
</dependency>
           

12.2 @SentinelResource 注解

注意:注解方式埋點不支援 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 邏輯中,而是會原樣抛出。
若 blockHandler 和 fallback 都進行了配置,則被限流降級而抛出

BlockException

時隻會進入

blockHandler

處理邏輯。若未配置

blockHandler

fallback

defaultFallback

,則被限流降級時會将

BlockException

直接抛出。

示例:

public class TestService {

    // 對應的 `handleException` 函數需要位于 `ExceptionUtil` 類中,并且必須為 static 函數.
    @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
    public void test() {
        System.out.println("Test");
    }

    // 原函數
    @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
    public String hello(long s) {
        return String.format("Hello at %d", s);
    }
    
    // Fallback 函數,函數簽名與原函數一緻或加一個 Throwable 類型的參數.
    public String helloFallback(long s) {
        return String.format("Halooooo %d", s);
    }

    // Block 異常處理函數,參數最後多一個 BlockException,其餘與原函數一緻.
    public String exceptionHandler(long s, BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return "Oops, error occurred at " + s;
    }
}
           

12.3 配置式埋點

12.3.1 AspectJ

aop.xml

檔案中引入對應的 Aspect

<aspects>
    <aspect name="com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect"/>
</aspects>
           

12.3.2 Spring AOP

SentinelResourceAspect

注冊為一個 Spring Bean 進行初始化:

示例: sentinel-demo-annotation-spring-aop

@Configuration
public class SentinelAspectConfiguration {

    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }
}
           

第十一章:動态規則擴充

13.1 概述

Sentinel 的理念是開發者隻需要關注資源的定義,當資源定義成功後可以動态增加各種流控降級規則。Sentinel 提供兩種方式修改規則:

  • 通過 API 直接修改 (

    loadRules

    )
  • 通過

    DataSource

    适配不同資料源修改

例如:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規則
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級規則
           

13.2 DataSource 擴充

開發的時候規則一般存儲在檔案、資料庫或者配置中心當中。

DataSource

接口給我們提供了對接任意配置源的能力。相比直接通過 API 修改規則,實作

DataSource

接口是更加可靠的做法。

​ 推薦通過控制台設定規則後将規則推送到統一的規則中心,用戶端實作

ReadableDataSource

接口端監聽規則中心實時擷取變更,流程如下:

Alibaba Sentinel 學習筆記Sentinel

DataSource

擴充常見的實作方式有:

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

Sentinel 目前支援以下資料源擴充:

  • Pull-based: 動态檔案資料源、Consul, Eureka
  • Push-based: ZooKeeper, Redis, Nacos, Apollo, etcd

13.2.1 拉模式擴充

實作拉模式的資料源最簡單的方式是繼承

AutoRefreshDataSource

抽象類,然後實作

readSource()

方法,在該方法裡從指定資料源讀取字元串格式的配置資料。比如 基于檔案的資料源。

13.2.2 推模式擴充

實作推模式的資料源最簡單的方式是繼承

AbstractDataSource

抽象類,在其構造方法中添加監聽器,并實作

readSource()

從指定資料源讀取字元串格式的配置資料。比如 基于 Nacos 的資料源。

13.2.6 注冊資料源

  1. 将資料源注冊至指定的規則管理器中:
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser);
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
           
  1. 也可以借助 Sentinel 的

    InitFunc

    SPI 擴充接口。隻需要實作自己的

    InitFunc

    接口,在

    init

    方法中編寫注冊資料源的邏輯。
package com.test.init;

public class DataSourceInitFunc implements InitFunc {

    @Override
    public void init() throws Exception {
        final String remoteAddress = "localhost";
        final String groupId = "Sentinel:Demo";
        final String dataId = "com.alibaba.csp.sentinel.demo.flow.rule";

        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
            source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }
}
           
  1. 接着将對應的類名添加到位于資源目錄(通常是

    resource

    目錄)下的

    META-INF/services

    目錄下的

    com.alibaba.csp.sentinel.init.InitFunc

    檔案中。
com.test.init.DataSourceInitFunc
           
  1. 當初次通路任意資源的時候,Sentinel 就可以自動去注冊對應的資料源了。

13.3 示例

13.3.1 API 模式:使用用戶端規則 API 配置規則

Sentinel Dashboard 通過 Sentinel 用戶端自帶的規則 API 來實時查詢和更改記憶體中的規則。

13.3.2 拉模式:使用檔案配置規則

這個示例展示 Sentinel 是如何從檔案擷取規則資訊的。

FileRefreshableDataSource

會周期性的讀取檔案以擷取規則,當檔案有更新時會及時發現,并将規則更新到記憶體中。使用時隻需添加以下依賴:
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-extension</artifactId>
    <version>x.y.z</version>
</dependency>
           

13.3.3 推模式:使用 Nacos 配置規則

Nacos 是阿裡中間件團隊開源的服務發現和動态配置中心。Sentinel 針對 Nacos 作了适配,底層可以采用 Nacos 作為規則配置資料源。使用時隻需添加以下依賴:
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>x.y.z</version>
</dependency>
           

然後建立

NacosDataSource

并将其注冊至對應的 RuleManager 上即可。比如:

// remoteAddress 代表 Nacos 服務端的位址
// groupId 和 dataId 對應 Nacos 中相應配置
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
    source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
           

詳細示例可以參見 sentinel-demo-nacos-datasource。

13.3.4 推模式:使用 ZooKeeper 配置規則

Sentinel 針對 ZooKeeper 作了相應适配,底層可以采用 ZooKeeper 作為規則配置資料源。使用時隻需添加以下依賴:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-zookeeper</artifactId>
    <version>x.y.z</version>
</dependency>
           

然後建立

ZookeeperDataSource

并将其注冊至對應的 RuleManager 上即可。比如:

// remoteAddress 代表 ZooKeeper 服務端的位址
// path 對應 ZK 中的資料路徑
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<>(remoteAddress, path, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
           

詳細示例可以參見 sentinel-demo-zookeeper-datasource。

13.3.5 推模式:使用 Apollo 配置規則

Sentinel 針對 Apollo 作了相應适配,底層可以采用 Apollo 作為規則配置資料源。使用時隻需添加以下依賴:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-apollo</artifactId>
    <version>x.y.z</version>
</dependency>
           

然後建立

ApolloDataSource

并将其注冊至對應的 RuleManager 上即可。比如:

// namespaceName 對應 Apollo 的命名空間名稱
// ruleKey 對應規則存儲的 key
// defaultRules 對應連接配接不上 Apollo 時的預設規則
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ApolloDataSource<>(namespaceName, ruleKey, defaultRules, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
           

詳細示例可以參見 sentinel-demo-apollo-datasource。

13.3.6 推模式:使用 Redis 配置規則

Sentinel 針對 Redis 作了相應适配,底層可以采用 Redis 作為規則配置資料源。使用時隻需添加以下依賴:

<!-- 僅支援 JDK 1.8+ -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-redis</artifactId>
    <version>x.y.z</version>
</dependency>
           

Redis 動态配置源采用 Redis PUB-SUB 機制實作,詳細文檔參考:https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-redis

第十二章:日志

無論觸發了限流、熔斷降級還是系統保護,它們的秒級攔截詳情日志都在

${user_home}/logs/csp/sentinel-block.log

裡。如果沒有發生攔截,則該日志不會出現。

12.1 攔截詳情日志(block 日志)

日志格式如下:

2014-06-20 16:35:10|1|sayHello(java.lang.String,long),FlowException,default,origin|61,0
2014-06-20 16:35:11|1|sayHello(java.lang.String,long),FlowException,default,origin|1,0
           

日志含義:

index 例子 說明
1

2014-06-20 16:35:10

時間戳
2

1

該秒發生的第一個資源
3

sayHello(java.lang.String,long)

資源名稱
4

XXXException

攔截的原因, 通常

FlowException

代表是被限流規則攔截,

DegradeException

則表示被降級,

SystemBlockException

則表示被系統保護攔截
5

default

生效規則的調用來源(參數限流中代表生效的參數)
6

origin

被攔截資源的調用者,可以為空
7

61,0

61 被攔截的數量,0無意義可忽略

12.2 秒級監控日志

所有的資源都會産生秒級日志,它在

${user_home}/logs/csp/${app_name}-${pid}-metrics.log

裡。格式如下:

1532415661000|2018-07-24 15:01:01|sayHello(java.lang.String)|12|3|4|2|295
           
  1. 1532415661000

    :時間戳
  2. 2018-07-24 15:01:01

    :格式化之後的時間戳
  3. sayHello(java.lang.String)

    :資源名
  4. 12

    :表示到來的數量,即此刻通過 Sentinel 規則 check 的數量(passed QPS)
  5. 3

    :實際該資源被攔截的數量(blocked QPS)
  6. 4

    :每秒結束的資源個數(完成調用),包括正常結束和異常結束的情況(exit QPS)
  7. 2

    :異常的數量
  8. 295

    :資源的平均響應時間(RT)

12.3 業務日志

其它的日志在

${user_home}/logs/csp/sentinel-record.log.xxx

裡。該日志包含規則的推送、接收、處理等記錄,排查問題的時候會非常友善。

12.4 叢集限流日志

  • ${log_dir}/sentinel-cluster-client.log

    :Token Client 日志,會記錄請求失敗的資訊

第十三章:實時監控

13.1 概述

Sentinel 提供對所有資源的實時監控。如果需要實時監控,用戶端需引入以下依賴(以 Maven 為例):
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>x.y.z</version>
</dependency>
           

引入上述依賴後,用戶端便會主動連接配接 Sentinel 控制台。通過 Sentinel 控制台 即可檢視用戶端的實時監控。

13.2 簇點監控

13.2.1 擷取簇點清單

相關 API:

GET /clusterNode

當應用啟動之後,可以運作下列指令,獲得目前所有簇點(

ClusterNode

)的清單(JSON 格式):

curl http://localhost:8719/clusterNode
           

結果示例:

[
 {"avgRt":0.0, //平均響應時間
 "blockRequest":0, //每分鐘攔截的請求個數
 "blockedQps":0.0, //每秒攔截個數
 "curThreadNum":0, //并發個數
 "passQps":1.0, // 每秒成功通過請求
 "passReqQps":1.0, //每秒到來的請求
 "resourceName":"/registry/machine", 資源名稱
 "timeStamp":1529905824134, //時間戳
 "totalQps":1.0, // 每分鐘請求數
 "totalRequest":193}, 
  ....
]
           

13.2.2 查詢某個簇點的詳細資訊

模糊查詢該簇點的具體資訊,其中

id

對應 resource name,支援模糊查詢:

curl http://localhost:8719/cnode?id=xxxx
           

結果示例:

idx id                                thread    pass      blocked   success    total    aRt   1m-pass   1m-block   1m-all   exeption   
6   /app/aliswitch2/machines.json     0         0         0         0          0        0     0         0          0        0          
7   /app/sentinel-admin/machines.json 0         1         0         1          1        6     0         0          0        0          
8   /identity/machine.json            0         0         0         0          0        0     0         0          0        0          
9   /registry/machine                 0         2         0         2          2        1     192       0          192      0          
10  /app/views/machine.html           0         1         0         1          1        2     0         0          0        0    
           

13.2.3 簇點調用者統計資訊

查詢該簇點的調用者統計資訊:

curl http://localhost:8719/origin?id=xxxx
           

結果示例:

id: nodeA
idx origin  threadNum passedQps blockedQps totalQps aRt   1m-passed 1m-blocked 1m-total 
1   caller1 0         0         0          0        0     0         0          0        
2   caller2 0         0         0          0        0     0         0          0      
           

其中的 origin 由

ContextUtil.enter(resourceName,origin)

方法中的

origin

指定。

13.3 鍊路監控

通過指令 

curl http://localhost:8719/tree

來查詢鍊路入口的鍊路樹形結構:

EntranceNode: machine-root(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
-EntranceNode1: Entrance1(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
--nodeA(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
-EntranceNode2: Entrance1(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
--nodeA(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)

t:threadNum  pq:passQps  bq:blockedQps  tq:totalQps  rt:averageRt  prq: passRequestQps 1mp:1m-passed 1mb:1m-blocked 1mt:1m-total
           

13.4 曆史資源資料

13.4.1 資源的秒級日志

所有資源的秒級日志在

${home}/logs/csp/${appName}-${pid}-metrics.log.${date}.xx

。例如,該日志的名字可能為

app-3518-metrics.log.2018-06-22.1

1529573107000|2018-06-21 17:25:07|sayHello(java.lang.String,long)|10|3601|10|0|2
           
index 例子 說明
1

1529573107000

時間戳
2

2018-06-21 17:25:07

日期
3

sayHello(java.lang.String,long)

資源名稱
4

10

每秒通過的資源請求個數
5

3601

每秒資源被攔截的個數
6

10

每秒結束的資源個數,包括正常結束和異常結束的情況
7 每秒資源的異常個數
8

2

資源平均響應時間

13.4.2 被攔截的秒級日志

每秒的攔截日志也會出現在

<使用者目錄>/logs/csp/sentinel-block.log

檔案下。如果沒有發生攔截,則該日志不會出現。

2014-06-20 16:35:10|1|sayHello(java.lang.String,long),FlowException,default,origin|61,0
2014-06-20 16:35:11|1|sayHello(java.lang.String,long),FlowException,default,origin|1,0
           
index 例子 說明
1

2014-06-20 16:35:10

時間戳
2

1

該秒發生的第一個資源
3

sayHello(java.lang.String,long)

資源名稱
4

XXXException

攔截的原因, 通常

FlowException

代表是被限流規則攔截,

DegradeException

則表示被降級,

SystemException

則表示被系統保護攔截
5

default

生效規則的調用應用
6

origin

被攔截資源的調用者。可以為空
7

61,0

61 被攔截的數量,0則代表可以忽略

13.1 實時查詢

相關 API:

GET /metric

curl http://localhost:8719/metric?identity=XXX&startTime=XXXX&endTime=XXXX&maxLines=XXXX
           

需指定以下 URL 參數:

  • identity

    :資源名稱
  • startTime

    :開始時間(時間戳)
  • endTime

    :結束時間
  • maxLines

    :監控資料最大行數

傳回和 資源的秒級日志 格式一樣的内容。例如:

1529998904000|2018-06-26 15:41:44|abc|100|0|0|0|0
1529998905000|2018-06-26 15:41:45|abc|4|5579|104|0|728
1529998906000|2018-06-26 15:41:46|abc|0|15698|0|0|0
1529998907000|2018-06-26 15:41:47|abc|0|19262|0|0|0
1529998908000|2018-06-26 15:41:48|abc|0|19502|0|0|0
1529998909000|2018-06-26 15:41:49|abc|0|18386|0|0|0
1529998910000|2018-06-26 15:41:50|abc|0|19189|0|0|0
1529998911000|2018-06-26 15:41:51|abc|0|16543|0|0|0
1529998912000|2018-06-26 15:41:52|abc|0|18471|0|0|0
1529998913000|2018-06-26 15:41:53|abc|0|19405|0|0|0
           

第十四章:啟動配置項

14.1 配置方式

若您的應用為 Spring Boot 或 Spring Cloud 應用,您可以使用 Spring Cloud Alibaba,通過 Spring 配置檔案來指定配置,詳情請參考 Spring Cloud Alibaba Sentinel 文檔。

Sentinel 提供如下的配置方式:

  • JVM -D 參數方式
  • properties 檔案方式(1.7.0 版本開始支援)

其中,

project.name

參數隻能通過 JVM -D 參數方式配置(since 1.8.0 取消該限制),其它參數支援所有的配置方式。

優先級順序:JVM -D 參數的優先級最高。若 properties 和 JVM 參數中有相同項的配置,以 JVM 參數配置的為準。

使用者可以通過

-Dcsp.sentinel.config.file

參數配置 properties 檔案的路徑,支援 classpath 路徑配置(如

classpath:sentinel.properties

)。預設 Sentinel 會嘗試從

classpath:sentinel.properties

檔案讀取配置,讀取編碼預設為 UTF-8。

14.2 配置項清單

14.2.1 sentinel-core 的配置項

基礎配置項
名稱 含義 類型 預設值 是否必需 備注

project.name

指定應用的名稱

String

null

csp.sentinel.app.type

指定應用的類型

int

0 (

APP_TYPE_COMMON

)
1.6.0 引入

csp.sentinel.metric.file.single.size

單個監控日志檔案的大小

long

52428800 (50MB)

csp.sentinel.metric.file.total.count

監控日志檔案的總數上限

int

6

csp.sentinel.statistic.max.rt

最大的有效響應時長(ms),超出此值則按照此值記錄

int

4900 1.4.1 引入

csp.sentinel.spi.classloader

SPI 加載時使用的 ClassLoader,預設為給定類的 ClassLoader

String

default

若配置

context

則使用 thread context ClassLoader。1.7.0 引入

roject.name

項用于指定應用名(appName)。若未指定,則預設解析 main 函數的類名作為應用名。實際項目使用中建議手動指定應用名。
日志相關配置項
名稱 含義 類型 預設值 是否必需 備注

csp.sentinel.log.dir

Sentinel 日志檔案目錄

String

${user.home}/logs/csp/

1.3.0 引入

csp.sentinel.log.use.pid

日志檔案名中是否加入程序号,用于單機部署多個應用的情況

boolean

false

1.3.0 引入

csp.sentinel.log.output.type

Record 日志輸出的類型,

file

代表輸出至檔案,

console

代表輸出至終端

String

file

1.6.2 引入
若需要在單台機器上運作相同服務的多個執行個體,則需要加入

-Dcsp.sentinel.log.use.pid=true

來保證不同執行個體日志的獨立性。

14.2.2 sentinel-transport-common 的配置項

名稱 含義 類型 預設值 是否必需

csp.sentinel.dashboard.server

控制台的位址,指定控制台後用戶端會自動向該位址發送心跳包。位址格式為:

hostIp:port

String

null

csp.sentinel.heartbeat.interval.ms

心跳包發送周期,機關毫秒

long

null

非必需,若不進行配置,則會從相應的

HeartbeatSender

中提取預設值

csp.sentinel.api.port

本地啟動 HTTP API Server 的端口号

int

8719

csp.sentinel.heartbeat.client.ip

指定心跳包中本機的 IP

String

- 若不指定則通過

HostNameUtil

解析;該配置項多用于多網卡環境

csp.sentinel.api.port

可不提供,預設為 8719,若端口沖突會自動向下探測可用的端口。

第十五章:Sentinel 實戰執行個體

15.1 Redis Sentinel 叢集容災部署

繼續閱讀