服务挂了?
线上服务在疯狂的报错,你还在悠哉悠哉的打代码,等到用户开始反馈问题,这时候才去线上查日志,黄花菜都凉了。老板:“去财务结一下账吧”。
异常告警
对于很多基础设施比较完善的公司,都会有比较完善的日志采集、分析、告警等组件,包括服务健康检查、接口拨测等等。但是对于刚起步的产品,我们可能啥也没有,追求的就是一个快速上线,那怎么优雅快速的实现异常告警呢?
异常分级
在处理异常之前,首先我们需要先对异常做分级,哪些是业务上定义的可接受的异常,比如参数校验的异常、权限异常等等;哪些是非预期的异常,比如空指针、数据库异常、缓存异常等等。我们一般重点关注的是非预期的异常。
业务异常我们一般会定义自己的异常基类:
/*** 异常基类,所有业务异常继承自此类*/
@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 {@Resourceprivate 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次。
总结
以上就可以简单快速且优雅的实现一个异常告警功能啦,对于缺乏基础设施建设,且需要快速上线的项目来说,这样最少可以保证我们项目前期的异常监控,不会等用户、运营、产品、老板都发现服务挂了,作为一个一线开发,你还在那笑嘻嘻的打代码,完全没有意识到,风雨欲来~