簡介
最近都在弄微服務的東西,現在來記錄下收穫。我從一知半解到現在能從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引數配置
可以看到一些引數說明,具體引數值如果還不明白的,看該規則的類,裡面會有預設引數及註釋,一般都是使用RuleConst裡的值,而裡面的值代表什麼意思可以看具體的Rule類
2.2Sentinel和SpringCloud結合
有一點比較重要,就是Sentinel和SpringCloud結合開始的地方在哪,在AbstractSentinelInterceptor開始
public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
...
}
所以Sentinel可以把控程式的入口和出口,而掌握不了裡面業務的處理(這點單這樣聽好像沒卵用,但在實際功能開發和BUG查詢中還是挺有用的,瞭解來龍去脈)
2.2.1Sentinel熔斷失效,不起作用
比如:Sentinel設定了熔斷,但是@FeignClient設定了Fallback方法對異常進行了處理,那麼熔斷是不生效的,以Sentinel的視角來看,沒丟擲異常就是正常執行。
還有專案中往往@ExceptionHandle進行全域性異常處理,這要也會導致熔斷失效,下面簡單的分析分析
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 }
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熱點引數的限流原理
2.3Sentinel統一降級處理
參考:https://blog.csdn.net/theOldCaptain/article/details/107756801
需要注意
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 }
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 }
2.4Sentinel與閘道器結合
參考:https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway
小結
本篇部落格圍繞著Sentinel的小知識點開始講,把我在學習瞭解,使用碰到的知識點分享給大家,希望能幫到大家~