起因

前几日正常版本发布后,经过发布验证,一个不在本次发布功能范围中的文件导出功能不能正常执行,具体表现为,任务启动后一直卡在执行中状态,无法完成且不会自动异常中断,就这么一直卡在执行中的状态,同时后续更多导出任务仍然在排队持续积压。接下来就是在众多业务部门狂轰滥炸的问询之下,顶住压力进行排查处理。

排查过程

首先初步排查确认故障范围,首先第一步想到的是去异常日志监管平台和统计平台查看有无明显的新增异常,翻了一圈无果,这就很奇怪了。接下来我只能去根据功能逻辑的特点去逐排查。同事去申请下载完整的近期应用日志,这个要走流程,很慢,就先不管他了。

交代下这个下载服务功能的基础逻辑,然后我们根据不同部分验证来排查确认。

该下载服务根据平台用户提交的下载请求创建导出任务,再有任务调度系统来定时执行启动导出任务执行。导出任务的执行是一个基本的生产者消费者模型,启动后分两部分,一部分为生产者线程,根据用户提交的查询参数去对应的ES索引或者MYSQL表查询数据。另外一方面启动消费者线程来消费这些数据,写入到一个或者多个csv文件中,消费完毕后将文件提交到OSS对象存储服务中,得到对应的文件下载地址。之后标记导出任务完成,并更新下载地址到导出任务信息中。那么就对这些步骤分别排查验证。

逐步排查第一步,任务状态能从排队中更新为执行中状态,且后续任务能保持在排队中状态,说明任务调度系统到当前执行导出任务的应用直接的通信应该是正常的。

第二步,任务启动后会首先查询待导出总数据量,经过测试,导出任务的该字段能正确更新,说明一方面具体的执行线程能正确查询到对应ES/MYSQL中的数据,另一方面也能反过来正确更新导出任务中的总数据量字段,说明这两个路径之间是正常。那么如果任务发生异常中断了的话,按照代码逻辑应当能正确进入Exception处理分支,去更新导出任务的状态为失败。到这里合理的怀疑是不是任务哪边进入了死循环不能中断跳出

第三步,去到最终的OSS服务器目录上查看下,确认没有对应的文件上传,到这里怀疑是不是上传OSS的时候网络问题导致文件上传非常慢从而卡在了这里,任务一直无法结束

第四步,回到我们的应用服务器,简单申请个权限看下对应的应用服务的临时文件夹,确认下文件是否正确生成了,从而确认第三步的怀疑是否正确。但是在应用服务器上的临时文件目录看了下后发现,对应的csv文件生成了,但是文件大小是0,是个空文件(看不到文件内容,涉及敏感信息管理,查看文件内容需要另外申请权限)。

最初是想看下是不是有什么异常数据导致写入文件失败的,但是一看只生成了空文件,那么可以确认应该是往文件写入的这部分代码的问题了。这样也就排除掉前面第三步的向OSS上传问题猜测。

第五步,回到相关业务代码中,追入相关引用jar包的源码,找到csv文件创建之后继续往csv文件追加写入数据的相关代码部分,进过调试确认确实是这行代码相关导致的问题,无法继续往csv文件写入

再经过调试确认此行执行抛出了一个NoClassDefFoundError,从他的名字就可以看出来这是一个Error,虽然他最终也是继承自Throwable,而我们代码中只会对Exception进行catch处理,所以我们的写线程执行到这一步的时候就抛出NoClassDefFoundError并中断了,且没有被捕获到异常。

而另外有个类似的Exception名为ClassNotFoundException,他是继承自Exception的

相关参考ClassNotFoundException和NoClassDefFoundError的区别。至此知道了具体是哪边的代码出了问题。我们接下来继续深入org.apache.commons.csv.CSVFormat类查看

第六步,继续点进CSVFormat类的源码,就直接的能看到很多报错内容

而顶部import的代码则直接报红,此处的原因是引用的commons-io.jar包中的几个类文件引发的错误

commons-csv-1.11.0.pom中引用了commons-io的包,版本是2.16.1,但是项目中实际关联到的commons-io.jar包的版本是2.2。

看到这里就可以直接下结论,是引用jar包的版本冲突的问题了。所以对应的我们只需要修改相应的pom文件引用的版本号应该即可解决。

阶段性结论

根据以上查出来的情况,我们可以得到一个阶段性的结论了,项目中引用的commons-csv-1.11.0.jar包中需要使用到某个版本的commons-io-??.??.jar包。但是项目里实际关联到的版本不对,导致在调用commons-csv包中的org.apache.commons.csv.CSVFormat类时相关引用调用失败而抛出了NoClassDefFoundError。至此可以得出结论是jar包版本冲突问题导致的。

但是我们这次发布并没有修改任何pom文件,之前执行得好好的,为什么这次发布就出问题了呢?

继续排查

使用Analyze Dependencies分析下哪边引用了commons-csv的jar包

一处是直接在项目的pom文件里引用的,去到pom文件里可以看到之前在这边项目里有两处引用的定义,且在同一个pom文件里

排在前面的引用
排在后面的引用

而另一个地方则是间接依赖的引用,由引用的mntcl-excel.jar包里引用了commons-csv的jar包,去到这个mntcl-excel.jar包的pom文件里可以看到这边定义了一个<version>RELEASE</version>,定义了version为RELEASE之后会去取最新发布版本。如果本地仓库没有缓存,会去远程仓库获取;如果本地仓库已缓存,即使远程仓库同一版本号有更新,也不再去远程仓库获取。mntcl-excel.jar是项目内封装一些常用业务/工具类的代码

相关知识背景

POM文件中的依赖可以区分为直接依赖:pom.xml中写的依赖、间接依赖:依赖所需的资源,或者说依赖的依赖。根据Maven中的依赖版本取值规则

声明优先:间接依赖,先声明的优先
特殊优先:直接依赖,后声明的优先
路径优先:出现相同资源,层级越深,优先级越低

举几个栗子,声明优先的规则,以下的依赖X应当取1.0版本

A → B → X(1.0)
A → C → X(1.2)

特殊优先,以下情况下,应当取依赖包A的1.5版本

A(1.0)
A(1.5)

路径优先,以下情况下应当取依赖包X的1.3版本

A → B → X(1.0)
D → X(1.3)
A → C → X(1.2)

根据上面所说的这些规则,理出项目中commons-csv和commons-io依赖包的引用顺序和依赖关系路径,commons-csv(RELEASE)的版本为1.11.0

xxxx-admin主包中

xxx-service子包
????.framework:?? → commons-io(无版本指定)
????.framework:?? → commons-io(2.2)
commons-io(无版本指定)

xxxx-service子包中

mntcl-excel → commons-csv(1.0.79.1) → commons-csv(RELEASE)  →  commons-io(2.16.1)
commons-csv(1.4) → commons-io(2.5)
commons-csv(RELEASE) → commons-io(2.16.1)

最终可以确认出来,commons-csv会取1.11.0版本的包,生效的是直接直接依赖,后声明的优先的那条,而那条依赖指定了version为RELEASE,所以就会取到最新的1.11.0版本。

去到公司的Maven仓库私服上可以看到确实有个1.11.0版本,是在当时我们的应用发布前几天的

而commons-io会去取2.2版本的包,最终生效的是在commons-io(无版本指定)中指定的这条。这条版本继承自父级项目pom中定义的版本2.2

就这样最终导致了项目中引用了commons-csv会取1.11.0版本但是却引用了commons-io2.2版本的包。也就导致了NoClassDefFoundError的抛出错误。

简单的解决办法

所以我们最简单的处理办法就是修改项目pom文件中的commons-csv(RELEASE)这条,因为这条是最终生效的commons-csv的版本,将其修改为所需的版本1.8即可。

commons-csv1.8的包可以配合commons-io2.2版本使用,所以问题解决。而在后面的开发中也要注意避免<version>RELEASE</version>的使用,外部jar包的切不可自动使用最新版本的设置。