點贊再看,養成習慣,微信搜尋【牧小農】關注我獲取更多資訊,風裡雨裡,小農等你,很高興能夠成為你的朋友。
專案原始碼地址:公眾號回覆 sentinel,即可免費獲取原始碼
在上一篇中,我們講解了 Sentinel 限流詳解,其中詳細講解了各個規則下的限流是如何操作,有興趣的小夥伴可以瞭解一下,有不少小夥伴在後臺留言說,想了解一下 sentinel
中如何使用@SentinelResource和openFeign
來進行服務熔斷和降級的操作,大家知道小農對於小夥伴的要求,那都是儘量滿足,今天我們就來好好說一下,@SentinelResource
和openFeign
在上一節中,我們也使用到過這個註解,我們需要了解的是其中兩個屬性:
@SentinelResource(value = "test/get")
@SentinelResource(value = "test/get",entryType = EntryType.IN)
blockHandler: 處理異常(BlockException)的函數名稱,不必填,使用時注意兩點:
BlockException
)型別的引數。blockHandlerClass: 非必填,存放blockHandler的類。對應的處理常式必須static修飾,否則無法解析,必須是public,返回型別與原方法一致,引數型別需要和原方法相匹配,並在最後加上BlockException
型別的引數
fallback: 非必填,用於在丟擲異常的時候提供fallback處理邏輯。fallback函數可以針對所有型別的異常(除了execptionsToIgnore 裡面排除掉的異常型別)進行處理
exceptionsToIgnore:非必填,指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣丟擲
今天我們就針對於上面的幾個點詳細的展開介紹,在實際應用中我們如何進行操作。我們先來編寫一個新的控制器型別,這裡我們使用cloud-alibaba-sentinel-8006
專案進行操作,對應原始碼已經放在開頭位置,需要請自取。
@SentinelResource
既可以設定資源名稱也可以設定URL,當我們設定了blockHandler
屬性時,如果達到閾值時,會呼叫對應的方法提示限流資訊,如果沒有設定blockHandler
屬性,系統會走預設的限流資訊(Blocked by Sentinel (flow limiting)
)
首先我們使用預設的@SentinelResource
註解,系統會針對對應的地址呼叫預設的例外處理方法。
@GetMapping("/restUrl")
@SentinelResource(value = "restUrl")
public String restUrl(){
return " restUrl";
}
注意:我們重啟專案之後,要先存取,才能去設定對應的限流規則
先存取http://localhost:8006/restUrl
,在新增流控規則
此時如果沒有自己定義限流處理方法,會走系統預設的
使用@SentinelResource
註解同時使用blockHandler
屬性
@GetMapping("resourceTest")
@SentinelResource(value = "resourceTest",blockHandler = "handler_resource")
public String resourceTest(){
return "resourceTest";
}
public String handler_resource(BlockException exception){
return "系統繁忙,請稍後再試";
}
先存取http://localhost:8006/resourceTest
,在新增流控規則
再去快速的去存取http://localhost:8006/resourceTest
就會出現我們在程式碼中設定的限流例外處理資訊,如下圖所示:
上面就展示了我們使用blockHandler
屬性時,出現的我們自己設定的異常提示,但是當我們使用上面兩種方案的時候,會出現一些問題,如果我們的業務邏輯比較複雜,熔斷的業務場景比較多,上面的顯然不能夠滿足我們的應用,而且這種自定義方法是和我們的業務程式碼耦合在一起的,在實際開發中,會顯得不夠優雅,每個業務方法對新增一個對應的限流處理方法,會讓程式碼顯得臃腫,而且無法實現統一處理。在這裡我們就需要提到我們另外一個屬性—blockHandlerClass
此屬性中設定的方法必需為 static 函數,否則無法解析。首先我們需要建立一個類用於專門處理自定義限流處理邏輯,這裡記住,方法一定要是靜態,否則無法解析,如下所示:
import com.alibaba.csp.sentinel.slots.block.BlockException;
/**
* Sentinel限流自定義邏輯
*/
public class SentinelExptioinHandler {
public static String handlerMethodError(BlockException exception){
return "handlerMethodError:服務異常,請稍後重試!";
}
public static String handlerMethodNetwork(BlockException exception){
return "handlerMethodNetwork:網路錯誤,連線超時,請稍後重試!";
}
}
同時我們新增一個可存取的介面方法,設定@SentinelResource
註解和blockHandlerClass
屬性對應的型別和這個型別中對應的處理方法。
/**
* 此方法用到了自定義限流處理型別CustomerBlockHandler
* 中的handlerException1方法來處理限流邏輯。
*/
@GetMapping("/buildExption")
@SentinelResource(value = "buildExption",
blockHandlerClass = SentinelExptioinHandler.class,blockHandler = "handlerMethodError")
public String buildExption(){
return "hello buildExption";
}
然後我們先存取http://localhost:8006/buildExption
後,來給它新增限流規則。
我們再次存取http://localhost:8006/buildExption
後,這個時候我們來看一下如果超過閾值之後使用的處理方法是否是我們的SentinelExptioinHandler.handlerMethodError()
,當我們頻繁的存取地址,就會看到出現了我們在例外處理類中設定的方法。
如果我們想要體現,網路異常的操作,我們只需要替換blockHandler
中的handlerMethodError
改為handlerMethodNetwork
,重啟專案後,重複上面的步驟,再來看一下,就會出現下面的提示:
在微服務中,由於業務的拆分,一般會出現請求鏈路過程的情況,當一個使用者發起一個請求,通常需要幾個微服務才能完成,在高並行的場景下,這種服務之間的依賴對系統的穩定性影響比較大,如果其中一個環節出現網路延遲或者請求超時等問題會導致其他服務的不可用並形成阻塞,從而導致雪崩,服務熔斷就是用來解決這種情況,當一個服務提供在無法提供正常服務時,為了放在雪崩的方式,會將當前介面和外部隔離,觸發熔斷,在熔斷時間內,請求都會返回失敗,直到服務提供正常,才會結束熔斷。簡單來說,服務熔斷就是應對微服務雪崩的一種鏈路保護機制
為了模擬實際的應用場景,我們需要整合Ribbon+openFeign
,來搭建真實的應用場景。首先我們需要利用Ribbon進行負載均衡的呼叫,我們先來建立消費者(cloud-alibab-consumer-8083
)和兩個服務提供者(cloud-alibaba-provider-9003/9004
)
我們先來搭建服務提供者
pom檔案
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.muxiaonong</groupId>
<artifactId>cloud-alibaba-commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
yml檔案
server:
port: 9003
spring:
application:
name: nacos-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #設定Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
主啟動類新增@EnableDiscoveryClient
註解
@SpringBootApplication
@EnableDiscoveryClient
public class CloudAlibabaProvider9003Application {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaProvider9003Application.class, args);
}
}
新增商品資訊請求類
@RestController
public class GoodsController {
@Value("${server.port}")
private String serverPort;
//模仿資料庫儲存資料
public static HashMap<Long,String> hashMap = new HashMap<>();
static {
hashMap.put(1l,"面膜");
hashMap.put(2l,"哈密瓜");
hashMap.put(3l,"方便麵");
}
@GetMapping("queryGoods/{id}")
public Response<String> queryGoods(@PathVariable("id") Long id){
Response<String> response = new Response(200,"成功請求:"+serverPort,hashMap.get(id));
return response;
}
}
到這裡服務提供者就搭建完成了
注意:另外一個服務提供者一樣,只需要埠不一樣即可,在這裡就不做重複性的演示
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.muxiaonong</groupId>
<artifactId>cloud-alibaba-commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
yml檔案
server:
port: 8083
spring:
application:
name: nacos-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#設定Sentinel dashboard地址
dashboard: localhost:8080
#預設8719埠,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的埠
port: 8719
#消費者將要去存取的微服務名稱(註冊成功進nacos的微服務提供者)
service-url:
nacos-user-service: http://nacos-provider
主啟動類新增@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class CloudAlibabConsumer8083Application {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabConsumer8083Application.class, args);
}
}
存取類
/**
* @program: spring-cloud-alibaba
* @ClassName DemoController
* @description:
* @author: 牧小農
* @create: 2022-06-04 23:10
* @Version 1.0
**/
@RestController
public class DemoController {
@Autowired
private RestTemplate restTemplate;
/**
* 消費者去存取具體服務,這種寫法可以實現
* 組態檔和程式碼的分離
*/
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/goods/{id}")
public Response<String> fallback(@PathVariable Long id){
//通過Ribbon發起遠端存取,存取9003/9004
if(id <= 3) {
Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
return result;
}else {
throw new NullPointerException("未查詢到對應的資料");
}
}
}
我們先啟動9003/9004,在啟動8083,然後存取http://localhost:8083/consumer/goods/2
,就可以看到在瀏覽器中,如果9003/9004相互切換,說明我們搭建成功。
SentinelResource
的fallback
屬性,是一個可選項,主要用於丟擲異常的時候提供處理邏輯,該函數可以針對所有的異常型別(除了exceptionsToIgnore
排除的異常型別,等下會講解)進行處理,對於fallback的函數簽名和位置要求:
fallbackClass
為對應的類的 Class
物件,注意對應的函數必需為 static 函數,否則無法解析案例:
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/goods/{id}")
//如果不設定這個註解和fallback引數,異常會原樣彈出
//如果設定SentinelResource註解的fallback屬性,會按照設定的方法處理Java異常
@SentinelResource(value = "falllback",fallback = "fallbackHandler")//被標註的異常將會被 原樣丟擲
public Response<String> fallback(@PathVariable Long id){
//通過Ribbon發起遠端存取,存取9003/9004
if(id <= 3) {
Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
return result;
}else {
throw new NullPointerException("未查詢到對應的資料");
}
}
//保證方法簽名基本保持一致,但是要新增異常型別引數
public Response<String> fallbackHandler(Long id,Throwable e){
Response<String> result = new Response<>(500,"出現未知商品id","商品不存在");
return result;
}
在這裡如果我們去存取id超過3的數位的時候請求時(http://localhost:8083/consumer/goods/6
),如果我們沒有設定fallback屬性,會彈出NullPointerException
的錯誤
現在當我們去存取設定了 fallback屬性的時http://localhost:8083/consumer/goods/6
會出現我們設定的引數。
fallback
屬性和blockHandler
有點類似,也可以設定fallbackClass
屬性,用來指定對應型別,來處理對應的異常型別,但是方法也是需要為靜態方法,否則無法解析。
那麼既然fallback
屬性和blockHandler
都能進行限流,那麼他們有什麼不同,哪一個的優先順序更高?首先我們要知道blockHandler屬性
是針對於Sentinel異常,blockHandler
對應處理 BlockException
的函數名稱,而fallback屬性
針對於Java異常,如果我們同時設定blockHandler和fallback
,會執行哪個方法呢?我們來看一下
@GetMapping("/consumer/goods/{id}")
//如果不設定這個註解和fallback引數,異常會原樣彈出
//如果設定SentinelResource註解的fallback屬性,會按照設定的方法處理Java異常
@SentinelResource(value = "falllback",fallback = "fallbackHandler",blockHandler = "blockHandler")
public Response<String> fallback(@PathVariable Long id){
//通過Ribbon發起遠端存取,存取9003/9004
if(id <= 3) {
Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
return result;
}else {
throw new NullPointerException("未查詢到對應的資料");
}
}
//保證方法簽名基本保持一致,但是要新增異常型別引數
public Response<String> fallbackHandler(Long id,Throwable e){
Response<String> result = new Response<>(500,"出現未知商品id","商品不存在");
return result;
}
//處理Sentinel限流
public Response<String> blockHandler(Long id, BlockException e){
Response<String> result = new Response<>(501,"sentinel限流操作","blockHandler 限流");
return result;
}
新增熔斷規則,在一秒內最小請求次數為5,如果異常超過2個時,觸發熔斷規則。
這個時候我們再來存取http://localhost:8083/consumer/goods/6
時,沒有觸發熔斷之前出現異常,由fallback進行處理
當我們快速點選,觸發熔斷規則時,這是時候則由blockHandler
進行處理。
當我們介紹上面的操作後,我們再給大家介紹關於sentinel
的另外一個屬性 exceptionsToIgnore
用於指定哪些異常被排除,不會計入異常統計中,也不會進入 fallback屬性處理的方法,會原樣丟擲
@GetMapping("/consumer/goods/{id}")
//新增SentinelResource註解的fallback屬性,同時設定方法來解決Java異常
@SentinelResource(value = "falllback",fallback = "fallbackHandler",blockHandler = "blockHandler",
exceptionsToIgnore = {NullPointerException.class})//被標註的異常將會被 原樣丟擲
public Response<String> fallback(@PathVariable Long id){
//通過Ribbon發起遠端存取,存取9003/9004
if(id <= 3) {
Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
return result;
}else {
throw new NullPointerException("未查詢到對應的資料");
}
}
啟動專案,當我們再去存取http://localhost:8083/consumer/goods/6
的時候,出現原有異常。
在這一節中,我們主要講解了sentinel服務熔斷的這些事,包括@SentinelResource
註解的使用方式和場景,以及ribbon實現負載均衡的使用,服務熔斷場景我們主要講解兩個,一個是ribbon實現的,一個是openFeign
實現。下面我們就來了解一下基於openFeign
如何實現負載均衡和服務熔斷。
OpenFeign是一種宣告式、模板化的HTTP使用者端。在Spring Cloud中使用OpenFeign,可以做到使用HTTP請求存取遠端服務,就像呼叫本地方法一樣的,開發者完全感知不到這是在呼叫遠端方法,更感知不到在存取HTTP請求,用法其實就是編寫一個介面,在介面上新增註解即可。
可以簡單理解它是借鑑Ribbon的基礎之上,封裝的一套服務介面+註解的方式的遠端呼叫器。由它來幫助我們定義和實現依賴服務介面的定義,只需建立一個介面並使用註解的方式進行設定。進一步簡化我們的操作。
演示專案為:cloud-alibaba-openFeign-8009
,呼叫服務為9003/9004,原始碼在開頭,需要請自取
pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
yml設定
server:
port: 8009
spring:
application:
name: nacos-consumer-openFeign
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: '*'
啟動類新增 @EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CloudAlibabaOpenFeign8009Application {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaOpenFeign8009Application.class, args);
}
}
@FeignClient
@Service
@FeignClient("nacos-provider")
public interface GoodsFeign {
@GetMapping("queryGoods/{id}")
public Response<String> queryGoods(@PathVariable("id") Long id);
}
請求控制類
@RestController
public class FeignController {
@Autowired
private GoodsFeign goodsFeign;
@GetMapping("query/{id}")
public Response<String> query(@PathVariable("id") Long id){
return goodsFeign.queryGoods(id);
}
}
我們一次啟動,9003/9004,以及我們的消費者服務cloud-alibaba-openFeign-8009
,當我們的服務都啟動成功後,存取http://localhost:8009/query/1
,如果看到我們的埠切換展示就表示成功了
OpenFeign 預設的超時時間為一秒鐘,如果伺服器端業務超過這個時間,則會報錯,為了避免這樣的情況,我們可以設定feign使用者端的超時控制。我們先來看一下如果我們設定一個延時任務openFeign會提示怎麼樣的資訊。我們需要在服務提供者(9003/9004)那裡設定一個阻塞三秒的請求。
@GetMapping("/readTimeOut")
public String readTimeOut() {
try {
System.out.println(serverPort+"網路連線超時,延遲響應");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
然後通過feign進行呼叫
@GetMapping("/readTimeOut")
public String readTimeOut();
@GetMapping("/query/readTimeOut")
public String readTimeOut() {
String str = goodsFeign.readTimeOut();
return str;
}
這個時候當我們去存取http://localhost:8009/query/readTimeOut
時,使用者端會提示報錯,提示我們連線超時
這個時候我們可以設定feign的超時時間進行控制,由於OpenFeign 底層是ribbon 。所以超時控制由ribbon來控制。在yml檔案中設定,只需要在8009中的yml新增這樣一段程式碼即可。
ribbon: #設定feign使用者端超時時間(預設支援ribbon)
ReadTimeout: 5000 #建立連線所用的時間,適用於網路狀況正常的情況下,兩端連線所用的時間
ConnectTimeout: 5000 #建立連線後從伺服器讀取到可用資源所用的時間
當我們重新啟動專案後,再來存取我們當前介面,成功返回正確資訊
說起OpenFeign
,我們不得不提它的一個很小,但是很實用的一個紀錄檔功能。我們可以通過設定調整紀錄檔級別,這樣有利於我們從feign中瞭解請求和響應的細節,對介面的呼叫情況進行監控。
OpenFeign
紀錄檔級別分類四種
我們在啟動類中通過@Bean註解注入紀錄檔功能即可
@Bean
Logger.Level feignLoggerLevel(){
//開啟全紀錄檔
return Logger.Level.FULL;
}
yml中新增紀錄檔開啟功能
logging:
level:
# openfeign紀錄檔以什麼級別監控哪個介面
com.muxiaonong.feign.GoodsFeign: debug
這樣我們就可以在請求呼叫以後看到紀錄檔的詳細資訊了
我們已經瞭解了openFeign的基本使用,那麼我們要如何將Sentinel和OpenFeign進行整合呢,下面我們就來帶大家通過Sentinel來進行整合OpenFegin
yml中新增Sentinel對OpenFeign的支援
# 啟用Sentinel對OpenFeign的支援
feign:
sentinel:
enabled: true
在feign中新增對fallback的支援
@Service
@FeignClient(value = "nacos-provider",fallback = GoodsServiceImpl.class)
public interface GoodsFeign {
@GetMapping("queryGoods/{id}")
public Response<String> queryGoods(@PathVariable("id") Long id);
@GetMapping("/readTimeOut")
public String readTimeOut();
}
@Component
public class GoodsServiceImpl implements GoodsFeign {
@Override
public Response<String> queryGoods(Long id) {
return new Response<>(501,"服務降級處理返回資訊",null);
}
@Override
public String readTimeOut() {
return null;
}
}
這個時候我們來請求http://localhost:8009/query/1
,時是正常的,但是當我們關閉服務提供者(9003/9004)時,就出觸發服務降級操作,提示下面資訊
熔斷由服務不可用引起,降級由業務實際情況和系統資源負載設定等關係引起,不管是對於熔斷還是降級都是從系統穩定性出發,保證系統的最大可用。
到這裡,我們今天的內容就講完了,有疑問或者想要交流的小夥伴記得在下方留言,小農看見了會第一時間回覆大家。如果覺得文中內容對你有幫助,記得點贊關注,您的支援是我創作的最大動力!
我是牧小農,怕什麼真理無窮,進一步有進一步的歡喜,大家加油!