第一次在生产上遇到死锁,好方!

1. 背景

之前做风控系统,为了灵活的配置各种风险规则,引入了Drools规则引擎。上线以来,一直很平衡地在运行,然而,最近按业务要求将规则包拆分成多个项目后,每隔一段时间就会出现规则容器无法正常启动的情况。新创建的规则容器一直处于CREATING状态(正常是STARTED),之前让运维的小伙伴重启下kieserver就能恢复正常,所以一直没有怎么在意。然而就在昨天晚上,突然收到运维反馈,重启也无法解决了。

2. 分析

那么问题来了,现在已启动的容器可以正常的对外提供服务,新的规则容器却无法正常启动。首先怀疑是系统资源不足了,但看过系统资源以及 JVM 的 GC 状况后,并没有发现任何问题。资源没有问题,那肯定是加载规则包的线程执行出现问题了,估计是死锁啥的,赶紧使用 jstack 命令看了下线程状态,然而就看到了如下内容。
Stack Dead Lock
天呀,真的死锁了,难怪卡住不动了呢!
好吧,问题现在是发现了,但想要解决估计还得花点时间,庞大的项目体系让我有点方。

2.1. Worker-2 线程

第一次在生产上遇到死锁,好方!_第1张图片
该线程的执行流程主要在 DefaultInternalTypesClassLoaderloadClass() 方法里面,先在 loanType() 方法里面请求并持有了锁对象 DefaultInternalTypesClassLoader,但因为加载的类不存在出现异常,然后将加载任务委托给的 ProjectClassLoader 对象的 internalLoadClass() 方法去加载,而 internalLoadClass() 的执行过程中,又请求了锁对象 ProjectClassLoader
第一次在生产上遇到死锁,好方!_第2张图片
第一次在生产上遇到死锁,好方!_第3张图片
代码看着可能会有点晕,将该线程的执行流整理如下:
第一次在生产上遇到死锁,好方!_第4张图片

2.2. Worker-0 线程

第一次在生产上遇到死锁,好方!_第5张图片
该线程的执行流程主要在 ProjectClassLoader.loadClass() 里面,先在 internalLoadClass() 请求并持有了锁对象 ProjectClassLoader,与Worker-2一样,未找到要加载的类而抛出异常,将加载任务委托给内部认定的加载器,由于使用的环境非Android,所以委托给了DefaultInternalTypesClassLoader,而 DefaultInternalTypesClassLoaderloadType() 方法又请求了锁对象 DefaultInternalTypesClassLoader
第一次在生产上遇到死锁,好方!_第6张图片
第一次在生产上遇到死锁,好方!_第7张图片
typesClassLoader 由 makeClassLoader() 方法初始化
第一次在生产上遇到死锁,好方!_第8张图片
在这里插入图片描述
同上,整理了一个执行时序图,可以看出,两个线程获取锁的顺序刚好相反。
第一次在生产上遇到死锁,好方!_第9张图片

2.3. 综合分析

而两个线程都是在 ClassLoaderloadClass() 方法里面请求锁,该方法的实现如下:
第一次在生产上遇到死锁,好方!_第10张图片
首先通过 getClassLoadingLock() 方法获取锁对象,获取锁之后,判断类是否已经加载过,如果未加载过,先由父级类加载器尝试加载,若加载失败,则由当前类加载器自行加载,即类加载器的双亲委派模型。

我们遇到的问题,主要是因为 getClassLoadingLock() 返回的锁对象为 ClassLoader 本身,而造成了并行加载的循环依赖。

对于该方法的作用,方法注释表述如下:

返回类加载操作的锁对象。 为了向后兼容,此方法的默认实现如下。
如果此ClassLoader对象注册为并行功能,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此ClassLoader对象本身。

Returns the lock object for class loading operations. For backward
compatibility, the default implementation of this method behaves as
follows. If this ClassLoader object is registered as parallel capable,
the method returns a dedicated object associated with the specified
class name. Otherwise, the method returns this ClassLoader object.

第一次在生产上遇到死锁,好方!_第11张图片
由实现可以看出,如果每次返回的锁对象不为 ClassLoader 自身,则 ProjectClassLoaderDefaultInternalTypesClassLoader 的加载流程将互不干扰。这样就好说了,我们可以将这两个类加载器注册为支持并行的,将类加载器中锁粒度由 ClassLoader 级别降为 ClassLoader + className 级别,粒度更小。

3. 解决方案

直接上代码,在这两个自定义的 ClassLoader 里面添加如下代码,完成并行能力注册即可。

static {
    registerAsParallelCapable();
}

感想: 如果要自定义 ClassLoader, 尽量按[双亲委派模型]规范来实现,如果硬要打破该公约,必须认真考虑并行加载的问题。

附:该问题在7.7.0.Final版本发现,7.8.0.Final版本已经修复。

你可能感兴趣的:(drools)