Sentinel高可用流量控制组件-SpringMVC使用Sentinel实现流量监控

本文基于Sentinel1.8.3、SpringBoot2.6.6分析:
SpringMVC整合Sentinel实现流量监控,并讲解SpringMVC全局异常处理和HandlerInterceptor共同使用时的注意点及原因,如何在HandlerInterceptor#afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)方法中准确的统计exceptionQps

Sentinel流量日志格式

Sentinel的日志默认在用户的home目录的/logs/csp/logs/eagleeye两个目录。
可自定义,参见另一篇: Sentinel高可用流量控制组件-目录设置

metrics.log

xxx--metrics.log.yyyy-MM-dd 日志用于记录所拦截资源的相关qps数据。该日志在有请求的情况下,每隔1s记录一次,示例如下:

1648909760000|2022-04-02 22:29:20| http-url/content | 1 | 0 | 1 | 1 | 93 | 0 | 0 | 0
1648909767000|2022-04-02 22:29:27| http-url/content | 3 | 0 | 3 | 3 | 3  | 0 | 0 | 0

日志格式如下:

timestamp|dataTime|resourceName|passQps|blockQps|successQps|exceptionQps|rt|occupiedPassQps|concurrency|classification

源码见: com.alibaba.csp.sentinel.node.metric.MetricNode#toFatString

  • passQps:表示到来的数量,即此刻通过 Sentinel 规则 check 的数量
  • blockQps:实际该资源被拦截的数量(blocked QPS)
  • successQps:每秒结束的资源个数(完成调用),包括正常结束和异常结束的情况。这个字段实际含义是 complete count,包括 success 和 error 的;类中名称由于兼容性原因需要保持旧的名称
  • exceptionQps:异常的数量
  • rt:资源的平均响应时间(RT)
  • occupiedPassQps: 该秒占用未来请求的数目(since 1.5.0)
  • concurrency: 最大并发数(预留用)
  • classification: 资源分类(since 1.7.0),通用,web,rpc等等,见源码: com.alibaba.csp.sentinel.ResourceTypeConstants

sentinel-block.log

该日志在有请求被限流熔断的情况下,每隔1s记录一次,示例如下:

2022-04-03 02:15:06|1|http-url/content,FlowException,default,|61,0

日志格式说明:
1 该秒发生的第一个资源,记录该似乎没啥意义。
XXXException:拦截的原因, 通常 FlowException 代表是被限流规则拦截,DegradeException 则表示被降级,SystemBlockException 则表示被系统保护拦截
default 生效规则的调用来源(参数限流中代表生效的参数)
origin 被拦截资源的调用者,默认为空
61 61 被拦截的数量,
无意义可忽略

SpringMVC HandlerInterceptor接口介绍

SpringMVC中有个org.springframework.web.servlet.HandlerInterceptor接口,用于对http请求或Controller中方法执行前后的拦截处理。该接口有如下三个方法:

public interface HandlerInterceptor {
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}

preHandle:在执行Controller中的请求方法之前被执行,返回true表示继续调用Controller中的方法;false则表示返回不再继续处理,直接返回200状态码。
postHandle:在成功执行Controller中的请求方法之后执行,但生成视图之前调用此方法,如果出现了异常怎么不会调用该方法。
afterCompletion:在视图渲染后执行,无论执行请求方法是否有异常都会执行,如果有异常则参数Exception会传入具体异常,如果没有异常Exception则为null。

通过分析,我们只要在preHandle中调用SentinelSphU#entry(String name)开始统计,在afterCompletion中调用Entry#exit()结束统计即可,出现出现异常时还要调用Tracer#traceEntry统计异常数。

全局异常处理和HandlerInterceptor同时使用注意点

使用了SpringMVC的全局异常处理后,会导致HandlerInterceptor#afterCompletion方法不再传入Exception,这样会导致Sentinel统计的exceptionQps不正确。
下面先复现一下该问题,并分析具体原因。

  1. ContentController中模拟异常
@RestController
public class ContentController {

    @RequestMapping("/content")
    public String content(String id) {
        int a = 10 / 0;
        return "success";
    }
}
  1. 自定义拦截器QpsHandlerInterceptor实现org.springframework.web.servlet.HandlerInterceptor接口
public class QpsHandlerInterceptor implements HandlerInterceptor {
    public static final Logger logger = LoggerFactory.getLogger(QpsHandlerInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.info("preHandle" + request.getRequestURI());
        return true;
    }

    //可以使用这个方法来获取视图渲染后的请求和响应数据
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("afterCompletion request url is " + request.getRequestURI());
        if (ex != null) {
            logger.error("", ex);
        } else {
            logger.info("ex is null", ex);
        }
    }
}
  1. QpsHandlerInterceptor添加到Spring中使其生效。
@Order(1)
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new QpsHandlerInterceptor());
    }
}

请求Url后发现,afterCompletion方法按照前面的分析,拦截到了抛出的异常
在这里插入图片描述
4. 加入全局异常处理

/**
 * Created by bruce on 2022/3/26 14:47
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public Object exception(Exception ex) throws Exception {
        logger.error(ex.getMessage());
        return "server error!";
    }
}

从结果看到加入全局异常后,优先走的全局异常处理,然后执行afterCompletion方法,而ex是null。
在这里插入图片描述

原因分析:

没有全局异常处理时是走到1087行的逻辑,这里给afterCompletion方法传入了Exception
在这里插入图片描述
加入全局异常处理后,DispatcherServlet#processDispatchResult中会调用全局异常处理,然后执行到方法最后,给afterCompletion方法传入的Exception参数是null。
在这里插入图片描述

如何在全局异常处理后也能在#afterCompletion方法中拿个这个异常呢?

方案 1
我们在可以GlobalExceptionHandler的处理方法中将Exception保存在HttpServletRequest中。然后在HandlerInterceptor#afterCompletion方法中从request中获取。

方案 2
在SpringBoot2.0后,默认添加了org.springframework.boot.web.servlet.error.DefaultErrorAttributes用于在请求出现异常时将异常记录在HttpServletRequest中。
key为:org.springframework.boot.web.servlet.error.DefaultErrorAttributes.ERROR,异常时,我们通过该key,也能拿到具体异常。
在这里插入图片描述
实现原理也比较简单,DefaultErrorAttributes实现了org.springframework.web.servlet.HandlerExceptionResolver,请求出现异常时,SpringMVC会在DispatcherServlet#processHandlerException方法中回调HandlerExceptionResolver的实现类方法。

多个HandlerExceptionResolver支持排序,源码见:DispatcherServlet#initHandlerExceptionResolvers方法
在这里插入图片描述
在这里插入图片描述
方案 3
如果是低于SpringBoot2.0版本或仅仅是SpringMVC工程,可以参考org.springframework.boot.web.servlet.error.DefaultErrorAttributes实现。

SpringMVC使用Sentinel实现流量监控具体实现

public class QpsHandlerInterceptor implements HandlerInterceptor {
    public static final Logger logger = LoggerFactory.getLogger(QpsHandlerInterceptor.class);

    public QpsHandlerInterceptor() {
       // 可以在此加载自己的限流熔断规则
       // initFlowRules(); 
    }

    private static final String SENTINEL_ENTRY_KEY = QpsHandlerInterceptor.class.getName() + ".sentinel_entry";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.info("preHandle" + request.getRequestURI() + " " + handler);
        Entry sentinelEntry = null;
        try {
            sentinelEntry = SphU.entry("handlerChain.getHandler().toString()");
            request.setAttribute(SENTINEL_ENTRY_KEY, sentinelEntry);
        } catch (Exception ex) {
            if (ex instanceof BlockException blockEx) {
                SentinelUtil.setExceptionMsg(blockEx);
                // 处理被流控的逻辑
                response.addHeader("error", "BlockException:" + blockEx.getRule());
                response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value());
                logger.warn("blocked!" + ex);
            }
            if (sentinelEntry != null) {
                sentinelEntry.exit();
            }
            return false;
        }
        return true;
    }

    //可以使用这个方法来获取视图渲染后的请求和响应数据
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("afterCompletion" + request.getRequestURI() + " " + handler);

        Entry sentinelEntry = (Entry) request.getAttribute(SENTINEL_ENTRY_KEY);
        if (sentinelEntry != null) {
            Exception traceEx = ex != null ? ex : GlobalExceptionHandler.getException(request);
            if (traceEx != null) {
                //统计非BlockException异常数
                Tracer.traceEntry(traceEx, sentinelEntry);
            }
            sentinelEntry.exit();
        }

    }
}
public static Exception getException(HttpServletRequest req) {
    Exception exceptionRecordedBySb = (Exception) req.getAttribute(EXCEPTION_RECORDED_BY_SpringBoot);
    if (exceptionRecordedBySb != null) {
        return exceptionRecordedBySb;
    }
    return (Exception) req.getAttribute(HAS_HANDLED_EXCEPTION);
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值