Sentinel导航
简介
最近都在弄微服务的东西,现在来记录下收获。我从一知半解到现在能从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的小知识点开始讲,把我在学习了解,使用碰到的知识点分享给大家,希望能帮到大家~