開發與維運

超詳細的Sentinel入門

文章已收錄Github精選,歡迎Starhttps://github.com/yehongzhi/learningSummary

一、什麼是Sentinel

Sentinel定位是分佈式系統的流量防衛兵。目前互聯網應用基本上都使用微服務,微服務的穩定性是一個很重要的問題,而限流、熔斷降級是微服務保持穩定的一個重要的手段。

下面看官網的一張圖,瞭解一下Sentinel的主要特性:
在這裡插入圖片描述
在Sentinel之前其實就有Hystrix做熔斷降級的事情,我們都知道出現新的事物肯定是原來的東西有不足的地方。

那Hystrix有什麼不足之處呢?

  • Hystrix常用的線程池隔離會造成線程上下切換的overhead比較大。
  • Hystrix沒有監控平臺,需要我們自己搭建。
  • Hystrix支持的熔斷降級維度較少,不夠細粒,而且缺少管理控制檯。

Sentinel有哪些組成部分?

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

Sentinel有哪些特徵?

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

二、Hello World

一般要學一種沒接觸過的技術框架,肯定要先做個Hello World熟悉一下。

引入Maven依賴

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

需要提醒一下,Sentinel僅支持JDK 1.8或者以上的版本

定義規則

通過定義規則來控制該資源每秒允許通過的請求次數,例如下面的代碼定義了資源 HelloWorld 每秒最多隻能通過 20 個請求。

private static void initFlowRules(){
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    // Set limit QPS to 20.
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

編寫Hello World代碼

其實代碼編寫很簡單,首先需要定義一個資源entry,然後用SphU.entry("HelloWorld")entry.exit()把需要流量控制的代碼包圍起來。代碼如下:

public static void main(String[] args) throws Exception {
    initFlowRules();
    while (true) {
        Entry entry = null;
        try {
            entry = SphU.entry("HelloWorld");
            /*您的業務邏輯 - 開始*/
            System.out.println("hello world");
            /*您的業務邏輯 - 結束*/
        } catch (BlockException e1) {
            /*流控邏輯處理 - 開始*/
            System.out.println("block!");
            /*流控邏輯處理 - 結束*/
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }
}

運行結果如下:
在這裡插入圖片描述
我們根據目錄查看日誌,文件名格式為${appName}-metrics.log.xxx:

|--timestamp-|------date time----|-resource-|p |block|s |e|rt
1616607101000|2021-03-25 01:31:41|HelloWorld|20|11373|20|0|1|0|0|0
1616607102000|2021-03-25 01:31:42|HelloWorld|20|24236|20|0|0|0|0|0

p 代表通過的請求。

block 代表被阻止的請求。

s 代表成功執行完成的請求個數。

e 代表用戶自定義的異常。

rt 代表平均響應時長。

三、使用Sentinel的方式

下面結合實際案例,寫一個Controller接口進行示範練習。

@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserService userService;

    @RequestMapping("/list")
    public List<User> getUserList() {
        return userService.getList();
    }
}

@Service
public class UserServiceImpl implements UserService {
    //模擬查詢數據庫數據,返回結果
    @Override
    public List<User> getList() {
        List<User> userList = new ArrayList<>();
        userList.add(new User("1", "周慧敏", 18));
        userList.add(new User("2", "關之琳", 20));
        userList.add(new User("3", "王祖賢", 21));
        return userList;
    }
}

假設我們要讓這個查詢接口限流,怎麼做呢?

1) 拋出異常的方式

SphU 包含了 try-catch 風格的 API。用這種方式,當資源發生了限流之後會拋出 BlockException。這個時候可以捕捉異常,進行限流之後的邏輯處理。

@RestController
@RequestMapping("/user")
public class UserController {
    //資源名稱
    public static final String RESOURCE_NAME = "userList";

    @Resource
    private UserService userService;

    @RequestMapping("/list")
    public List<User> getUserList() {
        List<User> userList = null;
        Entry entry = null;
        try {
            // 被保護的業務邏輯
            entry = SphU.entry(RESOURCE_NAME);
            userList = userService.getList();
        } catch (BlockException e) {
            // 資源訪問阻止,被限流或被降級
            return Collections.singletonList(new User("xxx", "資源訪問被限流", 0));
        } catch (Exception e) {
            // 若需要配置降級規則,需要通過這種方式記錄業務異常
            Tracer.traceEntry(e, entry);
        } finally {
            // 務必保證 exit,務必保證每個 entry 與 exit 配對
            if (entry != null) {
                entry.exit();
            }
        }
        return userList;
    }

}

實際上還沒寫完,還要定義限流的規則。

@SpringBootApplication
public class SpringmvcApplication {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(SpringmvcApplication.class, args);
        //初始化限流規則
        initFlowQpsRule();
    }
    //定義了每秒最多接收2個請求
    private static void initFlowQpsRule() {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule(UserController.RESOURCE_NAME);
        // set limit qps to 2
        rule.setCount(2);
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule.setLimitApp("default");
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}

然後啟動項目,測試。快速刷新幾次,我們就看到觸發限流的邏輯了。
在這裡插入圖片描述

2) 返回布爾值的方式

拋出異常的方式是當被限流時以拋出異常的形式感知,我們通過捕獲異常進行限流的處理,這種方式跟上面不同的在於不拋出異常,而是返回一個布爾值,我們通過判斷布爾值來進行限流邏輯的處理。這樣我們就可以很容易寫出if-else結構的代碼。

public static final String RESOURCE_NAME_QUERY_USER_BY_ID = "queryUserById";

@RequestMapping("/get/{id}")
public String queryUserById(@PathVariable("id") String id) {
    if (SphO.entry(RESOURCE_NAME_QUERY_USER_BY_ID)) {
        try {
            //被保護的邏輯
            //模擬數據庫查詢數據
            return JSONObject.toJSONString(new User(id, "Tom", 25));
        } finally {
            //關閉資源
            SphO.exit();
        }
    } else {
        //資源訪問阻止,被限流或被降級
        return "Resource is Block!!!";
    }
}

添加規則的代碼跟前面的例子一樣,我就不寫了,然後啟動項目,測試。
在這裡插入圖片描述

3) 註解的方式

看了上面兩種方式,肯定有人會說,代碼侵入性太強了,如果原來舊的系統要接入的話,要改原來的代碼。眾所周知,舊代碼是不能動的,否則後果很嚴重。

那麼註解的方式就很好地解決了這個問題。註解式怎麼寫呢?

@Service
public class UserServiceImpl implements UserService {
    //資源名稱
    public static final String RESOURCE_NAME_QUERY_USER_BY_NAME = "queryUserByUserName";

    //value是資源名稱,是必填項。blockHandler填限流處理的方法名稱
    @Override
    @SentinelResource(value = RESOURCE_NAME_QUERY_USER_BY_NAME, blockHandler = "queryUserByUserNameBlock")
    public User queryByUserName(String userName) {
        return new User("0", userName, 18);
    }

    //注意細節,一定要跟原函數的返回值和形參一致,並且形參最後要加個BlockException參數
    //否則會報錯,FlowException: null
    public User queryUserByUserNameBlock(String userName, BlockException ex) {
        //打印異常
        ex.printStackTrace();
        return new User("xxx", "用戶名稱:{" + userName + "},資源訪問被限流", 0);
    }
}

寫完這個核心代碼後,還要加個配置,否則不生效。

引入sentinel-annotation-aspectj的Maven依賴。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>1.8.1</version>
</dependency>

然後將SentinelResourceAspect註冊為一個Bean。

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

別忘了添加規則,可以參考第一個例子,這裡就不寫了。

最後啟動項目,測試,刷新多幾次接口後,出發限流,可以看到以下結果。
在這裡插入圖片描述

4) 熔斷降級

除了可以對接口進行限流之外,當接口出現異常時,Sentinel也可以提供熔斷降級的功能。

@SentinelResource註解中有一個屬性fallback,當拋出非BlockException的異常時,就會進入到fallback方法中,實現熔斷機制,這有點類似於Hystrix的FallBack。

我們拿上面的例子做示範,如果userName為空則拋出RuntimeException。然後我們設置fallback屬性的屬性值,也就是fallback的方法,返回系統異常。

@Override
@SentinelResource(value = RESOURCE_NAME_QUERY_USER_BY_NAME, blockHandler = "queryUserByUserNameBlock", fallback = "queryUserByUserNameFallBack")
public User queryByUserName(String userName) {
    if (userName == null || "".equals(userName)) {
        //拋出異常
        throw new RuntimeException("queryByUserName() command failed, userName is null");
    }
    return new User("0", userName, 18);
}

public User queryUserByUserNameFallBack(String userName, Throwable ex) {
    //打印日誌
    ex.printStackTrace();
    return new User("-1", "用戶名稱:{" + userName + "},系統異常,請稍後重試", 0);
}

然後啟動項目,故意不傳userName,進行測試,可以看到走了fallback的方法邏輯。
在這裡插入圖片描述
IDEA控制檯也可以看到自定義的異常信息。

四、管理控制檯

上面講完了Sentinel的基本用法,實際上重頭戲在Sentinel的管理控制檯,管理控制檯提供了很多實用的功能。下面我們看看怎麼使用。

首先下載控制檯的jar包,當然你也可以通過下載源碼編譯得到。

//下載頁面地址
https://github.com/alibaba/Sentinel/releases

然後使用以下命令啟動:

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

啟動成功後,訪問http://localhost:8080,默認登錄的用戶名和密碼都是sentinel
在這裡插入圖片描述
登錄進去之後,可以看到主頁面,有許多功能菜單,這裡就不一一介紹了。
在這裡插入圖片描述

客戶端接入控制檯

那麼我們自己的應用怎麼接入到控制檯,使用控制檯對應用的流量進行監控呢,諸位客官,請繼續往下看。

首先添加maven依賴,客戶端需要引入 Transport 模塊來與 Sentinel 控制檯進行通信。

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

配置filter,把所有訪問的 Web URL 自動統計為 Sentinel 的資源。

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

        return registration;
    }
}

在啟動命令中加入以下配置,-Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制檯地址和端口,-Dcsp.sentinel.api.port=xxxx 指定客戶端監控 API 的端口(默認是8019,因為控制檯已經使用了8719,應用端為了防止衝突就使用8720):

-Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.api.port=8720 -Dproject.name=sentinelDemo

在這裡插入圖片描述
啟動項目,我們可以看到多了一個應用名稱sentinelDemo,點擊機器列表,查看健康狀況。
在這裡插入圖片描述
請求/user/list接口,然後我們可以看到實時監控的接口的QPS情況。
在這裡插入圖片描述
這樣就代表客戶端接入控制檯成功了!

動態規則

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

  • 通過 API 直接修改 (loadRules)
  • 通過 DataSource 適配不同數據源修改

手動通過API定義規則,前面Hello World的例子已經寫過,是一種硬編碼的形式,因為不夠靈活,所以肯定不能應用於生產環境。

所以要引入DataSource,規則設置可以存儲在數據源中,通過更新數據源中存儲的規則,推送到Sentinel規則中心,客戶端就可以實時獲取最新的規則,根據最新的規則進行限流、降級。

一般DataSource拓展常見的實現方式有:

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

拉模式

pull模式的數據源一般是可寫入的(比如本地文件)。首先要在客戶端註冊數據源,將對應的讀數據源註冊至對應的 RuleManager;然後將寫數據源註冊至 transport 的 WritableDataSourceRegistry 中。

由此看出這是一個雙向讀寫的過程,我們既可以在應用本地直接修改文件來更新規則,也可以通過 Sentinel 控制檯推送規則。下圖為控制檯推送規則的流程圖。
在這裡插入圖片描述
首先引入maven依賴。

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

使用SPI機制進行擴展,創建一個實現類,實現InitFunc接口的init()方法。

public class FileDataSourceInit implements InitFunc {

    public FileDataSourceInit() {
    }

    @Override
    public void init() throws Exception {
        String filePath = System.getProperty("user.home") + "\\sentinel\\rules\\sentinel.json";
        ReadableDataSource<String, List<FlowRule>> ds = new FileRefreshableDataSource<>(
            filePath, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
            })
        );
        // 將可讀數據源註冊至 FlowRuleManager.
        FlowRuleManager.register2Property(ds.getProperty());

        WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(filePath, this::encodeJson);
        // 將可寫數據源註冊至 transport 模塊的 WritableDataSourceRegistry 中.
        // 這樣收到控制檯推送的規則時,Sentinel 會先更新到內存,然後將規則寫入到文件中.
        WritableDataSourceRegistry.registerFlowDataSource(wds);
    }

    private <T> String encodeJson(T t) {
        return JSON.toJSONString(t);
    }
}

在項目的 resources/META-INF/services 目錄下創建文件,名為com.alibaba.csp.sentinel.init.InitFunc ,內容則是FileDataSourceInit的全限定名稱:

io.github.yehongzhi.springmvc.config.FileDataSourceInit

在這裡插入圖片描述
接著在${home}目錄下,創建\sentinel\rules目錄,再創建sentinel.json文件。
在這裡插入圖片描述
然後啟動項目,發送請求,當客戶端接收到請求後就會觸發初始化操作。初始化完成後我們到控制檯,然後設置流量限流規則。
在這裡插入圖片描述
新增後,本地文件sentinel.json同時也保存了規則內容(壓縮成一行的json)。

[{"clusterConfig":{"acquireRefuseStrategy":0,"clientOfflineTime":2000,"fallbackToLocalWhenFail":true,"resourceTimeout":2000,"resourceTimeoutStrategy":0,"sampleCount":10,"strategy":0,"thresholdType":0,"windowIntervalMs":1000},"clusterMode":false,"controlBehavior":0,"count":3.0,"grade":1,"limitApp":"default","maxQueueingTimeMs":500,"resource":"userList","strategy":0,"warmUpPeriodSec":10}]

我們可以通過修改文件來更新規則內容,也可以通過控制檯推送規則到文件中,這就是拉模式。缺點是不保證一致性,實時性不保證,拉取過於頻繁也可能會有性能問題。

推模式

剛剛說了拉模式實時性不能保證,推模式就解決了這個問題。除此之外還可以持久化,也就是數據保存在數據源中,即使重啟也不會丟失之前的配置,這也解決了原始模式存在內存中不能持久化的問題。

可以和Sentinel配合使用的數據源有很多種,比如ZooKeeper,Nacos,Apollo等等。這裡介紹使用Nacos的方式。

首先要啟動Nacos服務器,然後登錄到Nacos控制檯,添加一個命名空間,添加配置。
在這裡插入圖片描述
接著我們就要改造Sentinel的源碼。因為官網提供的Sentinel的jar是原始模式的,所以需要改造,所以我們需要拉取源碼下來改造一下,然後自己編譯jar包。

源碼地址:https://github.com/alibaba/Sentinel

拉取下來之後,導入到IDEA中,然後我們可以看到以下目錄結構。
在這裡插入圖片描述
首先修改sentinel-dashboard的pom.xml文件:
在這裡插入圖片描述
第二步,把test目錄下的四個關於Nacos關聯的類,移到rule目錄下。
在這裡插入圖片描述
FlowRuleNacosProvider和FlowRuleNacosPublisher不需要怎麼改造,本人不太喜歡名稱後綴,所以去掉了後面的後綴。
在這裡插入圖片描述
接著NacosConfig添加Nacos的地址配置。
在這裡插入圖片描述
最關鍵的是FlowControllerV1的改造,這是規則配置的增刪改查的一些接口。

把移動到rule目錄下的兩個服務,添加到FlowControllerV1類中。

@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

添加私有方法publishRules(),用於推送配置:

private void publishRules(/*@NonNull*/ String app) throws Exception {
    List<FlowRuleEntity> rules = repository.findAllByApp(app);
    rulePublisher.publish(app, rules);
}

修改apiQueryMachineRules()方法。
在這裡插入圖片描述
修改apiAddFlowRule()方法。

修改apiUpdateFlowRule()方法。

修改apiDeleteFlowRule()方法。
在這裡插入圖片描述
Sentinel控制檯的項目就改造完成了,用於生產環境就編譯成jar包運行,如果是學習可以直接在IDEA運行。

我們在前面創建的HelloWord工程的pom.xml文件加上依賴。

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

然後在application.yml文件加上以下配置:

spring:
  cloud:
    sentinel:
      datasource:
        flow:
          nacos:
            server-addr: localhost:8848
            namespace: 05f447bc-8a0b-4686-9c34-344d7206ea94
            dataId: springmvc-sentinel-flow-rules
            groupId: SENTINEL_GROUP
            # 規則類型,取值見:
            # org.springframework.cloud.alibaba.sentinel.datasource.RuleType
            rule-type: flow
            data-type: json
  application:
    name: springmvc-sentinel-flow-rules

以上就完成了全部的配置和改造,啟動Sentinel控制檯,還有Java應用。

打開Nacos控制檯,我們添加限流配置如下:
在這裡插入圖片描述

配置內容如下:

[{"app":"springmvc-sentinel-flow-rules","clusterConfig":{"acquireRefuseStrategy":0,"clientOfflineTime":2000,"fallbackToLocalWhenFail":true,"resourceTimeout":2000,"resourceTimeoutStrategy":0,"sampleCount":10,"strategy":0,"thresholdType":0,"windowIntervalMs":1000},"clusterMode":false,"controlBehavior":0,"count":1.0,"grade":1,"limitApp":"default","maxQueueingTimeMs":500,"resource":"userList","strategy":0,"warmUpPeriodSec":10},{"app":"springmvc-sentinel-flow-rules","clusterConfig":{"acquireRefuseStrategy":0,"clientOfflineTime":2000,"fallbackToLocalWhenFail":true,"resourceTimeout":2000,"resourceTimeoutStrategy":0,"sampleCount":10,"strategy":0,"thresholdType":0,"windowIntervalMs":1000},"clusterMode":false,"controlBehavior":0,"count":3.0,"grade":1,"limitApp":"default","maxQueueingTimeMs":500,"resource":"queryUserByUserName","strategy":0,"warmUpPeriodSec":10}]

然後我們打開Sentinel控制檯,能看到配置,證明Nacos的配置推送成功了。
在這裡插入圖片描述

我們嘗試調用Java應用的接口,測試是否生效。
在這裡插入圖片描述
可以看到限流是生效的,再看看Sentinel監控的QPS情況。
在這裡插入圖片描述

從QPS監控的情況看,最高的QPS只有3,其他請求都被拒絕了,證明限流配置是實時生效的。
在這裡插入圖片描述
配置信息也被持久化到Nacos相關的配置表中。

這時候,再回頭看Sentinel官網上關於推模式的架構圖就比較清楚了。
在這裡插入圖片描述

總結

本篇文章主要介紹了Sentinel的基本用法,還有動態規則的兩種方式,除此之外當然還有許多功能,這裡由於篇幅問題就不一一介紹了,有興趣的朋友可以自己探索一下。我個人覺得Sentinel是一個非常優秀的組件,比原來用的Hystrix的確有著非常大的改進,值得推薦。

我們看到官網上登記的企業列表,也有很多知名企業在使用,相信以後Sentinel會越來越好。
在這裡插入圖片描述
這篇文章就講到這裡了,感謝大家的閱讀,希望看完大家能有所收穫!

覺得有用就點個贊吧,你的點贊是我創作的最大動力~

我是一個努力讓大家記住的程序員。我們下期再見!!!

能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!

Leave a Reply

Your email address will not be published. Required fields are marked *