前言
隨著微服務(wù)的流行,服務(wù)和服務(wù)之間的穩(wěn)定性變得越來越重要。Sentinel 以流量為切入點(diǎn),從流量控制、熔斷降級、系統(tǒng)負(fù)載保護(hù)等多個維度保護(hù)服務(wù)的穩(wěn)定性。
Sentinel 具有以下特征:
豐富的應(yīng)用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發(fā)流量控制在系統(tǒng)容量可以承受的范圍)、消息削峰填谷、集群流量控制、實(shí)時熔斷下游不可用應(yīng)用等。
完備的實(shí)時監(jiān)控:Sentinel 同時提供實(shí)時的監(jiān)控功能。您可以在控制臺中看到接入應(yīng)用的單臺機(jī)器秒級數(shù)據(jù),甚至 500 臺以下規(guī)模的集群的匯總運(yùn)行情況。
廣泛的開源生態(tài):Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應(yīng)的依賴并進(jìn)行簡單的配置即可快速地接入 Sentinel。
完善的 SPI 擴(kuò)展點(diǎn):Sentinel 提供簡單易用、完善的 SPI 擴(kuò)展接口。您可以通過實(shí)現(xiàn)擴(kuò)展接口來快速地定制邏輯。例如定制規(guī)則管理、適配動態(tài)數(shù)據(jù)源等。
以上內(nèi)容引自 Sentinel 官方介紹。在本文中,筆者將從實(shí)際應(yīng)用的角度,來學(xué)習(xí)Sentinel的使用。
一、初識Sentinel
首先,我們需要引入Sentinel的依賴。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.7.2</version>
</dependency>
Sentinel 支持以下幾種規(guī)則:流量控制規(guī)則、熔斷降級規(guī)則、系統(tǒng)保護(hù)規(guī)則、來源訪問控制規(guī)則 和 熱點(diǎn)參數(shù)規(guī)則。
在這里,我們來展示一個流量控制和熔斷降級的示例。
1、流量控制
流量控制,其原理是監(jiān)控應(yīng)用流量的 QPS 或并發(fā)線程數(shù)等指標(biāo),當(dāng)達(dá)到指定的閾值時對流量進(jìn)行控制,以避免被瞬時的流量高峰沖垮,從而保障應(yīng)用的高可用性。
我們以 QPS 為例,先來定義它的規(guī)則,相關(guān)屬性含義見注釋。
/**
* 加載限流規(guī)則
* @param resource
*/
public static void loadFlowRules(String resource){
FlowRule rule = new FlowRule();
//資源名稱,可以是任意字符串
rule.setResource(resource);
//限流閾值
rule.setCount(5);
//限流閾值類型,設(shè)置為QPS。即每秒QPS大于5時,觸發(fā)限流
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//針對的調(diào)用來源
rule.setLimitApp("default");
//調(diào)用關(guān)系限流策略,默認(rèn)按照資源本身
rule.setStrategy(RuleConstant.STRATEGY_DIRECT);
//限流效果,默認(rèn)直接拒絕
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
//是否集群限流
rule.setClusterMode(false);
FlowRuleManager.loadRules(Collections.singletonList(rule));
}
如上代碼,當(dāng)每秒的請求數(shù)達(dá)到 5 之后,就會直接拒絕當(dāng)前時間窗口的后續(xù)請求。
接下來,我們把需要控制流量的代碼用 Sentinel API SphU.entry("resource") 和 entry.exit() 包圍起來即可。
public static void main(String[] args) throws InterruptedException {
loadFlowRules("orderService");
while (!stop){
count.incrementAndGet();
Entry entry = null;
try {
entry = SphU.entry(resource);
logger.info("業(yè)務(wù)操作...{}",count.get());
} catch (BlockException e) {
logger.error("請求被限流...{}",count.get());
Thread.sleep(1000);
} finally {
if (entry != null) {
entry.exit();
}
if (count.get()>=20){
stop = true;
}
}
}
}
如上代碼,我們先通過loadFlowRules()方法加載限流規(guī)則。然后將業(yè)務(wù)操作用Sentinel API包圍起來。
我們定義的限流閾值是5,這里一共有20個請求。觸發(fā)限流之后,我們的線程停頓1秒,以便度過當(dāng)前的時間窗口,所以會有3個請求被限流。
運(yùn)行代碼,我們可以得到以下結(jié)果:
14:38:00.463 - 業(yè)務(wù)操作...1
14:38:00.465 - 業(yè)務(wù)操作...2
14:38:00.465 - 業(yè)務(wù)操作...3
14:38:00.465 - 業(yè)務(wù)操作...4
14:38:00.465 - 業(yè)務(wù)操作...5
14:38:00.494 - 請求被限流...6
14:38:01.494 - 業(yè)務(wù)操作...7
14:38:01.494 - 業(yè)務(wù)操作...8
14:38:01.495 - 業(yè)務(wù)操作...9
14:38:01.495 - 業(yè)務(wù)操作...10
14:38:01.495 - 業(yè)務(wù)操作...11
14:38:01.496 - 請求被限流...12
14:38:02.497 - 業(yè)務(wù)操作...13
14:38:02.497 - 業(yè)務(wù)操作...14
14:38:02.497 - 業(yè)務(wù)操作...15
14:38:02.497 - 業(yè)務(wù)操作...16
14:38:02.497 - 業(yè)務(wù)操作...17
14:38:02.497 - 請求被限流...18
14:38:03.498 - 業(yè)務(wù)操作...19
14:38:03.498 - 業(yè)務(wù)操作...20
2、熔斷
除了流量控制以外,對調(diào)用鏈路中不穩(wěn)定的資源進(jìn)行熔斷降級也是保障高可用的重要措施之一。
Sentinel 熔斷降級會在調(diào)用鏈路中某個資源出現(xiàn)不穩(wěn)定狀態(tài)時(例如調(diào)用超時或異常比例升高),對這個資源的調(diào)用進(jìn)行限制,讓請求快速失敗,避免影響到其它的資源而導(dǎo)致級聯(lián)錯誤。
那怎么來衡量資源是否穩(wěn)定呢?
Sentinel提供了三種方式,平均響應(yīng)時間、異常比例和異常數(shù)。
我們拿平均響應(yīng)時間為例,先來定義它的規(guī)則。
/**
* 1秒內(nèi)的5個請求,平均響應(yīng)時間大于10ms,接下來的3秒內(nèi)都會自動熔斷。
* @param resourceName
*/
public static void loadDegradeRule(String resourceName){
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
//資源名稱
rule.setResource(resourceName);
//閾值 - 10ms
rule.setCount(10);
//熔斷策略 - RT模式
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
//時間窗口 - 3s
rule.setTimeWindow(3);
//RT模式下,1秒內(nèi)連續(xù)多少個請求的平均RT超出閾值,才可以觸發(fā)熔斷
rule.setRtSlowRequestAmount(5);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
如上代碼,我們定義了熔斷的規(guī)則,屬性的含義見注釋內(nèi)容,然后來看測試用例。
public static void main(String[] args)throws InterruptedException {
loadDegradeRule(resource);
while (!stop){
count.incrementAndGet();
Entry entry = null;
try {
entry = SphU.entry(resource);
logger.info("業(yè)務(wù)操作...{}",count.get());
Thread.sleep(15);
} catch (BlockException e) {
if (e instanceof DegradeException){
logger.error("觸發(fā)熔斷機(jī)制...{}",count.get());
Thread.sleep(500);
}
} finally {
if (entry != null) {
entry.exit();
}
if (count.get()>=20){
stop = true;
}
}
}
logger.info("----------------------------");
}
在上面的代碼中,我們一共有20個請求。我們讓線程停頓15ms使平均RT超過閾值,也就是超過10ms。
我們定義的規(guī)則里面是1秒內(nèi)連續(xù)5個請求的平均RT超出閾值,就可以觸發(fā)熔斷,所以當(dāng)?shù)?個請求到達(dá)時,就會觸發(fā)熔斷。
熔斷多久呢?就在3秒的時間窗口。
上面的測試代碼中,在觸發(fā)熔斷之后,我們又手動讓線程停頓了 1000ms ,所以每次熔斷的請求會有3個。
是不是這樣,我們運(yùn)行代碼,看下結(jié)果:
10:56:20.022 [main] INFO orderService - 業(yè)務(wù)操作...1
10:56:20.040 [main] INFO orderService - 業(yè)務(wù)操作...2
10:56:20.056 [main] INFO orderService - 業(yè)務(wù)操作...3
10:56:20.072 [main] INFO orderService - 業(yè)務(wù)操作...4
10:56:20.088 [main] INFO orderService - 業(yè)務(wù)操作...5
10:56:20.127 [main] ERROR orderService - 觸發(fā)熔斷機(jī)制...6
10:56:21.128 [main] ERROR orderService - 觸發(fā)熔斷機(jī)制...7
10:56:22.128 [main] ERROR orderService - 觸發(fā)熔斷機(jī)制...8
10:56:23.129 [main] INFO orderService - 業(yè)務(wù)操作...9
10:56:23.145 [main] INFO orderService - 業(yè)務(wù)操作...10
10:56:23.160 [main] INFO orderService - 業(yè)務(wù)操作...11
10:56:23.176 [main] INFO orderService - 業(yè)務(wù)操作...12
10:56:23.192 [main] INFO orderService - 業(yè)務(wù)操作...13
10:56:23.207 [main] ERROR orderService - 觸發(fā)熔斷機(jī)制...14
10:56:24.208 [main] ERROR orderService - 觸發(fā)熔斷機(jī)制...15
10:56:25.208 [main] ERROR orderService - 觸發(fā)熔斷機(jī)制...16
10:56:26.209 [main] INFO orderService - 業(yè)務(wù)操作...17
10:56:26.224 [main] INFO orderService - 業(yè)務(wù)操作...18
10:56:26.240 [main] INFO orderService - 業(yè)務(wù)操作...19
10:56:26.255 [main] INFO orderService - 業(yè)務(wù)操作...20
10:56:26.271 [main] INFO orderService - ----------------------------
至此,我們就可以說,Sentinel 能夠正常工作了。
二、系統(tǒng)集成
上面只是一個很簡單的Demo示例,如果我們希望在我們的SpringBoot項(xiàng)目中使用Sentinel,還需要一些工作。
1、Sentinel 控制臺
Sentinel 提供一個輕量級的開源控制臺,它是使用SpringBoot開發(fā)的。
它提供機(jī)器發(fā)現(xiàn)以及健康情況管理、監(jiān)控(單機(jī)和集群),規(guī)則管理和推送的功能。
所以,我們先把這個控制臺運(yùn)行起來。
第一步,需要在https://github.com/alibaba/Sentinel/releases這個地址,下載最新版本的控制臺 jar 包。
第二步,使用命令啟動控制臺程序,其中 -Dserver.port=9080 用于指定 Sentinel 控制臺端口。
java -Dserver.port=9080 -Dcsp.sentinel.dashboard.server=localhost:9080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
第三步,我們的業(yè)務(wù)系統(tǒng)引入 Transport 模塊來與 Sentinel 控制臺進(jìn)行通信。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.7.2</version>
</dependency>
第四步,在我們的業(yè)務(wù)系統(tǒng)中,設(shè)置JVM啟動參數(shù),用來指明Sentinel控制臺的地址。
-Dcsp.sentinel.dashboard.server=127.0.0.1:9080
最后,啟動我們的業(yè)務(wù)系統(tǒng),然后打開Sentinel控制臺,如果可以看到機(jī)器列表就可以了。
2、定義規(guī)則
在定義規(guī)則之前,我們需要規(guī)劃好資源范圍。
什么意思呢?比如我們拿一個訂單業(yè)務(wù)來說,是不是所有的訂單操作都算一個資源?還是拆分開來看,創(chuàng)建訂單算一個資源,訂單查詢算另外一個資源。
所以,我們可以先把希望流控的資源名稱定義出來。
public final class ResourceConstants {
public static final String ORDER_SERVICE = OrderService.class.getName();
public static final String ORDER_SERVICE_ORDERS = ORDER_SERVICE+".orders";
public static final String ORDER_SERVICE_CREATE = ORDER_SERVICE+".create";
}
由于是一個SpringBoot項(xiàng)目,我們可以在系統(tǒng)啟動的時候,來加載流控規(guī)則。
@Component
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
initFlowRule(ResourceConstants.ORDER_SERVICE,5);
initFlowRule(ResourceConstants.ORDER_SERVICE_ORDERS,5);
}
public void initFlowRule(String resourceName,int count) {
FlowRule flowRule = new FlowRule(resourceName)
.setCount(count)
.setGrade(RuleConstant.FLOW_GRADE_QPS);
List<FlowRule> list = new ArrayList<>();
list.add(flowRule);
FlowRuleManager.loadRules(list);
}
}
然后,我們在Controller加入Sentinel的代碼,來達(dá)到流控的效果。
@RequestMapping("/getOrders")
public ResponseEntity getOrders(){
Entry entry = null;
try {
entry = SphU.entry(ResourceConstants.ORDER_SERVICE_ORDERS);
return ResponseEntity.ok(orderService.orders());
} catch (BlockException e) {
logger.error("請求被限流...{}",e.getRule().getResource());
return ResponseEntity.badRequest().body(e.getRule());
} finally {
if (entry != null) {
entry.exit();
}
}
}
現(xiàn)在,我們拿JMeter來測試一下,啟動10個線程來請求這個接口。只會通過5個請求,拒絕5個請求。
至此,我們已經(jīng)可以在SpringBoot項(xiàng)目中簡單使用Sentinel了,不過此時還有兩個很明顯的問題。
- 在每個需要流控的地方,通過API硬編碼,侵入性太強(qiáng)而且也不方便;
- 流控規(guī)則只保留在內(nèi)存中,系統(tǒng)重啟就沒了,沒有持久化規(guī)則數(shù)據(jù)。
接下來,我們來解決上述的兩個問題。
三、框架適配
得益于廣泛的開源生態(tài),Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊。我們只需要引入相應(yīng)的依賴并進(jìn)行簡單的配置即可快速地接入 Sentinel。
我們希望可以對 Web 請求進(jìn)行流量控制,那么需要引入Sentinel 提供與 Servlet 的整合。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-servlet</artifactId>
<version>1.7.2</version>
</dependency>
1、Filter配置
因?yàn)槭荢pringBoot應(yīng)用,我們通過Configuration進(jìn)行配置。
@Configuration
public class SentinelFilterConfig {
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
在我們自己的業(yè)務(wù)代碼中,就可以免去Sentinel API部分了。
@RequestMapping("/getOrders")
public ResponseEntity getOrders(){
return ResponseEntity.ok(orderService.orders());
}
在流控規(guī)則不變的情況下,我們拿JMeter啟動10個線程來請求這個接口。同樣的只會通過5個請求,拒絕5個請求。
2、UrlBlockHandler
默認(rèn)情況下,當(dāng)請求被限流時會返回默認(rèn)的提示頁面。
我們可以在代碼中調(diào)用WebServletConfig.setBlockPage(blockPage) 方法設(shè)定自定義的跳轉(zhuǎn) URL,當(dāng)請求被限流時會自動跳轉(zhuǎn)至設(shè)定好的 URL。
如果不打算讓它跳轉(zhuǎn)頁面,我們也可以實(shí)現(xiàn) UrlBlockHandler 接口并編寫定制化的限流處理邏輯。
比如像下面這樣,限流或熔斷之后,會向客戶端返回一個異常的HTTP狀態(tài)碼和提示信息。
public class SentinelUrlBlockHandler implements UrlBlockHandler {
public static final String flowMsg = "觸發(fā)流控機(jī)制~";
public static final String degradeMsg = "觸發(fā)熔斷機(jī)制~";
Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex){
logger.error("熔斷限流...{}",ex.getRule());
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.BAD_REQUEST.value());
PrintWriter out = response.getWriter();
if (ex instanceof FlowException){
out.print(flowMsg);
}else if (ex instanceof DegradeException){
out.print(degradeMsg);
}
out.flush();
out.close();
}
}
然后將其注冊至 WebCallbackManager 中。
WebCallbackManager.setUrlBlockHandler(new SentinelUrlBlockHandler());
3、UrlCleaner
Sentinel Web Filter 會將每個到來的不同的 URL 都作為不同的資源處理。
比如訂單業(yè)務(wù)中的,創(chuàng)建訂單、訂單查詢、訂單刪除等等,因?yàn)閁RL的不同,都會被當(dāng)作不同的資源。
如果我們希望將這些操作都?xì)w到訂單資源下/order/*,就需要實(shí)現(xiàn) UrlCleaner 接口清洗一下資源。
比如像下面這樣,將資源歸類。比如/order/getOrders和/order/createOrder,都會變成/order/*。
public class SentinelUrlClean implements UrlCleaner {
@Override
public String clean(String originUrl) {
if (originUrl == null || originUrl.isEmpty()) {
return originUrl;
}
int lastSlashIndex = originUrl.lastIndexOf("/");
if (lastSlashIndex >= 0) {
originUrl = originUrl.substring(0, lastSlashIndex) + "/*";
}
return originUrl;
}
}
然后將其注冊至 WebCallbackManager 中。
WebCallbackManager.setUrlCleaner(new SentinelUrlClean());
當(dāng)時,更絕對一些,如果整個系統(tǒng)都采用一個資源,那么這里只返回一個固定的url也可以。
四、最佳實(shí)踐
上面我們說到,現(xiàn)在的Sentinel規(guī)則數(shù)據(jù)都只保留在內(nèi)存中,沒辦法做到集中管理和推送規(guī)則,不具備生產(chǎn)環(huán)境可用性。
規(guī)則管理及推送,一般有三種方式。
- 原始模式
將規(guī)則推送至客戶端并直接更新到內(nèi)存中。重啟即消失,不建議在生產(chǎn)環(huán)境中使用。
- Pull 模式
客戶端主動向某個規(guī)則管理中心定期輪詢拉取規(guī)則,這個規(guī)則中心可以是 RDBMS、文件 等。不保證實(shí)時性,拉取過于頻繁可能會導(dǎo)致性能問題。
- Push 模式
規(guī)則中心統(tǒng)一推送,客戶端通過注冊監(jiān)聽器的方式時刻監(jiān)聽變化,比如使用 Nacos、Zookeeper 等配置中心,有更好的實(shí)時性和一致性。生產(chǎn)環(huán)境下一般采用 push 模式的數(shù)據(jù)源。
生產(chǎn)環(huán)境下一般更常用的是 push 模式的數(shù)據(jù)源。對于 push 模式的數(shù)據(jù)源,如遠(yuǎn)程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不應(yīng)由 Sentinel 客戶端進(jìn)行,而應(yīng)該經(jīng)控制臺統(tǒng)一進(jìn)行管理,直接進(jìn)行推送,數(shù)據(jù)源僅負(fù)責(zé)獲取配置中心推送的配置并更新到本地。因此推送規(guī)則正確做法應(yīng)該是 ** 配置中心控制臺/Sentinel 控制臺 → 配置中心 → Sentinel 數(shù)據(jù)源 → Sentinel **,而不是經(jīng) Sentinel 數(shù)據(jù)源推送至配置中心。
接下來我們來實(shí)現(xiàn)由Nacos配置中心統(tǒng)一管理數(shù)據(jù)。
1、啟動Nacos
關(guān)于Nacos本文不再多說,下載一個啟動就好了。
2、引入依賴
NacosDataSource,官方已經(jīng)提供了,我們引入相關(guān)依賴即可。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.7.2</version>
</dependency>
3、從數(shù)據(jù)源中讀取規(guī)則數(shù)據(jù)
在初始化NacosDataSource的時候,我們要指定Nacos的服務(wù)地址,groupId和dataId。
然后根據(jù)這些信息連接Nacos,去讀取里面的數(shù)據(jù)。并且注冊監(jiān)聽器,在Nacos配置中心的規(guī)則數(shù)據(jù)發(fā)生變化后,通知到客戶端。
說起來可能比較復(fù)雜,但是作為客戶端使用的話,其實(shí)比較簡單。我們搞一個類,去連接它就可以了。
@Component
public class DataSourceRuleManager {
private static final String remoteAddress = "localhost:8848";
private static final String groupId = "sentinel.group";
private static final String flowDataId = "flow.rule";
@PostConstruct
public void loadFlowRules() {
FlowConverter converter = new FlowConverter();
//連接Nacos,讀取配置信息并通過converter將內(nèi)容轉(zhuǎn)換為對象
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource =
new NacosDataSource<>(remoteAddress,groupId,flowDataId,converter);
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
//轉(zhuǎn)換器 從Nacos配置中心讀取到的數(shù)據(jù)轉(zhuǎn)換為對象
public class FlowConverter implements Converter {
@Override
public Object convert(Object source) {
return JSON.parseArray(source.toString(),FlowRule.class);
}
}
}
配置完之后,我們就可以啟動業(yè)務(wù)系統(tǒng)了。
4、從Nacos配置中心添加規(guī)則數(shù)據(jù)
現(xiàn)在就可以通過Nacos控制臺,向配置中心添加規(guī)則數(shù)據(jù)了。
有一點(diǎn)需要注意的是,由于我們的轉(zhuǎn)換器是通過JSON解析FlowRule類型的數(shù)組對象,所以配置內(nèi)容里面的格式和屬性名稱要對應(yīng)起來,否則解析會失敗。
通過擴(kuò)展讀數(shù)據(jù)源的方式,當(dāng)我們在Nacos配置中心發(fā)布新的內(nèi)容后,相應(yīng)的我們業(yè)務(wù)系統(tǒng)里面的規(guī)則也會更新,Sentinel控制臺里面的規(guī)則也一樣會同步更新,就實(shí)現(xiàn)了規(guī)則中心統(tǒng)一推送和持久化。
還有一種方式是直接通過 Sentinel 控制臺 → 配置中心,這樣的話需要修改dashboard的實(shí)現(xiàn),過程雖然不難但比較復(fù)雜,由于篇幅有限,本文就不再贅述。感興趣的朋友可以留言交流~
總結(jié)
本文簡單介紹了分布式系統(tǒng)熔斷、限流組件Sentinel的使用。為了達(dá)到生產(chǎn)環(huán)境的基本可用,包含了 Sentinel 與 Servlet 的整合和規(guī)則中心統(tǒng)一推送和持久化。
本文只是Sentinel生態(tài)中的一小部分,更多內(nèi)容如多種策略的流控和熔斷機(jī)制、黑白名單控制、框架適配、實(shí)現(xiàn)原理等內(nèi)容,有時間后續(xù)分享~
原創(chuàng)不易,客官們點(diǎn)個贊再走嘛,這將是筆者持續(xù)寫作的動力~