文章已收錄Github精選,歡迎Star:https://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包。
拉取下來之後,導入到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會越來越好。
這篇文章就講到這裡了,感謝大家的閱讀,希望看完大家能有所收穫!
覺得有用就點個贊吧,你的點贊是我創作的最大動力~
我是一個努力讓大家記住的程序員。我們下期再見!!!
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!