服务挂了?
线上服务在疯狂的报错,你还在悠哉悠哉的打代码,等到用户开始反馈问题,这时候才去线上查日志,黄花菜都凉了。老板:“去财务结一下账吧”。
异常告警
对于很多基础设施比较完善的公司,都会有比较完善的日志采集、分析、告警等组件,包括服务健康检查、接口拨测等等。但是对于刚起步的产品,我们可能啥也没有,追求的就是一个快速上线,那怎么优雅快速的实现异常告警呢?
异常分级
在处理异常之前,首先我们需要先对异常做分级,哪些是业务上定义的可接受的异常,比如参数校验的异常、权限异常等等;哪些是非预期的异常,比如空指针、数据库异常、缓存异常等等。我们一般重点关注的是非预期的异常。
业务异常我们一般会定义自己的异常基类:
/**
* 异常基类,所有业务异常继承自此类
*/
@Getter
public class BaseException extends RuntimeException{
private final Integer code;
private final String message;
public BaseException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
}
AOP
AOP真是一个好东西,可以减少代码侵入性,重用逻辑减化开发工作量。这么好用的特性那我们肯定也要用上:
/**
* 异常告警Aspect,打印对应异常日志并推送告警
*/
@Aspect
@Component
@Slf4j
public class ExceptionAspect {
@Resource
private ExceptionNotice exceptionNotice;
@Value("${notice.bz.ex}")
private String bizEx;
@Value("${notice.ex.enable}")
private boolean enable;
@Around("execution(* com.demo.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
try {
return pjp.proceed();
} catch (Throwable e) {
//处理异常
handleException(pjp, e);
//处理完还要继续向上抛
throw e;
}
}
private void handleException(ProceedingJoinPoint pjp, Throwable e) {
//如果没开启告警,则直接返回
if (!enable) {
return;
}
try {
//如果是非业务异常,或者是配置中的业务异常,才进行打印和告警
boolean needToNotice = !(e instanceof BaseException) || bizEx.contains(((BaseException)e).getCode().toString())
if (needToNotice) {
//打印异常
Object[] args = pjp.getArgs();
log.error("异常参数: {}", ArrayUtil.toString(args));
log.error(e.getMessage(), e);
//异常告警通知,注意:这里需要异步发送消息!!
exceptionNotice.send(formatMsg(e));
}
}catch(Exception e) {
//告警处理不能影响正常流程,忽略异常,只打印
log.error("handleException处理异常", e);
}
}
/**
* 格式化异常信息
*/
private String formatMsg(Throwable e) {
String template = "【业务名称】接口异常啦,请马上处理:traceId: %s, message: %s, \n %s";
//全局traceId,用于后续定位问题
String traceId = ServerContext.getTraceId();
String ex = ExceptionUtil.getMessage(e);
String trace = ExceptionUtil.stacktraceToString(e);
return String.format(template, traceId, ex, trace);
}
}
逻辑很简单,我们在代理类中捕获对应方法中的所有异常,然后再根据异常分级和配置,来决定是否要打印告警信息并且通过邮件、短信或企微告警。里面比较重要的几个点:
- 异常处理逻辑不能影响原有流程,因此需要catch住异常处理逻辑中的所有异常。
- 告警通知是一个较为耗时的操作,需要使用线程池异步处理,并且为了异常处理的逻辑不影响我们正常的服务,一定要设置线程池的队列大小和拒绝策略,拒绝策略应该是直接丢弃。
- 在发送异常告警需要考虑收敛,否则在某些情况下,邮件或短信可能会爆炸(别问我怎么知道的)。而且邮件算还好,但是短信是要钱的!!!
告警收敛可以根据一定的规则,比如根据告警信息、特定参数或者异常类型作为唯一标识,在时间范围内只告警N次。
总结
以上就可以简单快速且优雅的实现一个异常告警功能啦,对于缺乏基础设施建设,且需要快速上线的项目来说,这样最少可以保证我们项目前期的异常监控,不会等用户、运营、产品、老板都发现服务挂了,作为一个一线开发,你还在那笑嘻嘻的打代码,完全没有意识到,风雨欲来~