在基礎篇中咱們學習瞭如何爲項目整合Sentinel,並搭建了Sentinel的可視化控制檯,介紹及演示了各類Sentinel所支持的規則配置方式。本文則對Sentinel進行更進一步的介紹。java
首先咱們來了解控制檯是如何獲取到微服務的監控信息的:node
微服務集成Sentinel須要添加
spring-cloud-starter-alibaba-sentinel
依賴,該依賴中包含了sentinel-transport-simple-http模塊。集成了該模塊後,微服務就會經過配置文件中所配置的鏈接地址,將自身註冊到Sentinel控制檯上,並經過心跳機制告知存活狀態,由此可知Sentinel是實現了一套服務發現機制的。git
以下圖:
github
經過該機制,從Sentinel控制檯的機器列表中就能夠查看到Sentinel客戶端(即微服務)的通訊地址及端口號:
web
如此一來,Sentinel控制檯就能夠實現與微服務通訊了,當須要獲取微服務的監控信息時,Sentinel控制檯會定時調用微服務所暴露出來的監控API,這樣就能夠實現實時獲取微服務的監控信息。spring
另一個問題就是使用控制檯配置規則時,控制檯是如何將規則發送到各個微服務的呢?同理,想要將配置的規則推送給微服務,只須要調用微服務上接收推送規則的API便可。apache
咱們能夠經過訪問http://{微服務註冊的ip地址}:8720/api
接口查看微服務暴露給Sentinel控制檯調用的API,以下:
api
相關源碼:app
com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeatSender
com.alibaba.csp.sentinel.command.CommandHandler
的實現類本小節簡單介紹一下在代碼中如何使用Sentinel API,Sentinel主要有如下三個API:dom
BlockException
異常)示例代碼以下:
@GetMapping("/test-sentinel-api") public String testSentinelAPI(@RequestParam(required = false) String a) { String resourceName = "test-sentinel-api"; // 這裏不使用try-with-resources是由於Tracer.trace會統計不上異常 Entry entry = null; try { // 定義一個sentinel保護的資源,名稱爲test-sentinel-api entry = SphU.entry(resourceName); // 標識對test-sentinel-api調用來源爲test-origin(用於流控規則中「針對來源」的配置) ContextUtil.enter(resourceName, "test-origin"); // 模擬執行被保護的業務邏輯耗時 Thread.sleep(100); return a; } catch (BlockException e) { // 若是被保護的資源被限流或者降級了,就會拋出BlockException log.warn("資源被限流或降級了", e); return "資源被限流或降級了"; } catch (InterruptedException e) { // 對業務異常進行統計 Tracer.trace(e); return "發生InterruptedException"; } finally { if (entry != null) { entry.exit(); } ContextUtil.exit(); } }
對幾個可能有疑惑的點說明一下:
test-sentinel-api
的調用來源均爲test-origin
。例如使用postman或其餘請求方式調用了該資源,其來源都會被標識爲test-origin
BlockException
及其子類進行統計,其餘異常不在統計範圍,因此須要使用Tracer.trace
手動統計。1.3.1 版本開始支持自動統計,將在下一小節進行介紹相關官方文檔:
通過上一小節的代碼示例,能夠看到這些Sentinel API的使用方式並非很優雅,有點相似於使用I/O流API的感受,顯得代碼比較臃腫。好在Sentinel在1.3.1 版本開始支持@SentinelResource
註解,該註解可讓咱們避免去寫這種臃腫不美觀的代碼。但即使如此,也仍是有必要去學習Sentinel API的使用方式,由於其底層仍是得經過這些API來實現。
學習一個註解除了須要知道它能幹什麼以外,還得了解其支持的屬性做用,下表總結了@SentinelResource
註解的屬性:
屬性 | 做用 | 是否必須 |
---|---|---|
value | 資源名稱 | 是 |
entryType | entry類型,標記流量的方向,取值IN/OUT,默認是OUT | 否 |
blockHandler | 處理BlockException的函數名稱 | 否 |
blockHandlerClass | 存放blockHandler的類。對應的處理函數必須static修飾,不然沒法解析,其餘要求:同blockHandler | 否 |
fallback | 用於在拋出異常的時候提供fallback處理邏輯。fallback函數能夠針對全部類型的異常(除了exceptionsToIgnore 裏面排除掉的異常類型)進行處理 |
否 |
fallbackClass【1.6支持】 | 存放fallback的類。對應的處理函數必須static修飾,不然沒法解析,其餘要求:同fallback | 否 |
defaultFallback【1.6支持】 | 用於通用的 fallback 邏輯。默認fallback函數能夠針對全部類型的異常(除了exceptionsToIgnore 裏面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,以fallback爲準 |
否 |
exceptionsToIgnore【1.6支持】 | 指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣拋出 | 否 |
exceptionsToTrace | 須要trace的異常 | Throwable |
blockHandler,處理BlockException函數的要求:
public
BlockException
類型的參數blockHandlerClass
,並指定blockHandlerClass裏面的方法fallback函數要求:
Throwable
類型的參數fallbackClass
,並指定fallbackClass裏面的方法defaultFallback函數要求:
Throwable
類型的參數fallbackClass
,並指定 fallbackClass
裏面的方法如今咱們已經對@SentinelResource
註解有了一個比較全面的瞭解,接下來使用@SentinelResource
註解重構以前的代碼,直觀地瞭解下該註解帶來了哪些便利,重構後的代碼以下:
@GetMapping("/test-sentinel-resource") @SentinelResource( value = "test-sentinel-resource", blockHandler = "blockHandlerFunc", fallback = "fallbackFunc" ) public String testSentinelResource(@RequestParam(required = false) String a) throws InterruptedException { // 模擬執行被保護的業務邏輯耗時 Thread.sleep(100); return a; } /** * 處理BlockException的函數(處理限流) */ public String blockHandlerFunc(String a, BlockException e) { // 若是被保護的資源被限流或者降級了,就會拋出BlockException log.warn("資源被限流或降級了.", e); return "資源被限流或降級了"; } /** * 1.6 以前處理降級 * 1.6 開始能夠針對全部類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理 */ public String fallbackFunc(String a) { return "發生異常了"; }
注:@SentinelResource
註解目前不支持標識調用來源
Tips:
1.6.0 以前的版本
fallback
函數只針對降級異常(DegradeException
)進行處理,不能針對業務異常進行處理若
blockHandler
和fallback
都進行了配置,則被限流降級而拋出BlockException
時只會進入blockHandler
處理邏輯。若未配置blockHandler
、fallback
和defaultFallback
,則被限流降級時會將BlockException
直接拋出從 1.3.1 版本開始,註解方式定義資源支持自動統計業務異常,無需手動調用
Tracer.trace(ex)
來記錄業務異常。Sentinel 1.3.1 之前的版本須要自行調用Tracer.trace(ex)
來記錄業務異常
@SentinelResource
註解相關源碼:
com.alibaba.csp.sentinel.annotation.aspectj.AbstractSentinelAspectSupport
com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect
相關官方文檔:
若是有了解過Hystrix的話,應該就會知道Hystrix除了能夠對當前服務的接口進行容錯,還能夠對服務提供者(被調用方)的接口進行容錯。到目前爲止,咱們只介紹了在Sentinel控制檯對當前服務的接口添加相關規則進行容錯,但尚未介紹如何對服務提供者的接口進行容錯。
實際上有了前面的鋪墊,如今想要實現對服務提供者的接口進行容錯就很簡單了,咱們都知道在Spring Cloud體系中能夠經過RestTemplate或Feign實現微服務之間的通訊。因此只須要在RestTemplate或Feign上作文章就能夠了,本小節先以RestTemplate爲例,介紹如何整合Sentinel實現對服務提供者的接口進行容錯。
很簡單,只須要用到一個註解,在配置RestTemplate的方法上添加@SentinelRestTemplate
註解便可,代碼以下:
package com.zj.node.contentcenter.configuration; import org.springframework.cloud.alibaba.sentinel.annotation.SentinelRestTemplate; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class BeanConfig { @Bean @LoadBalanced @SentinelRestTemplate public RestTemplate restTemplate() { return new RestTemplate(); } }
注:@SentinelRestTemplate
註解包含blockHandler、blockHandlerClass、fallback、fallbackClass屬性,這些屬性的使用方式與@SentinelResource
註解一致,因此咱們能夠利用這些屬性,在觸發限流、降級時定製本身的異常處理邏輯
而後咱們再來寫段測試代碼,用於調用服務提供者的接口,代碼以下:
package com.zj.node.contentcenter.controller.content; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @Slf4j @RestController @RequiredArgsConstructor public class TestController { private final RestTemplate restTemplate; @GetMapping("/test-rest-template-sentinel/{userId}") public UserDTO test(@PathVariable("userId") Integer userId) { // 調用user-center服務的接口(此時user-center即爲服務提供者) return restTemplate.getForObject( "http://user-center/users/{userId}", UserDTO.class, userId); } }
編寫完以上代碼重啓項目並能夠正常訪問該測試接口後,此時在Sentinel控制檯的簇點鏈路中,就能夠看到服務提供者(user-center)的接口已經註冊到這裏來了,如今只須要對其添加相關規則就能夠實現容錯:
若咱們在開發期間,不但願Sentinel對服務提供者的接口進行容錯,能夠經過如下配置進行開關:
# 用於開啓或關閉@SentinelRestTemplate註解 resttemplate: sentinel: enabled: true
Sentinel實現與RestTemplate整合的相關源碼:
org.springframework.cloud.alibaba.sentinel.custom.SentinelBeanPostProcessor
上一小節介紹RestTemplate整合Sentinel時已經作了相關鋪墊,這裏就不廢話了直接上例子。首先在配置文件中添加以下配置:
feign: sentinel: # 開啓Sentinel對Feign的支持 enabled: true
定義一個FeignClient接口:
package com.zj.node.contentcenter.feignclient; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "user-center") public interface UserCenterFeignClient { @GetMapping("/users/{id}") UserDTO findById(@PathVariable Integer id); }
一樣的來寫段測試代碼,用於調用服務提供者的接口,代碼以下:
package com.zj.node.contentcenter.controller.content; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import com.zj.node.contentcenter.feignclient.UserCenterFeignClient; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor public class TestFeignController { private final UserCenterFeignClient feignClient; @GetMapping("/test-feign/{id}") public UserDTO test(@PathVariable Integer id) { // 調用user-center服務的接口(此時user-center即爲服務提供者) return feignClient.findById(id); } }
編寫完以上代碼重啓項目並能夠正常訪問該測試接口後,此時在Sentinel控制檯的簇點鏈路中,就能夠看到服務提供者(user-center)的接口已經註冊到這裏來了,行爲與RestTemplate整合Sentinel是同樣的:
默認當限流、降級發生時,Sentinel的處理是直接拋出異常。若是須要自定義限流、降級發生時的異常處理邏輯,而不是直接拋出異常該如何作?@FeignClient
註解中有一個fallback屬性,用於指定當遠程調用失敗時使用哪一個類去處理。因此在這個例子中,咱們首先須要定義一個類,並實現UserCenterFeignClient接口,代碼以下:
package com.zj.node.contentcenter.feignclient.fallback; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import com.zj.node.contentcenter.feignclient.UserCenterFeignClient; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component public class UserCenterFeignClientFallback implements UserCenterFeignClient { @Override public UserDTO findById(Integer id) { // 自定義限流、降級發生時的處理邏輯 log.warn("遠程調用被限流/降級了"); return UserDTO.builder(). wxNickname("Default"). build(); } }
而後在UserCenterFeignClient接口的@FeignClient
註解上指定fallback屬性,以下:
@FeignClient(name = "user-center", fallback = UserCenterFeignClientFallback.class) public interface UserCenterFeignClient { ...
接下來作一個簡單的測試,看看當遠程調用失敗時是否調用了fallback屬性所指定實現類裏的方法。爲服務提供者的接口添加一條流控規則,以下圖:
使用postman頻繁發生請求,當QPS超過1時,返回結果以下:
能夠看到,返回了代碼中定義的默認值。由此可證當限流、降級或其餘緣由致使遠程調用失敗時,就會調用UserCenterFeignClientFallback類裏所實現的方法。
可是又有另一個問題,這種方式沒法獲取到異常對象,而且控制檯不會輸出任何相關的異常信息,若業務須要打印異常日誌或針對異常進行相關處理的話該怎麼辦呢?此時就得用到@FeignClient
註解中的另外一個屬性:fallbackFactory,一樣須要定義一個類,只不過實現的接口不同。代碼以下:
package com.zj.node.contentcenter.feignclient.fallbackfactory; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import com.zj.node.contentcenter.feignclient.UserCenterFeignClient; import feign.hystrix.FallbackFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> { @Override public UserCenterFeignClient create(Throwable cause) { return new UserCenterFeignClient() { @Override public UserDTO findById(Integer id) { // 自定義限流、降級發生時的處理邏輯 log.warn("遠程調用被限流/降級了", cause); return UserDTO.builder(). wxNickname("Default"). build(); } }; } }
在UserCenterFeignClient接口的@FeignClient
註解上指定fallbackFactory屬性,以下:
@FeignClient(name = "user-center", fallbackFactory = UserCenterFeignClientFallbackFactory.class) public interface UserCenterFeignClient { ...
須要注意的是,fallback與fallbackFactory只能二選一,不能同時使用。
重複以前的測試,此時控制檯就能夠輸出相關異常信息了:
Sentinel實現與Feign整合的相關源碼:
org.springframework.cloud.alibaba.sentinel.feign.SentinelFeign
Sentinel默認在當前服務觸發限流或降級時僅返回簡單的異常信息,以下:
而且限流和降級返回的異常信息是同樣的,致使沒法根據異常信息區分是觸發了限流仍是降級。
因此咱們須要對錯誤信息進行相應優化,以即可以細緻區分觸發的是什麼規則。Sentinel提供了一個UrlBlockHandler接口,實現該接口便可自定義異常處理邏輯。具體以下示例:
package com.zj.node.contentcenter.sentinel; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException; import com.alibaba.csp.sentinel.slots.system.SystemBlockException; import lombok.Builder; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定義流控異常處理 * * @author 01 * @date 2019-08-02 **/ @Slf4j @Component public class MyUrlBlockHandler implements UrlBlockHandler { @Override public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e) throws IOException { MyResponse errorResponse = null; // 不一樣的異常返回不一樣的提示語 if (e instanceof FlowException) { errorResponse = MyResponse.builder() .status(100).msg("接口限流了") .build(); } else if (e instanceof DegradeException) { errorResponse = MyResponse.builder() .status(101).msg("服務降級了") .build(); } else if (e instanceof ParamFlowException) { errorResponse = MyResponse.builder() .status(102).msg("熱點參數限流了") .build(); } else if (e instanceof SystemBlockException) { errorResponse = MyResponse.builder() .status(103).msg("觸發系統保護規則") .build(); } else if (e instanceof AuthorityException) { errorResponse = MyResponse.builder() .status(104).msg("受權規則不經過") .build(); } response.setStatus(500); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); new ObjectMapper().writeValue(response.getWriter(), errorResponse); } } /** * 簡單的響應結構體 */ @Data @Builder class MyResponse { private Integer status; private String msg; }
此時再觸發流控規則就能夠響應代碼中自定義的提示信息了:
當配置流控規則或受權規則時,若須要針對調用來源進行限流,得先實現來源的區分,Sentinel提供了RequestOriginParser
接口來處理來源。只要Sentinel保護的接口資源被訪問,Sentinel就會調用RequestOriginParser
的實現類去解析訪問來源。
寫代碼:首先,服務消費者須要具有有一個來源標識,這裏假定爲服務消費者在調用接口的時候都會傳遞一個origin的header參數標識來源。具體以下示例:
package com.zj.node.contentcenter.sentinel; import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; import com.alibaba.nacos.client.utils.StringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * 實現區分來源 * * @author 01 * @date 2019-08-02 **/ @Slf4j @Component public class MyRequestOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest request) { // 從header中獲取名爲 origin 的參數並返回 String origin = request.getHeader("origin"); if (StringUtils.isBlank(origin)) { // 若是獲取不到,則拋異常 String err = "origin param must not be blank!"; log.error("parse origin failed: {}", err); throw new IllegalArgumentException(err); } return origin; } }
編寫完以上代碼並重啓項目後,此時header中不包含origin參數就會報錯了:
瞭解過RESTful URL的都知道這類URL路徑能夠動態變化,而Sentinel默認是沒法識別這種變化的,因此每一個路徑都會被當成一個資源,以下圖:
這顯然是有問題的,好在Sentinel提供了UrlCleaner接口解決這個問題。實現該接口可讓咱們對來源url進行編輯並返回,這樣就能夠將RESTful URL裏動態的路徑轉換爲佔位符之類的字符串。具體實現代碼以下:
package com.zj.node.contentcenter.sentinel; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; import org.springframework.stereotype.Component; import java.util.Arrays; /** * RESTful URL支持 * * @author 01 * @date 2019-08-02 **/ @Slf4j @Component public class MyUrlCleaner implements UrlCleaner { @Override public String clean(String originUrl) { String[] split = originUrl.split("/"); // 將數字轉換爲特定的佔位標識符 return Arrays.stream(split) .map(s -> NumberUtils.isNumber(s) ? "{number}" : s) .reduce((a, b) -> a + "/" + b) .orElse(""); } }
此時該RESTful接口就不會像以前那樣一個數字就註冊一個資源了: