本文基于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 被拦截的数量,
0
无意义可忽略
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
中调用Sentinel
的SphU#entry(String name)
开始统计,在afterCompletion
中调用Entry#exit()
结束统计即可,出现出现异常时还要调用Tracer#traceEntry
统计异常数。
全局异常处理和HandlerInterceptor同时使用注意点
使用了SpringMVC的全局异常处理后,会导致HandlerInterceptor#afterCompletion方法不再传入Exception
,这样会导致Sentinel统计的exceptionQps不正确。
下面先复现一下该问题,并分析具体原因。
ContentController
中模拟异常
@RestController
public class ContentController {
@RequestMapping("/content")
public String content(String id) {
int a = 10 / 0;
return "success";
}
}
- 自定义拦截器
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);
}
}
}
- 将
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);
}