簡介

最近都在弄微服務的東西,現在來記錄下收穫。我從一知半解到現在能從0搭建使用最大的感觸有兩點

1.微服務各大元件的版本很多,網上很多部落格內容不一定適合你的版本,很多時候苦苦琢磨都是無用功

2.網上部落格參差不齊,有些甚至錯誤的。更離譜的是,好的文章閱讀量除非高出天際,不然就都很低,比那些複製貼上,隨便應付的都低(這個搜尋推薦演算法不知道基於什麼的)

通過這段時間學習,我覺得最重要是從好的部落格入手,先不要著急怎麼元件怎麼使用,而是先了解元件的作用,大概的原理,然後才是使用,這樣搭建和嘗試的過程中才能更好的定位問題,最後再次回到原理和一些實際問題的處理(不知道實際問題怎樣的,直接搜那個元件的面試題往往效果最好)

接下來的內容,都以導航的形式展現給大家(畢竟優秀的輪子很多,直接看大佬寫的不香嘛),再順帶提些自己的理解

傳送門

更多微服務的介紹可點選下方連結

微服務介紹Nginx導航Nacos導航Gateway導航Ribbon導航Feign導航Sentinel導航

博主微服務git練手專案:https://github.com/NiceJason/SpringCloudDemo

Sentinel簡介

1.限流相關

1.1限流點

在聊Sentinel之前,先簡單梳理下微服務限流的幾個地方:

1.Nginx限流,系統的最外層限流地點

2.Gateway閘道器限流,Gateway可以用內建的限流Filter(RequestRateLimiterGatewayFilterFactory,依賴於redis),或者其他外掛的限流Filter(如Redis的Redis RateLimiter),或者自定義Filter(自己實現限流演算法),或者結合Sentinel或者Hystrix來限流。

參考:https://blog.csdn.net/qq_38380025/article/details/102968559(部落格的第八點 請求限流)

3.微服務內部的限流,結合Sentinel或者Hystrix

4.訊息佇列的限流,通過控制生產者和消費者的速度

5.資料庫連線池限流,一定時間內只能有一定數量的連線

當需要限流的時候可以從這5個點去思考

1.2限流演算法

參考:https://blog.csdn.net/qq_38380025/article/details/102968559(部落格的第八點 請求限流)

文章簡述了常用的限流演算法,如令牌桶演算法、漏桶演算法,可以大致瞭解一下,以後萬一有場景需要手動實現的時候就能有個思路

2.Sentinel相關

Sentinel十分友好的中文安裝使用文件:https://github.com/alibaba/Sentinel/wiki/%E6%96%B0%E6%89%8B%E6%8C%87%E5%8D%97

裡面介紹了基礎的安裝與使用(十分簡單易上手),因為同是阿里系的元件,所以和Nacos結合的特別好,熔斷限流等配置直接在Nacos上寫即可,具體使用看文件(重點了解“資源”這個概念),這裡講下文件沒有或者被忽視的地方

2.1Json引數配置

參考(規則種類):https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8#%E8%A7%84%E5%88%99%E7%9A%84%E7%A7%8D%E7%B1%BB

可以看到一些引數說明,具體引數值如果還不明白的,看該規則的類,裡面會有預設引數及註釋,一般都是使用RuleConst裡的值,而裡面的值代表什麼意思可以看具體的Rule類

 
如:限流規則 FlowRule

2.2Sentinel和SpringCloud結合

有一點比較重要,就是Sentinel和SpringCloud結合開始的地方在哪,在AbstractSentinelInterceptor開始

public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
...
}

所以Sentinel可以把控程式的入口和出口,而掌握不了裡面業務的處理(這點單這樣聽好像沒卵用,但在實際功能開發和BUG查詢中還是挺有用的,瞭解來龍去脈)

2.2.1Sentinel熔斷失效,不起作用

比如:Sentinel設定了熔斷,但是@FeignClient設定了Fallback方法對異常進行了處理,那麼熔斷是不生效的,以Sentinel的視角來看,沒丟擲異常就是正常執行。

還有專案中往往@ExceptionHandle進行全域性異常處理,這要也會導致熔斷失效,下面簡單的分析分析

Sentinel與SpringCloud結合,是從AbstractSentinelInterceptor開始的

 1 public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
2 @Override
3 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
4 throws Exception {
5 try {
6 String resourceName = getResourceName(request);
7
8 if (StringUtil.isNotEmpty(resourceName)) {
9 // Parse the request origin using registered origin parser.
10 String origin = parseOrigin(request);
11 ContextUtil.enter(SENTINEL_SPRING_WEB_CONTEXT_NAME, origin);
12 Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
13
14 setEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName(), entry);
15 }
16 return true;
17 } catch (BlockException e) {
18 handleBlockException(request, response, e);
19 return false;
20 }
21 }
22
23 @Override
24 public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
25 Object handler, Exception ex) throws Exception {
26 Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName());
27 if (entry != null) {
28 //跟蹤記錄異常
29 traceExceptionAndExit(entry, ex);
30 removeEntryInRequest(request);
31 }
32 ContextUtil.exit();
33 }
34
35 protected void traceExceptionAndExit(Entry entry, Exception ex) {
36 if (entry != null) {
37 //要ex引數不為空才記錄,但這個引數是可能為空的
38 if (ex != null) {
39 Tracer.traceEntry(ex, entry);
40 }
41 entry.exit();
42 }
43 }
44 }
可以看出,它就是一個Spring的攔截器,preHandle方法就進行資源繫結(然後進行Sentinel的一連串的Slot判斷),通過後才能正常訪問資源
而afterCompletion方法則進行收尾工作,如資源執行時丟擲異常則記錄,為熔斷功能提供資料
失效的原因恰恰是這個afterCompletion,我們可以看到traceExceptionAndExit方法裡,需要ex不為空才記錄異常資訊,但這個引數是有可能會空的
這就要到DispatcherServlet的doDispatch裡面來,這裡面負責呼叫interceptor,具體看裡面的註釋
 1 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
2 ...
3
4 try {
5 ModelAndView mv = null;
6 Exception dispatchException = null;
7
8 try {
9 ...
10
11 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
12
13 ...
14
15 // 這裡會呼叫intercepter過濾鏈,呼叫其preHandle方法
16 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
17
18 ...
19 }
20 catch (Exception ex) {
21 //這裡捕獲了所有異常
22 dispatchException = ex;
23 }
24 catch (Throwable err) {
25 //這裡捕獲了所有的Throwable
26 dispatchException = new NestedServletException("Handler dispatch failed", err);
27 }
28 //這個方法很關鍵,它拋不拋異常,取決於後面的catch能不能執行!!
29 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
30 }
31 catch (Exception ex) {
32 //呼叫interceptor的afterCompletion,有沒熔斷記錄就看它有沒執行了
33 triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
34 }
35 catch (Throwable err) {
36 //呼叫interceptor的afterCompletion,有沒熔斷記錄就看它有沒執行了
37 triggerAfterCompletion(processedRequest, response, mappedHandler,
38 new NestedServletException("Handler processing failed", err));
39 }
40 finally {
41 ...
42 }
43 }

我們首先來看看非常關鍵的processDispatchResult方法,這個方法只有兩個地方丟擲異常,如果這兩個地方都沒丟擲異常,則doDispatch方法就不可能執行43或47行程式碼,意思是熔斷資訊將不會被記錄,從而導致熔斷失效

 1 private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
2 @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
3 @Nullable Exception exception) throws Exception {
4
5 boolean errorView = false;
6
7 if (exception != null) {
8 if (exception instanceof ModelAndViewDefiningException) {
9 logger.debug("ModelAndViewDefiningException encountered", exception);
10 mv = ((ModelAndViewDefiningException) exception).getModelAndView();
11 }
12 else {
13 Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
14 //這裡是執行異常處理器,如果用了@ExceptionHandler全域性異常處理,就會被處理
15 //這裡是能夠丟擲異常的第一個地方,但我們全域性異常處理的目的就是終止程式丟擲的異常
16 mv = processHandlerException(request, response, handler, exception);
17 errorView = (mv != null);
18 }
19 }
20
21 // Did the handler return a view to render?
22 if (mv != null && !mv.wasCleared()) {
23 //這裡是能夠丟擲異常的第二個地方,讀取檢視拋異常
24 //可是有大部分情況都是返回資料,並不需要查詢檢視,所以mv經常等於null
25 //並不會進這個if裡面來
26 render(mv, request, response);
27 if (errorView) {
28 WebUtils.clearErrorRequestAttributes(request);
29 }
30 }
31 else {
32 if (logger.isTraceEnabled()) {
33 logger.trace("No view rendering, null ModelAndView returned.");
34 }
35 }
36
37 if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
38 // Concurrent handling started during a forward
39 return;
40 }
41
42 //這裡注意第三個引數是null,即傳入AbstractSentinelInterceptor類裡的afterCompletion方法的ex引數為null
43 //這樣是做不了熔斷記錄的,所以上面的方法必須要丟擲異常
44 if (mappedHandler != null) {
45 mappedHandler.triggerAfterCompletion(request, response, null);
46 }
47 }

從上述流程可以看出,當我們使用@ExceptionHandler進行全域性異常處理的時候,Sentinel並不能成功記錄熔斷資訊,因為程式碼執行到它這異常已經被處理了,所以視為此次業務邏輯被正確執行

2.2熱點引數的限流原理

 參考:
主要是要知道,相同引數是根據equal方法來比較是不是同一個物件,所以Object要重寫此方法來符合自己的邏輯,不然的話熱點引數設定無效

2.3Sentinel統一降級處理

參考:https://blog.csdn.net/theOldCaptain/article/details/107756801

需要注意

統一降級處理的資源不能用@SentinelResources修飾,否則是不走這個路的,需要自己配置@SentinelResources裡的blockHandler和fallback這些方法
在Sentinel2.0之後,統一降級處理這樣寫(實踐過可行)
 1 import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
2 import com.alibaba.csp.sentinel.slots.block.BlockException;
3 import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
4 import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
5 import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
6 import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
7 import com.alibaba.fastjson.JSONObject;
8 import com.syni.common.result.ResultJson;
9 import lombok.extern.slf4j.Slf4j;
10 import org.springframework.stereotype.Component;
11
12 import javax.servlet.ServletOutputStream;
13 import javax.servlet.http.HttpServletRequest;
14 import javax.servlet.http.HttpServletResponse;
15 import java.nio.charset.StandardCharsets;
16
17 /**
18 * @Author DiaoJianBin
19 * @Description 別看idea標著@Component未被使用,實際上是被使用了的
20 * 去掉的話此類無法生效
21 * @Date 2021/4/22 10:44
22 */
23 @Component
24 @Slf4j
25 public class SystemBlockExceptionHandler implements BlockExceptionHandler {
26 @Override
27 public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException blockException) throws Exception {
28 StringBuilder errorMessage = new StringBuilder();
29 errorMessage.append("資源名稱=");
30 String resourceName = blockException.getRule().getResource();
31
32 if (blockException instanceof FlowException) {
33 errorMessage.append(" 被限流了");
34 }else if(blockException instanceof DegradeException){
35 errorMessage.append(" 被熔斷了");
36 }else if(blockException instanceof SystemBlockException){
37 SystemBlockException systemBlockException = (SystemBlockException)blockException;
38 errorMessage.append(" 觸發系統保護 limitType=").append(systemBlockException.getLimitType());
39 }else if(blockException instanceof ParamFlowException){
40 //具體異常有特殊的方法
41 ParamFlowException paramFlowException = (ParamFlowException)blockException;
42 errorMessage.append(" 觸發熱點引數限流 limitParam=").append(paramFlowException.getLimitParam());
43 }
44 errorMessage.insert(5,resourceName);
45 //日誌記錄具體原因
46 log.error(errorMessage.toString());
47
48 //這裡返回個前端,不能說明具體原因的
49 ResultJson resultJson = new ResultJson("102","系統繁忙,稍後再試");
50
51 httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
52 ServletOutputStream outputStream = httpServletResponse.getOutputStream();
53 try{
54 outputStream.write(JSONObject.toJSONString(resultJson).getBytes(StandardCharsets.UTF_8));
55 }finally {
56 outputStream.close();
57 }
58 }
59 }
sentinel降級方式的順序,原始碼貼出,程式碼通俗易懂.
(1).檢視是否有自定義了BlockExceptionHandler異常處理器.
(2).檢視是否配置了降級頁面.
(3).以上都沒有,則建立預設的BlockExceptionHandler異常處理器,也就是響應Blocked by Sentinel (flow limiting)的那個類.
SentinelWebAutoConfiguration.java
 1 @Bean
2 @ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true)
3 public SentinelWebMvcConfig sentinelWebMvcConfig() {
4 SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig();
5 sentinelWebMvcConfig.setHttpMethodSpecify(properties.getHttpMethodSpecify());
6
7 if (blockExceptionHandlerOptional.isPresent()) {
8 blockExceptionHandlerOptional
9 .ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler);
10 }
11 else {
12 if (StringUtils.hasText(properties.getBlockPage())) {
13 sentinelWebMvcConfig.setBlockExceptionHandler(((request, response,
14 e) -> response.sendRedirect(properties.getBlockPage())));
15 }
16 else {
17 sentinelWebMvcConfig
18 .setBlockExceptionHandler(new DefaultBlockExceptionHandler());
19 }
20 }
21
22 urlCleanerOptional.ifPresent(sentinelWebMvcConfig::setUrlCleaner);
23 requestOriginParserOptional.ifPresent(sentinelWebMvcConfig::setOriginParser);
24 return sentinelWebMvcConfig;
25 }
 
在Sentinel 2.0以前,據說是這樣處理(未實踐過)

2.4Sentinel與閘道器結合

參考:https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway

大體原理(以SpringCloudGateway為例)
由於SpringCloudGateway裡有一系列的Filter組成
Sentinel會構造一個Filter放入其中,這樣就能被Sentinel處理
同時,Sentinel將路由名稱或者一組API定義成資源,用新的路由規則來控制這些資源(明面上是GatewayFlowRule實際就變成了ParamFlowRule),這樣就回到了Sentinel本身的程式碼邏輯,從閘道器邏輯裡分離出來

小結

本篇部落格圍繞著Sentinel的小知識點開始講,把我在學習瞭解,使用碰到的知識點分享給大家,希望能幫到大家~