记录一次Spring5中事件通知机制bug引起的生产事故

服务器在4月18号当天上线了一个实物拉新的裂变分享活动,当日的上午,服务器出现非常不稳定的情况,导致H5页面无法正常打开,APP请求无法被响应,时长接近2小时;

  1. 从9点20起,服务器QPS激增至1500, 10点20达到2500;此时服务器就慢慢失去响应了;
  2. 从10点20开始,服务器开始出现大量任务堆积,从指标监控平台上观察发现,短短18分钟,任务堆积量达到5000W
  3. 堆内存迅速飙升至30G,达到最大内存,GC开始频繁进入STW,导致HTTP服务器失去响应。

故障排查

  1. 最初怀疑是有人刷接口(实际上也存在),经过一番盘查之后,决定先调整限流直接。之后线上逐渐稳定,因此推测是因为刷接口导致了服务器无法大量的请求(其实也不合理,因为在压测阶段做过大量的压测,当天的QPS量级应该是能够轻松应对的)
  2. 当天组内人员发现某一个线程池的任务队列指标异常,该线程池是被事件通知系统用来实现异步通知的。发现线程池的任务队列中堆积的任务量巨大;此时仍然不清楚这些任务是从哪里来的,我们选择先优化线程池参数,增加核心线程数、减少队列容量,增加最大线程数。希望这样能够提高线程池的任务消耗速度。
  3. 第二天,另外一个业务场景中大量查询MongoDB的时候发现服务器再次不稳定,并且发现大量的MongoDB事件抛出。至此,几乎可以判定事件派发有问题。

故障详解

通过查看源码,我们发现Spring在广播事件的时候会给每个事件生成一个ResolvableType,主要是用来解析事件属性,包括类、字段、泛型、参数等等;由于MongoDB抛出的是一个泛型事件,而泛型在运行时会被擦除,导致在MongoDB泛型事件的ResolvableType解析不了泛型,因此就把这个事件派发给了所有的监听器。瞬间泛型事件的派发瞬间增大了N倍,导致监听者队列中产生大量的无效任务,造成大量任务堆积。而这些空任务执行与否由监听者自行决定,监听者遍历这些任务发现绝大多数都不需要执行就直接丢弃,因此CPU几乎处于空转状态,使用率迅速飙升,服务几乎瘫痪。

解决方案

  1. 如果泛型事件无法控制:魔改监听方法适配器ApplicationListenerMethodAdapter,只有发现监听器方法参数类型是当前事件类型的时候才认为当前监听器支持该事件
java 复制代码
private static class MyApplicationListenerMethodAdapter extends ApplicationListenerMethodAdapter {
    private final boolean isMyClass;
    final Class<?> methodParamClazz;

    /**
     * Construct a new ApplicationListenerMethodAdapter.
     *
     * @param beanName    the name of the bean to invoke the listener method on
     * @param targetClass the target class that the method is declared on
     * @param method      the listener method to invoke
     */
    public MyApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
        super(beanName, targetClass, method);

        isMyClass = targetClass.getPackageName().startsWith("xxx") || targetClass.getPackageName().startsWith("xxx");
        methodParamClazz = isMyClass ? ResolvableType.forMethodParameter(method, 0).getRawClass() : null;
    }

    @Override
    public boolean supportsEventType(ResolvableType eventType) {
        if (isMyClass) {
            return eventType.getRawClass() == methodParamClazz;
        }

        return super.supportsEventType(eventType);
    }
}
  1. 如果能够控制泛型事件:无法解析事件真正的类型的原因在于运行时泛型会被擦除,如果我们主动将泛型类型传递进去,那么解析器就能拿到事件的泛型类型了。问题就会得到解决。因此我们可以在构建事件的实例的时候直接将ResolvableType构建好,后续执行泛型检查的时候拿这个对象去检查即可。
java 复制代码
public class GenericEvent<T> extends ApplicationEvent {
    private final ResolvableType resolvableType;

    // 构建对象事件的时候就构建出解析对象并告知具体的泛型类型
    public GenericEvent(T source) {
        super(source);
        // 构造 ResolvableType,保留泛型信息
        this.resolvableType = ResolvableType.forClassWithGenerics(
            getClass(), ResolvableType.forInstance(source)
        );
    }
	// 获取resolveType的时候使用提前构建好的解析对象
    @Override
    public ResolvableType getResolvableType() {
        return this.resolvableType;
    }
}
相关推荐
凹凸曼说我是怪兽y4 分钟前
python后端之DRF框架(上篇)
开发语言·后端·python
Victor3564 分钟前
MySQL(173)MySQL中的存储过程和函数有什么区别?
后端
wenb1n6 分钟前
【docker】揭秘容器启动命令:四种方法助你轻松还原
后端
孟君的编程札记9 分钟前
别只知道 Redis,真正用好缓存你得懂这些
java·后端
用户9601022516211 分钟前
kubesphere的告别,从可用环境提取Kubesphere镜像
后端
种子q_q14 分钟前
组合索引、覆盖索引、聚集索引、非聚集索引的区别
后端·面试
码事漫谈15 分钟前
WaitForSingleObject 函数参数影响及信号处理分析
后端
ffutop15 分钟前
gRPC mTLS 问题调试指南
后端
讨厌吃蛋黄酥17 分钟前
利用Mock实现前后端联调的解决方案
前端·javascript·后端