一、Sentinel介紹
Sentinel 是面向分布式服務(wù)架構(gòu)的流量控制組件,主要以流量為切入點,從限流、流量整形、熔斷降級、系統(tǒng)負載保護、熱點防護等多個維度來幫助開發(fā)者保障微服務(wù)的穩(wěn)定性。
-
流量控制
任意時間到來的請求往往是隨機不可控的,而系統(tǒng)的處理能力是有限的。我們需要根據(jù)系統(tǒng)的處理能力對流量進行控制。Sentinel 作為一個調(diào)配器,可以根據(jù)需要把隨機的請求調(diào)整成合適的形狀。
-
熔斷降級
及時對調(diào)用鏈路中的不穩(wěn)定因素進行熔斷也是 Sentinel 的使命之一,Sentinel 和 Hystrix 的原則是一致的,當檢測到調(diào)用鏈路中某個資源出現(xiàn)不穩(wěn)定的表現(xiàn),例如請求響應(yīng)時間長或異常比例升高的時候,則對這個資源的調(diào)用進行限制,讓請求快速失敗,避免影響到其它的資源而導(dǎo)致級聯(lián)故障。
-
系統(tǒng)保護
Sentinel 為服務(wù)集群提供了對應(yīng)的保護機制,讓系統(tǒng)的入口流量和系統(tǒng)的負載達到一個平衡,保證系統(tǒng)在能力范圍之內(nèi)處理最多的請求。
-
實時監(jiān)控
Sentinel 提供實時的監(jiān)控系統(tǒng),方便您快速了解目前系統(tǒng)的狀態(tài)。
二、Dashboard的搭建與啟動
從官網(wǎng)下載最新版本的jar包,然后按照如下命令啟動Dashboard。
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.x.x.jar
啟動成功后,我們就可以在localhost:8080端口訪問Dashboard的網(wǎng)站了,初始賬密為sentinel。
三、應(yīng)用入門案例
3.1 應(yīng)用搭建
首先新建一個項目,引入必要的模塊:web和sentinel
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.1</version>
</dependency>
然后新建一個Controller
@Slf4j
@RestController
public class HelloController {
@GetMapping("/getHello")
public String getHello(){
return "hello";
}
}
最后,我們需要增加如下的配置內(nèi)容,來對sentinel進行配置。
server.port=8081
spring.application.name=sentinel-demo
# 指定sentinel控制臺的地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
# 指定和控制臺通信的端口,默認8719
spring.cloud.sentinel.transport.port=8719
# 指定心跳周期,默認null
spring.cloud.sentinel.transport.heartbeat-interval-ms=10000
到此,我們可以啟動項目了,然后通過訪問localhost:8081/getHello多嘗試幾次,然后過一會就可以在Dahboard上看到我們的調(diào)用監(jiān)控了。
當然,如果需要監(jiān)控的不是接口,而是普通的方法,我們可以使用@SentinelResource注解來達成目的。
@Slf4j
@Component
public class HelloJob {
@Scheduled(cron = "0/10 * * * * ?")
@SentinelResource("helloJob")
public void startHello(){
log.info("hello! hello.");
}
}
@EnableScheduling
@SpringBootApplication
public class SentinelDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelDemoApplication.class, args);
}
}
重新啟動項目,發(fā)現(xiàn)我們的helloJob也可以被Dashboard監(jiān)控管理了。
3.2 拋出異常的方式定義資源
此時我們還沒有用到Sentinel的任何功能,我們來試下對如上的Controller來進行QPS的限流,這里使用的方式是拋出異常的方式。
/**
* 拋出異常的方式定義資源
* @return
*/
@GetMapping("/getHello")
public String getHello() {
// 使用限流規(guī)則,保護”業(yè)務(wù)邏輯“
try(Entry entry = SphU.entry("getHello")) {
// 正常的業(yè)務(wù)邏輯
return "OK";
} catch (Exception e) {
log.info("當前請求被限流了!", e);
// 降級方案
return "系統(tǒng)繁忙,請稍后再試!";
}
}
/**
* 定義隔離規(guī)則
*
* @PostConstruct 當前類的構(gòu)造函數(shù)執(zhí)行之后執(zhí)行該方法
*/
@PostConstruct
public void initFlowRule() {
List<FlowRule> ruleList = new ArrayList<>();
FlowRule rule = new FlowRule();
// 設(shè)置資源名稱
rule.setResource("getHello");
// 指定限流模式為QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 指定QPS限流閾值
rule.setCount(2);
ruleList.add(rule);
// 加載該規(guī)則
FlowRuleManager.loadRules(ruleList);
}
我們在代碼中設(shè)置了/getHello的QPS閾值為2,那么此時啟動項目后,快速刷新訪問該URL,就可能會看到限流后的降級輸出。
3.3 返回布爾值方式定義資源
我們還有其它方式來實現(xiàn)如上相同的功能:
/**
* 返回布爾值方式定義資源
* @return
*/
@GetMapping("/getHi")
public String getHi() {
if (SphO.entry("getHello")) {
try {
// 正常的業(yè)務(wù)邏輯
return "Hi";
} finally {
SphO.exit();
}
} else {
log.info("當前請求被限流了!");
// 降級方案
return "系統(tǒng)繁忙,請稍后再試!";
}
}
3.4 異步調(diào)用方式定義資源
前面的例子都是同步調(diào)用的,我們來個異步調(diào)用的例子,前端請求到了之后,異步去處理具體的業(yè)務(wù)邏輯。
首先我們需要在啟動類上增加@EnableAsync注解。
其次我們創(chuàng)建一個異步業(yè)務(wù)類:
@Slf4j
@Service
public class AsyncService {
@Async
public void asyncInvoke(){
log.info("開始asyncInvoke。。。");
try {
// 沉睡10秒,模擬遠程調(diào)用
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("結(jié)束asyncInvoke。。。");
}
}
然后,我們在上面例子中的controller類中增加一個:
@Autowired
private AsyncService asyncService;
/**
* 異步調(diào)用方式定義資源
*/
@GetMapping("/getAsync")
public String getAsync(){
AsyncEntry asyncEntry = null;
try {
asyncEntry = SphU.asyncEntry("getHello");
// 正常的業(yè)務(wù)邏輯
asyncService.asyncInvoke();
} catch (BlockException e) {
log.info("當前請求被限流了:",e);
// 降級方案
return "系統(tǒng)繁忙,請稍后再試!";
} finally {
if(asyncEntry != null){
asyncEntry.exit();
}
}
return "OK";
}
3.5 使用注解的方式定義資源
/**
* 使用注解額方式定義資源
*/
@GetMapping("/getAnnotation")
@SentinelResource(value = "getHello", blockHandler = "exceptionHandler")
public String getAnnotation(){
// 正常的業(yè)務(wù)邏輯
return "annotation";
}
public String exceptionHandler(BlockException blockException){
// 降級方案
log.info("當前請求被限流了:",blockException);
return "系統(tǒng)繁忙,請稍后再試!";
}
在實際使用Sentinel的過程中,我們很少使用如上硬編碼的方式來設(shè)置規(guī)則,因為如果需要改規(guī)則會很不方便,所以一般推薦使用Sentinel Dashboard來設(shè)置和編輯規(guī)則,這部分在如下例子中講解。
四、流量控制
4.1 并發(fā)線程數(shù)控制
用于保護業(yè)務(wù)線程池不被慢調(diào)用耗盡。和Hystrix相比,Hystrix是使用了線程池隔離的技術(shù),雖然隔離的比較徹底,不會存在兩個不同業(yè)務(wù)調(diào)用相互競爭的情況,但是線程資源會比較浪費,線程的切換也會帶來很大的性能損耗。Sentinel則是不會創(chuàng)建和管理線程池,僅僅是統(tǒng)計當前資源上下文的總線程數(shù),只要超過閾值,就拒絕新的調(diào)用,效果類似于Hystrix中的信號量模式。
下面我們來實驗下并發(fā)線程數(shù)控制的效果:
還是上面入門案例,我們改造一下HelloController,主要是使得請求線程沉睡1秒,使得每一個請求線程在1秒內(nèi)只會處理一個請求。
@GetMapping("/getHello")
public String getHello() throws InterruptedException {
Thread.sleep(1000);
return "hello";
}
然后我們使用Jmeter做并發(fā)壓力測試,設(shè)置線程數(shù)量為30,循環(huán)2次,開始后Seninel控制臺會顯示所有請求全部成功,此時系統(tǒng)能承受的最大并發(fā)數(shù)量上限為tomcat容器允許的最大線程數(shù)。

然后,在Sentinel的控制臺上找到”流控規(guī)則“,新增一個規(guī)則,并在如下選項中依次填寫:

此時,我們重復(fù)如上Jmeter的并發(fā)測試,會發(fā)現(xiàn)1秒內(nèi)的并發(fā)數(shù)量被降低到20以下了,說明Sentinel限流成功。

4.2 QPS控制
是用于限制某個資源的請求并發(fā)數(shù)量,和線程無關(guān),我們使用如上HelloController的例子,去掉線程的沉睡代碼。在不限QPS的時候,Jmeter并發(fā)30個線程,循環(huán)兩次,發(fā)現(xiàn)都可以訪問成功。
然后我們增加流控規(guī)則如下:

此時啟動Jmeter,會發(fā)現(xiàn)QPS被限制為一秒內(nèi)最多20個,超過20個的都會拒絕,從而使請求失敗。

五、熔斷降級
5.1 RT響應(yīng)時間
DEGRADE_GRADE_RT,當一秒內(nèi)對某個資源發(fā)起的多個請求,它們的平均響應(yīng)時間超過了閾值T,那么在接下來的N秒內(nèi),所有對這個資源的請求訪問都會被熔斷進行降級處理。其中,閾值T是我們設(shè)置的毫秒數(shù),N秒是我們設(shè)置的熔斷時間窗口。
5.2 異常比例
DEGRADE_GRADE_EXCEPTION_RATIO,當每秒對某個資源的請求量超過閾值C的時候,異常請求/正常請求的比例超過閾值P時,在接下來的T秒時間窗口內(nèi),所有對這個資源的請求訪問都會被熔斷進行降級處理。其中并發(fā)量C、比例閾值P、時間窗口T都是我們可以設(shè)置的。
5.3 異常數(shù)
DEGRADE_GRADE_EXCEPTION_COUNT,最近一分鐘內(nèi)對某個資源的請求異常數(shù)量達到我們設(shè)置的閾值C的時候,在該分鐘的剩余時間里,所有對這個資源的請求訪問都會被熔斷進行降級處理。在下一分鐘才能恢復(fù)訪問。
這部分熔斷降級的案例既可以通過如上案例中硬編碼在代碼里,也可以在Sentinel Dashboard的降級規(guī)則里面設(shè)置,比較簡單,不再演示。
六、整合Feign
首先需要引入Fein的依賴:
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.0.3</version>
</dependency>
然后,我們創(chuàng)建一個Feign調(diào)用的service,并指定降級處理方法。
@FeignClient(name="feign-helo", url = "http://localhost:8081", fallback = FeignFallBackService.class)
public interface HelloFeignService {
@RequestMapping(value = "/getHello0", method = RequestMethod.GET)
String getHello();
}
@Component
public class FeignFallBackService implements HelloFeignService {
@Override
public String getHello() {
// 降級方案
return "Feign提示系統(tǒng)繁忙,請稍后再試!";
}
}
然后,我們在如上Controller里面新增一個:
@Autowired
private HelloFeignService helloFeignService;
/**
* 整合Feign
*/
@GetMapping("/getFeignHello")
public String getFeignHello(){
return helloFeignService.getHello();
}
現(xiàn)在我們需要在啟動類上加上@EnableFeignClients注解,使得Feign生效。
最后一步,增加如下的配置內(nèi)容:
# feign開啟sentinel支持,否則降級方法得不到執(zhí)行,直接拋出異常
feign.sentinel.enabled=true
此時我們使用JMeter做測試,調(diào)用/getFeignHello接口,就會發(fā)現(xiàn)通過Feign調(diào)用的/getHello是有限流的,當/getHello服務(wù)不可用時,會執(zhí)行Feign的降級方法。
相關(guān)內(nèi)容和Hystrix整合OpenFeign比較類似,可以參考:
Hystrix使用入門
七、整合Gateway
關(guān)于Gateway的配置可以參考:Gateway使用入門,本文直接在這基礎(chǔ)上進行改造。
首先,我們新建一個配置類,用以當網(wǎng)關(guān)轉(zhuǎn)發(fā)請求失敗的時候降級處理的邏輯。
@Component
public class GatewayConfiguration {
/**
* 設(shè)置限流或者降級之后的處理方法,對所有路徑都生效
*/
@PostConstruct
public void doInit(){
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
return ServerResponse.status(200).syncBody("gateway提示:系統(tǒng)繁忙,請稍后再試!");
}
});
}
}
然后在配置文件中先增加和Sentinel Dashboard的鏈接配置:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8720
然后增加一個路由規(guī)則,用以將請求網(wǎng)關(guān)的請求轉(zhuǎn)發(fā)到上面例子中的getHello中。
spring:
cloud:
gateway:
routes:
- id: gateway-hello
uri: http://localhost:8081
predicates:
- Path=/getHello
此時,我們同時啟動網(wǎng)關(guān)服務(wù)和上面的getHello服務(wù)。先測試下gateway能否正常工作,請求localhost:8089/getHello,如果正常返回getHello的返回內(nèi)容,則說明gateway項目沒有問題。
此時,我們的降級方案配置GatewayConfiguration 還不能起作用,因為還沒有限流的設(shè)置,所以我們可以在SentinelDashboard上增加相應(yīng)的限流規(guī)則。
- Route ID,根據(jù)配置文件中配置的id進行限流;
- API分組,根據(jù)請求API路徑進行匹配限流;
一旦觸發(fā)限流,那么就會使用GatewayConfiguration 的降級處理方案來響應(yīng)請求。
需要注意的是,在本案例中,gateway和hello項目的限流規(guī)則將會同時起作用,如果要準確測試gateway的限流效果,建議可以先關(guān)閉hello項目的限流規(guī)則。
- 場景1:gateway限流6,hello限流2,我們并發(fā)20個請求,那么gateway將會攔截14個,進入gateway降級方案處理,只通過6個請求轉(zhuǎn)發(fā)給hello,然后hello再攔截4個,進入hello降級方案處理,最終只通過2個。
- 場景2:gateway限流6,hello不限流,我們并發(fā)20個請求,那么gateway將會攔截14個,進入gateway降級方案處理,只通過6個請求轉(zhuǎn)發(fā)給hello,全部處理成功。
- 場景3:gateway限流6,hello停機,我們并發(fā)20個請求,那么gateway攔截的會走降級處理方案,通過的請求因為找不到hello服務(wù),所以拋出異常了。
八、系統(tǒng)自適應(yīng)保護
Sentinel還支持根據(jù)應(yīng)用機器的負載情況來自適應(yīng)地對我們的資源進行限流和降級。使得系統(tǒng)的入口流量和系統(tǒng)的負載達到一個平衡,不會因為流量過大拖垮機器。
Sentinel自適應(yīng)保護的主要模式有如下幾種:
- Load自適應(yīng),僅對Linux/Unix-like的機器生效,即將系統(tǒng)的Load1作為啟發(fā)指標,當系統(tǒng)超過設(shè)定的啟發(fā)值后,Sentinel估算當前機器的并發(fā)線程數(shù)超過了預(yù)估的最大容量后,對資源進行熔斷降級。
- CPU Usage,根據(jù)當前機器的CPU使用率是否超過了設(shè)定的閾值來觸發(fā)熔斷降級。
- 平均RT,當前機器的所有入口流量平均RT超過設(shè)定的閾值時觸發(fā)熔斷降級。
- 并發(fā)線程數(shù),當前機器的所有入口流量造成的并發(fā)線程數(shù)超過設(shè)定的閾值;
- 入口QPS,當前機器的所有入口流量造成的QPS數(shù)超過設(shè)定的閾值;
需要注意的是,以上設(shè)定的自適應(yīng)保護規(guī)則,會對所有入口流量的資源生效。
/**
* 系統(tǒng)自適應(yīng)保護規(guī)則測試
*/
@GetMapping("/getAdapt1")
// 標識為入口流量資源
@SentinelResource(entryType = EntryType.IN)
public String getAdapt1(){
return "adapt1";
}
@GetMapping("/getAdapt2")
// 未標識為入口資源,但也屬于入口流量
public String getAdapt2(){
return "adapt2";
}
以上兩個都屬于系統(tǒng)的入口流量資源,當超過設(shè)定的閾值后,都會被Sentinel自動降級處理,比如返回結(jié)果如下:
Blocked by Sentinel (flow limiting)
九、授權(quán)控制
主要作用就是對資源的訪問進行授權(quán)控制,通過黑白名單的形式來限制某個請求是否有權(quán)限訪問我們的某個資源。
- 白名單,在白名單中的請求來源(IP)允許訪問資源;
- 黑名單,在黑名單中的請求來源(IP)不允許訪問資源;
原理就是通過HttpServletRequest.getRemoteAddr()來獲取請求方的IP地址,從而判斷其是否在黑、白名單中來達到限制的目的。
十、動態(tài)規(guī)則擴展
在上面的例子中,講到規(guī)則的配置只有兩種方式,要么在代碼里面寫好了,要么在Sentinel Dashboard上進行配置。在實際的場景中,我們很少采用第一種方式,因為如果涉及到規(guī)則的變更那就需要改動代碼了,十分不便,那么對于第二種方式,配置的規(guī)則是保存在內(nèi)存中的,一旦服務(wù)重啟規(guī)則就沒有了,所以不能持久化。
動態(tài)規(guī)則要想實現(xiàn)擴展,一般都是結(jié)合配置中心來實現(xiàn)持久化的,同時也方便動態(tài)地更改規(guī)則。Sentinel支持Consul、Zookeeper、Redis、Nacos、Apollo、etcd等數(shù)據(jù)源的持久化支持,本文則演示使用Nacos的例子。
開始之前,可以先參考Nacos使用入門了解下Nacos的基本入門使用,我們需要先將Nacos服務(wù)端啟動起來。
然后,我們的sentinel項目中需要引入nacos的datasource支持:
<!-- https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.2</version>
<scope>test</scope>
</dependency>
然后在nacos服務(wù)端,新建并寫上我們的限流規(guī)則:
[
{
"resource": "/getNacos",
"limitApp": "default",
"grade": 1,
"count": 2,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
對應(yīng)dataId為自定義的,比如sentinel-nacos;group就是用DEFAULT_GROUP。
然后,我們就需要將如上Nacos中的限流規(guī)則加載到應(yīng)用中,如下兩種方式是等同的,可以視情況選擇一種。
- init
我們在上面getHello的例子中使用過在代碼中配置限流規(guī)則的案例,現(xiàn)在把其改為:
/**
* 使用Nacos數(shù)據(jù)源來管理規(guī)則
*/
@PostConstruct
public void initDataSource() {
String remoteAddress = "192.168.31.17:8848";
String groupId = "DEFAULT_GROUP";
String dataId = "sentinel-nacos";
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId
, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
如上代碼即是在初始化的時候就將Nacos上的限流規(guī)則加載進來了,此時我們重啟項目,對/getNacos資源的限流就生效了,此時查看Sentinel Dashboard的限流規(guī)則就能看到這條規(guī)則,并且重啟后不會丟失。
注意:@PostConstruct只能有一個,如果有多個,那么按照順序只有第一個生效,所以此處需要替換/getHello中的例子,而不是增加。
- 配置
新建一個類:
public class DataSourceInitFunc implements InitFunc {
@Override
public void init() throws Exception {
String remoteAddress = "192.168.31.17:8848";
String groupId = "DEFAULT_GROUP";
String dataId = "sentinel-nacos";
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
}
然后在resources目錄下,新建目錄META-INF/services,然后再創(chuàng)建一個文件,名稱為com.alibaba.csp.sentinel.init.InitFunc,里面寫上剛才新建類的名稱,比如:
com.example.sentineldemo.config.DataSourceInitFunc
此時就OK了。
注意,當啟用Nacos作為配置規(guī)則的數(shù)據(jù)源之后,代碼里面init時配置的規(guī)則就不再生效了。
十一、參考資料:
官方文檔講解的很詳細很到位,各種問題和案例基本都能找到,推薦閱讀官方文檔。