记录一次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;
    }
}
相关推荐
像风一样自由20203 分钟前
Go语言详细指南:特点、应用场景与开发工具
开发语言·后端·golang
IT_陈寒32 分钟前
《Java 21新特性实战:5个必学的性能优化技巧让你的应用快30%》
前端·人工智能·后端
choice of34 分钟前
SpringMVC通过注解实现全局异常处理
java·后端·spring
单线程bug34 分钟前
Spring Boot中Filter与Interceptor的区别
java·spring boot·后端
小蒜学长40 分钟前
基于uni-app的蛋糕订购小程序的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·小程序·uni-app
程序员爱钓鱼44 分钟前
Go语言实战案例 — 工具开发篇:Go 实现条形码识别器
后端·google·go
二掌柜,酒来!7 小时前
完美解决:应用版本更新,增加字段导致 Redis 旧数据反序列化报错
redis·spring·bootstrap
期待のcode8 小时前
Spring框架1—Spring的IOC核心技术1
java·后端·spring·架构
Livingbody10 小时前
10分钟完成 ERNIE-4.5-21B-A3B-Thinking深度思考模型部署
后端
胡萝卜的兔11 小时前
go 日志的分装和使用 Zap + lumberjack
开发语言·后端·golang