我們接着承接上篇繼續講下去 : 第三章 Nacos Discovery–服務治理,開始第四篇的學習
第四章 Sentinel–服務容錯
4.1 高並發帶來的問題
在微服務架構中,我們將業務拆分成一個個的服務,服務與服務之間可以相互調用,但是由於網絡原因或者自身的原因,服務並不能保證服務的100%可用,如果單個服務出現問題,調用這個服務就會出現網絡延遲,此時若有大量的網絡涌入,會形成任務堆積,最終導致服務癱瘓。
接下來,我們來模擬一個高並發的場景
@RestController
@Slf4j
public class OrderController2 {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
@RequestMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info("接收到{}號商品的下單請求,接下來調用商品微服務查詢此商品信息", pid);
//調用商品微服務,查詢商品信息
Product product = productService.findByPid(pid);
log.info("查詢到{}號商品的信息,內容是:{}", pid, JSON.toJSONString(product));
//模擬一次網絡延時
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//下單(創建訂單)
Order order = new Order();
order.setUid(1);
order.setUsername("測試用戶");
order.setPid(pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
//為了不產生太多垃圾數據,暫時不做訂單保存
//orderService.createOrder(order);
log.info("創建訂單成功,訂單信息為{}", JSON.toJSONString(order));
return order;
}
@RequestMapping("/order/message")
public String message() {
return "高並發下的問題測試";
}
}
2 修改配置文件中tomcat的並發數
server:
port: 8091
tomcat:
max-threads: 10
3 接下來使用壓測工具,對請求進行壓力測試
下載地址https://jmeter.apache.org/
- 第一步:修改配置,並啟動軟件
進入bin目錄,修改jmeter.properties
文件中的語言支持為language=zh_CN,然后點擊jmeter.bat
啟動軟件。
- 第二步:添加線程組
-
第三步:配置線程並發數
-
第四步:添加Http取樣
- 第五步:配置取樣,並啟動測試
4 訪問message方法觀察效果
結論:
此時會發現, 由於order方法囤積了大量請求, 導致message方法的訪問出現了問題,這就是服務雪崩的雛形。
4.2 服務雪崩效應
在分布式系統中,由於網絡原因或自身的原因,服務一般無法保證 100% 可用。如果一個服務出現了問題,調用這個服務就會出現線程阻塞的情況,此時若有大量的請求涌入,就會出現多條線程阻塞等待,進而導致服務癱瘓。
由於服務與服務之間的依賴性,故障會傳播,會對整個微服務系統造成災難性的嚴重后果,這就是服務故障的 “雪崩效應” 。
雪崩發生的原因多種多樣,有不合理的容量設計,或者是高並發下某一個方法響應變慢,亦或是某台機器的資源耗盡。我們無法完全杜絕雪崩源頭的發生,只有做好足夠的容錯,保證在一個服務發生問題,不會影響到其它服務的正常運行。也就是"雪落而不雪崩"。
常見容錯方案
要防止雪崩的擴散,我們就要做好服務的容錯,容錯說白了就是保護自己不被豬隊友拖垮的一些措施, 下面介紹常見的服務容錯思路和組件。
常見的容錯思路
常見的容錯思路有隔離
、超時
、限流
、熔斷
、降級
這幾種,下面分別介紹一下。
- 隔離
它是指將系統按照一定的原則划分為若干個服務模塊,各個模塊之間相對獨立,無強依賴。當有故障發生時,能將問題和影響隔離在某個模塊內部,而不擴散風險,不波及其它模塊,不影響整體的系統服務。常見的隔離方式有:線程池隔離和信號量隔離.
- 超時
在上游服務調用下游服務的時候,設置一個最大響應時間,如果超過這個時間,下游未作出反應,就斷開請求,釋放掉線程。
- 限流
限流就是限制系統的輸入和輸出流量已達到保護系統的目的。為了保證系統的穩固運行,一旦達到的需要限制的閾值,就需要限制流量並采取少量措施以完成限制流量的目的。
- 熔斷 在互聯網系統中,當下游服務因訪問壓力過大而響應變慢或失敗,上游服務為了保護系統整體的可用性,可以暫時切斷對下游服務的調用。這種犧牲局部,保全整體的措施就叫做熔斷。
服務熔斷一般有三種狀態:
-
熔斷關閉狀態(Closed)
服務沒有故障時,熔斷器所處的狀態,對調用方的調用不做任何限制 -
熔斷開啟狀態(Open)
后續對該服務接口的調用不再經過網絡,直接執行本地的fallback方法
- 半熔斷狀態(Half-Open)
嘗試恢復服務調用,允許有限的流量調用該服務,並監控調用成功率。如果成功率達到預期,則說明服務已恢復,進入熔斷關閉狀態;如果成功率仍舊很低,則重新進入熔斷關閉狀態。
- 降級
降級其實就是為服務提供一個托底方案,一旦服務無法正常調用,就使用托底方案。
常見的容錯組件
** Hystrix**
Hystrix是由Netflix開源的一個延遲和容錯庫,用於隔離訪問遠程系統、服務或者第三方庫,防止
級聯失敗,從而提升系統的可用性與容錯性。
- Resilience4J
Resilicence4J一款非常輕量、簡單,並且文檔非常清晰、豐富的熔斷工具,這也是Hystrix官方推薦的替代產品。不僅如此,Resilicence4j還原生支持Spring Boot 1.x/2.x,而且監控也支持和prometheus等多款主流產品進行整合。
- Sentinel
Sentinel 是阿里巴巴開源的一款斷路器實現,本身在阿里內部已經被大規模采用,非常穩定。
下面是三個組件在各方面的對比:
圖片來源 :黑馬
4.4 Sentinel入門
4.4.1 什么是Sentinel
Sentinel (分布式系統的流量防衛兵) 是阿里開源的一套用於服務容錯的綜合性解決方案。它以流量為切入點, 從流量控制、熔斷降級、系統負載保護等多個維度來保護服務的穩定性。
Sentinel 具有以下特征:
-
豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景, 例如秒殺(即突發流量控制在系統容量可以承受的范圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等。
-
完備的實時監控:Sentinel 提供了實時的監控功能。通過控制台可以看到接入應用的單台機器秒級數據, 甚至 500 台以下規模的集群的匯總運行情況。
-
完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定制邏輯。例如定制規則管理、適配動態數據源等。
Sentinel 分為兩個部分:
-
**核心庫(Java 客戶端)**不依賴任何框架/庫,能夠運行於所有 Java 運行時環境,同時對 Dubbo /Spring Cloud 等框架也有較好的支持。
-
控制台(Dashboard)基於 Spring Boot 開發,打包后可以直接運行,不需要額外的 Tomcat 等應用容器。
4.4.2 微服務集成Sentinel
為微服務集成Sentinel非常簡單, 只需要加入Sentinel的依賴即可
1 在pom.xml
中加入下面依賴
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2 編寫一個Controller測試使用
@RestController
@Slf4j
public class OrderController3 {
@RequestMapping("/order/message1")
public String message1() {
return "message1";
}
@RequestMapping("/order/message2")
public String message2() {
return "message2";
}
}
4.4.3 安裝Sentinel控制台
Sentinel 提供一個輕量級的控制台, 它提供機器發現、單機資源實時監控以及規則管理等功能。
- 1 下載jar包,解壓到文件夾
https://github.com/alibaba/Sentinel/releases
2 啟動控制台
# 直接使用jar命令啟動項目(控制台本身是一個SpringBoot項目)
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080-Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
3 修改shop-order ,在里面加入有關控制台的配置
spring:
cloud:
sentinel:
transport:
port: 9999 #跟控制台交流的端口,隨意指定一個未使用的端口即可
dashboard: localhost:8080 # 指定控制台服務的地址
第4步: 通過瀏覽器訪問localhost:8080 進入控制台 ( 默認用戶名密碼是sentinel/sentinel)
補充:了解控制台的使用原理
Sentinel
的控制台其實就是一個SpringBoot編寫的程序。我們需要將我們的微服務程序注冊到控制台上,即在微服務中指定控制台的地址, 並且還要開啟一個跟控制台傳遞數據的端口, 控制台也可以通過此端口調用微服務中的監控程序獲取微服務的各種信息。
4.4.4 實現一個接口的限流
1 通過控制台為message1添加一個流控規則
2 通過控制台快速頻繁訪問, 觀察效果
4.5 Sentinel的概念和功能
4.5.1 基本概念
- 資源
資源就是Sentinel要保護的東西
資源是 Sentinel 的關鍵概念。它可以是 Java 應用程序中的任何內容,可以是一個服務,也可以是一個方法,甚至可以是一段代碼。
我們入門案例中的message1方法就可以認為是一個資源
- 規則
規則就是用 來定義如何進行保護資源的
作用在資源之上, 定義以什么樣的方式保護資源,主要包括流量控制規則、熔斷降級規則以及系統保護規則。
我們入門案例中就是為message1資源設置了一種流控規則, 限制了進入message1的流量
4.5.2 重要功能
Sentinel的主要功能就是容錯,主要體現為下面這三個:
- 流量控制
流量控制在網絡傳輸中是一個常用的概念,它用於調整網絡包的數據。任意時間到來的請求往往是隨機不可控的,而系統的處理能力是有限的。我們需要根據系統的處理能力對流量進行控制。Sentinel 作為一個調配器,可以根據需要把隨機的請求調整成合適的形狀。
- 熔斷降級
當檢測到調用鏈路中某個資源出現不穩定的表現,例如請求響應時間長或異常比例升高的時候,則對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯故障。
Sentinel 對這個問題采取了兩種手段:
-
通過並發線程數進行限制
Sentinel 通過限制資源並發線程的數量,來減少不穩定資源對其它資源的影響。當某個資源出現不穩定的情況下,例如響應時間變長,對資源的直接影響就是會造成線程數的逐步堆積。當線程數在特定資源上堆積到一定的數量之后,對該資源的新請求就會被拒絕。堆積的線程完成任務后才開始繼續接收請求。 -
通過響應時間對資源進行降級
除了對並發線程數進行控制以外,Sentinel 還可以通過響應時間來快速降級不穩定的資源。當依賴的資源出現響應時間過長后,所有對該資源的訪問都會被直接拒絕,直到過了指定的時間窗口之后才重新恢復。
Sentinel 和 Hystrix 的區別
兩者的原則是一致的, 都是當一個資源出現問題時, 讓其快速失敗, 不要波及到其它服務
但是在限制的手段上, 確采取了完全不一樣的方法:
Hystrix 采用的是線程池隔離的方式, 優點是做到了資源之間的隔離, 缺點是增加了線程
切換的成本。
Sentinel 采用的是通過並發線程的數量和響應時間來對資源做限制。
- 系統負載保護
Sentinel 同時提供系統維度的自適應保護能力。當系統負載較高的時候,如果還持續讓
請求進入可能會導致系統崩潰,無法響應。在集群環境下,會把本應這台機器承載的流量轉發到其它的機器上去。如果這個時候其它的機器也處在一個邊緣狀態的時候,Sentinel 提供了對應的保護機制,讓系統的入口流量和系統的負載達到一個平衡,保證系統在能力范圍之內處理最多的請求。
總之一句話: 我們需要做的事情,就是在Sentinel的資源上配置各種各樣的規則,來實現各種容錯的功能。
4.6 Sentinel規則
4.6.1 流控規則
流量控制
,其原理是監控應用流量的QPS(每秒查詢率) 或並發線程數等指標,當達到指定的閾值時對流量進行控制,以避免被瞬時的流量高峰沖垮,從而保障應用的高可用性。
第1步: 點擊簇點鏈路,我們就可以看到訪問過的接口地址,然后點擊對應的流控按鈕,進入流控規則配置頁面。新增流控規則界面如下:
資源名:唯一名稱,默認是請求路徑,可自定義
針對來源:指定對哪個微服務進行限流,默認指default,意思是不區分來源,全部限制
閾值類型/單機閾值:
- QPS(每秒請求數量): 當調用該接口的QPS達到閾值的時候,進行限流
- 線程數:當調用該接口的線程數達到閾值的時候,進行限流
是否集群:暫不需要集群
接下來我們以QPS為例來研究限流規則的配置。
4.6.1.1 簡單配置
我們先做一個簡單配置,設置閾值類型為QPS,單機閾值為3。即每秒請求量大於3的時候開始限流。
接下來,在流控規則頁面就可以看到這個配置。
然后快速訪問/order/message1
接口,觀察效果。此時發現,當QPS > 3的時候,服務就不能正常響應,而是返回Blocked by Sentinel (flow limiting)結果。
4.6.1.2 配置流控模式
點擊上面設置流控規則的編輯按鈕,然后在編輯頁面點擊高級選項,會看到有流控模式一欄。
sentinel共有三種流控模式,分別是:
sentinel共有三種流控模式,分別是:
- 直接(默認):接口達到限流條件時,開啟限流
- 關聯:當關聯的資源達到限流條件時,開啟限流 [適合做應用讓步]
- 鏈路:當從某個接口過來的資源達到限流條件時,開啟限流
下面呢分別演示三種模式:
直接流控模式
直接流控模式是最簡單的模式,當指定的接口達到限流條件時開啟限流。上面案例使用的就是直接流控模式。
關聯流控模式
關聯流控模式指的是,當指定接口關聯的接口達到限流條件時,開啟對指定接口開啟限流。
第1步:配置限流規則, 將流控模式設置為關聯,關聯資源設置為的 /order/message2。
第2步:通過postman軟件向/order/message2
連續發送請求,注意QPS一定要大於3
第3步:訪問/order/message1
,會發現已經被限流
鏈路流控模式
鏈路流控模式指的是,當從某個接口過來的資源達到限流條件時,開啟限流。它的功能有點類似於針對來源配置項,區別在於:針對來源是針對上級微服務,而鏈路流控是針對上級接口,也就是說它的粒度更細。
第1步: 編寫一個service,在里面添加一個方法message
@Service
public class OrderServiceImpl3 {
@SentinelResource("message")
public void message() {
System.out.println("message");
}
}
第2步: 在Controller中聲明兩個方法,分別調用service中的方法
@RestController
@Slf4j
public class OrderController3 {
@Autowired
private OrderServiceImpl3 orderServiceImpl3;
@RequestMapping("/order/message1")
public String message1() {
orderServiceImpl3.message();
return "message1";
}
@RequestMapping("/order/message2")
public String message2() {
orderServiceImpl3.message();
return "message2";
}
}
第3步: 禁止收斂URL的入口 context
從1.6.3 版本開始,Sentinel Web filter默認收斂所有URL的入口context,因此鏈路限流不生效。
1.7.0 版本開始(對應SCA的2.1.1.RELEASE),官方在CommonFilter 引入了
WEB_CONTEXT_UNIFY 參數,用於控制是否收斂context。將其配置為 false 即可根據不同的URL 進行鏈路限流。
SCA 2.1.1.RELEASE之后的版本,可以通過配置spring.cloud.sentinel.web-context-unify=false即可關閉收斂
我們當前使用的版本是SpringCloud Alibaba 2.1.0.RELEASE,無法實現鏈路限流。
目前官方還未發布SCA 2.1.2.RELEASE,所以我們只能使用2.1.1.RELEASE,需要寫代碼的形式實
現
(1) 暫時將SpringCloud Alibaba的版本調整為2.1.1.RELEASE
<spring-cloud-alibaba.version>2.1.1.RELEASE</spring-cloud-alibaba.version>
(2) 配置文件中關閉sentinel的CommonFilter實例化
spring:
cloud:
sentinel:
filter:
# sentienl 默認生效,本地調試false
enabled: false
(3) 添加一個配置類,自己構建CommonFilter實例
import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterContextConfig {
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口資源關閉聚合
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
第4步: 控制台配置限流規則
第5步: 分別通過/order/message1
和/order/message2
訪問, 發現2沒問題, 1的被限流了
4.6.1.3 配置流控效果
- 快速失敗(默認): 直接失敗,拋出異常,不做任何額外的處理,是最簡單的效果
- Warm Up:它從開始閾值到最大QPS閾值會有一個緩沖階段,一開始的閾值是最大QPS閾值的
1/3,然后慢慢增長,直到最大閾值,適用於將突然增大的流量轉換為緩步增長的場景。 - 排隊等待:讓請求以均勻的速度通過,單機閾值為每秒通過數量,其余的排隊等待; 它還會讓設
置一個超時時間,當請求超過超時間時間還未處理,則會被丟棄。
4.6.2 降級規則
降級規則就是設置當滿足什么條件的時候,對服務進行降級。Sentinel提供了三個衡量條件:
- 平均響應時間 :當資源的平均響應時間超過閾值(以 ms 為單位)之后,資源進入准降級狀態。如果接下來 1s 內持續進入 5 個請求,它們的 RT都持續超過這個閾值,那么在接下的時間窗口(以 s 為單位)之內,就會對這個方法進行服務降級。
注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項 -Dcsp.sentinel.statistic.max.rt=xxx 來配置。
- 異常比例:當資源的每秒異常總數占通過量的比值超過閾值之后,資源進入降級狀態,即在接下的時間窗口(以 s 為單位)之內,對這個方法的調用都會自動地返回。異常比率的閾值范圍是 [0.0,1.0]。
第1步: 首先模擬一個異常
int i = 0;
@RequestMapping("/order/message2")
public String message2() {
i++;
//異常比例為0.333
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message2";
}
第2步: 設置異常比例為0.25
- 異常數 :當資源近 1 分鍾的異常數目超過閾值之后會進行服務降級。注意由於統計時間窗口是分鍾級別的,若時間窗口小於 60s,則結束熔斷狀態后仍可能再進入熔斷狀態。
問題:
流控規則和降級規則返回的異常頁面是一樣的,我們怎么來區分到底是什么原因導致的呢?
4.6.3 熱點規則
熱點參數流控規則是一種更細粒度的流控規則, 它允許將規則具體到參數上。
熱點規則簡單使用
第1步: 編寫代碼
@RequestMapping("/order/message3")
@SentinelResource("message3")//注意這里必須使用這個注解標識,熱點規則不生效
public String message3(String name, Integer age) {
return name + age;
}
第2步: 配置熱點規則
第3步: 分別用兩個參數訪問,會發現只對第一個參數限流了
熱點規則增強使用
參數例外項允許對一個參數的具體值進行流控
編輯剛才定義的規則,增加參數例外項
4.6.4 授權規則
很多時候,我們需要根據調用來源來判斷該次請求是否允許放行,這時候可以使用 Sentinel 的來源訪問控制的功能。來源訪問控制根據資源的請求來源(origin)限制資源是否通過:
- 若配置白名單,則只有請求來源位於白名單內時才可通過;
- 若配置黑名單,則請求來源位於黑名單時不通過,其余的請求通過。
上面的資源名和授權類型不難理解,但是流控應用怎么填寫呢?
其實這個位置要填寫的是來源標識,Sentinel提供了RequestOriginParser 接口來處理來源。
只要Sentinel保護的接口資源被訪問,Sentinel就會調用RequestOriginParser 的實現類去解析訪問來源。
第1步: 自定義來源處理規則
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}
第2步: 授權規則配置
這個配置的意思是只有serviceName=pc不能訪問(黑名單)
第3步: 訪問 http://localhost:8091/order/message1?serviceName=pc觀察結果
4.6.5 系統規則
系統保護規則是從應用級別的入口流量進行控制,從單台機器的總體 Load、RT、入口 QPS 、CPU使用率和線程數五個維度監控應用數據,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。
系統保護規則是應用整體維度的,而不是資源維度的,並且僅對入口流量 (進入應用的流量) 生效。
-
Load(僅對 Linux/Unix-like 機器生效):當系統 load1 超過閾值,且系統當前的並發線程數超過系統容量時才會觸發系統保護。系統容量由系統的 maxQps * minRt 計算得出。設定參考值一般是 CPU cores * 2.5。
-
RT:當單台機器上所有入口流量的平均 RT 達到閾值即觸發系統保護,單位是毫秒。
-
線程數:當單台機器上所有入口流量的並發線程數達到閾值即觸發系統保護。
-
入口 QPS:當單台機器上所有入口流量的 QPS 達到閾值即觸發系統保護。
-
CPU使用率:當單台機器上所有入口流量的 CPU使用率達到閾值即觸發系統保護。
擴展: 自定義異常返回
import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//自定義異常返回頁面
@Component
public class ExceptionHandlerPage implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e) throws IOException {
response.setContentType("application/json;charset=utf-8");
ResponseData responseData = null;
//BlockException 異常接口,包含Sentinel的五個異常
// FlowException 限流異常
// DegradeException 降級異常
// ParamFlowException 參數限流異常
// AuthorityException 授權異常
// SystemBlockException 系統負載異常
if (e instanceof FlowException) {
responseData = new ResponseData(-1, "接口被限流了...");
} else if (e instanceof DegradeException) {
responseData = new ResponseData(-2, "接口被降級了...");
}
response.getWriter().write(JSON.toJSONString(responseData));
}
}
@Data
@AllArgsConstructor//全參構造
@NoArgsConstructor
//無參構造
class ResponseData {
private int code;
private String message;
}
4.7 @SentinelResource的使用
在定義了資源點之后,我們可以通過Dashboard來設置限流和降級策略來對資源點進行保護。同時還能通過@SentinelResource
來指定出現異常時的處理策略。
@SentinelResource
用於定義資源,並提供可選的異常處理和 fallback 配置項。其主要參數如下:
定義限流和降級后的處理方法
方式一:直接將限流和降級方法定義在方法中
@Service
@Slf4j
public class OrderServiceImpl3 {
int i = 0;
@SentinelResource(
value = "message",
blockHandler = "blockHandler",//指定發生BlockException時進入的方法
fallback = "fallback"//指定發生Throwable時進入的方法
方式二:將限流和降級方法外置到單獨的類中
)
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message";
}
//BlockException時進入的方法
public String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降級了...";
}
//Throwable時進入的方法
public String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口發生異常了...";
}
}
方式二: 將限流和降級方法外置到單獨的類中
@Service
@Slf4j
public class OrderServiceImpl3 {
int i = 0;
@SentinelResource(
value = "message",
blockHandlerClass = OrderServiceImpl3BlockHandlerClass.class,
blockHandler = "blockHandler",
fallbackClass = OrderServiceImpl3FallbackClass.class,
fallback = "fallback"
)
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message4";
}
}
@Slf4j
public class OrderServiceImpl3BlockHandlerClass {
//注意這里必須使用static修飾方法
public static String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降級了...";
}
}
@Slf4j
public class OrderServiceImpl3FallbackClass {
//注意這里必須使用static修飾方法
public static String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口發生異常了...";
}
}
4.8 Sentinel規則持久化
通過前面的講解,我們已經知道,可以通過Dashboard來為每個Sentinel客戶端設置各種各樣的規則,但是這里有一個問題,就是這些規則默認是存放在內存中,極不穩定,所以需要將其持久化。
本地文件數據源會定時輪詢文件的變更,讀取規則。這樣我們既可以在應用本地直接修改文件來更新規則,也可以通過 Sentinel 控制台推送規則。以本地文件數據源為例,推送過程如下圖所示:
首先 Sentinel 控制台通過 API 將規則推送至客戶端並更新到內存中,接着注冊的寫數據源會將新的規則保存到本地的文件中。
新建一個 config 寶
package com.spiritmark.config;
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.*;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Value;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class FilePersistence implements InitFunc {
@Value("spring.application.name")
private String appcationName;
@Override
public void init() throws Exception {
String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + appcationName;
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控規則
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降級規則
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系統規則
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授權規則
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 熱點參數規則
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
2 添加配置
在resources下創建配置目錄META-INF/services
,然后添加文件
com.alibaba.csp.sentinel.init.InitFunc
在文件中添加配置類的全路徑
com.spiritmark.config.FilePersistence
4.9 Feign整合Sentinel
第1步: 引入sentinel的依賴
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
第2步: 在配置文件中開啟Feign對Sentinel的支持
# 開啟feign對sentinel的支持
feign:
sentinel:
enabled: true
第3步: 創建容錯類
//容錯類要求必須實現被容錯的接口,並為每個方法實現容錯方案
@Component
@Slf4j
public class ProductServiceFallBack implements ProductService {
@Override
public Product findByPid(Integer pid) {
Product product = new Product();
product.setPid(-1);
return product;
}
}
第4步: 為被容器的接口指定容錯類
//value用於指定調用nacos下哪個微服務
//fallback用於指定容錯類
@FeignClient(value = "service-product", fallback = ProductServiceFallBack.class)
public interface ProductService {
@RequestMapping("/product/{pid}")
//指定請求的URI部分
Product findByPid(@PathVariable Integer pid);
}
第5步: 修改controller
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
//下單--fegin
@RequestMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info("接收到{}號商品的下單請求,接下來調用商品微服務查詢此商品信息", pid);
//調用商品微服務,查詢商品信息
Product product = productService.findByPid(pid);
if (product.getPid() == -1) {
Order order = new Order();
order.setPname("下單失敗");
return order;
}
log.info("查詢到{}號商品的信息,內容是:{}", pid, JSON.toJSONString(product));
//下單(創建訂單)
Order order = new Order();
order.setUid(1);
order.setUsername("測試用戶");
order.setPid(pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("創建訂單成功,訂單信息為{}", JSON.toJSONString(order));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return order;
}
}
第6步: 停止所有shop-product
服務,重啟shop-order 服務,訪問請求,觀察容錯效果
擴展: 如果想在容錯類中拿到具體的錯誤,可以使用下面的方式
@FeignClient(
value = "service-product",
//fallback = ProductServiceFallBack.class,
fallbackFactory = ProductServiceFallBackFactory.class)
public interface ProductService {
//@FeignClient的value + @RequestMapping的value值 其實就是完成的請求地址
"http://service-product/product/"+pid
@RequestMapping("/product/{pid}")//指定請求的URI部分
Product findByPid(@PathVariable Integer pid);
}
@Component
public class ProductServiceFallBackFactory implements
FallbackFactory<ProductService> {
@Override
public ProductService create(Throwable throwable) {
return new ProductService() {
@Override
public Product findByPid(Integer pid) {
throwable.printStackTrace();
Product product = new Product();
product.setPid(-1);
return product;
}
};
}
}
終於 寫完了 Sentinel 組件 ,更多具體 內容 請參照官網進行深入學習
官網 : https://github.com/alibaba/Sentinel