问题现象:
由于业务需要对一个老项目(war)进行tomcat7升级tomcat 8, dev/test/reg/stage 升级正常, 业务验证也是OK。生产进行灰度发布后,提示以下错误,找不到业务方法(隐藏业务相关信息)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.NoSuchMethodError: com.xxx.yyy.enums.DifficultyEnum.valueOf(Lcom/xxx/yyy/enums/DifficultyValue;)Lcom/xxxx/yyy/enums/DifficultyEnum;
at com.xxx.yyy.module.question.service.ExamQuestionBiz.getCommonAnalysisData(ExamQuestionBiz.java:960)
分析过程:
1,代码验证
先对业务进行回滚,然后联系开发进行排查。将stage和生产包进行md5值进行比较,确定代码是同一份,开发对包进行反编译,在代码中可以找到对应的方法。
2,环境比较
除生产环境以外,其他环境都是docker容器部署并且运行正常,jdk大版本一致。本打算生产直接迁移docker,开发担心迁移docker有性能影响,希望找到原因。
3, arthas 排查
利用arthas 确认进程是否正确加载,通过命令查看该方法信息
sc -d com.xxx.yyy.enums.DifficultyEnum
- stage环境 结果
class-info com.xxx.yyy.enums.DifficultyEnum
code-source /data/tomcat/webapps/ROOT/WEB-INF/lib/exam-biz-2.1.111.jar
- 生产环境(隔离机器)结果
class-info com.xxx.yyy.enums.DifficultyEnum
code-source /app/tomcat/webapps/ROOT/WEB-INF/lib/exam-service-api-2.1.111.jar
比较结果后发现,两个环境提供同一个方法不是同一个jar文件,开发搜索代码后确认这2个jar文件确实都提供了同样的方法,但是exam-service-api这个包里方法没有业务代码,exam-biz 是正常业务代码。
原因:
网搜索了下资料,其他人遇到过类似问题。具体原因如下:
1,tomcat7会搜索项目 lib/目录下的jar文件,并按照名字排序 ,然后加载。排序后exam-biz 在exam-service-api前面,所以正常。
2,tomcat8会搜索项目 lib/目录下的jar文件,直接进行加载,取消了排序。文件系统返回的结果 exam-service-api 在exam-biz前面,所以业务提示找不到方法,发送异常错误。
解决办法:
- 临时解决办法
1,修改tomcat context.xml,指定优先加载包
<Resources>
<PreResources base="/data/tomcat/webapps/ROOT/WEB-INF/lib/exam-biz-2.1.111.jar"
className="org.apache.catalina.webresources.JarResourceSet"
webAppMount="/WEB-INF/classes"/>
</Resources>
- 长期方案
1,规范开发流程,一个项目里不同jar包不能提供相同方法.
扩展讨论:
查阅相关资料的同时,发现这种情况是由于tomcat 使用 file.list() 方法获取项目目录下jar文件时,系统是随机返回结果,并且该方法是调用的系统libc函数readdir_r() 获取的。该函数不同系统和不同的文件系统都会有不同差异。 我们生产环境非容器化部署使用的centos 7.6 xfs格式,docker环境使用centos 7.7 overlay2;
readddir_r() 调用验证
使用 文心一言 提供C代码片段进行验证
- centos7.6 非docker环境 exam-biz 在 exam-service-api后面,业务异常
- centos 7.7 docker环境 exam-biz 在 exam-service-api前面,业务正常
- c代码片段
gcc -o list_dir list_dir.c
./list_dir tomcat/webapps/ROOT/WEB-INF/lib
list_dir.c
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
// 检查是否提供了目录路径作为参数
if (argc != 2) {
fprintf(stderr, "Usage: %s <directory_path>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 打开指定目录
DIR *dir = opendir(argv[1]);
if (dir == NULL) {
perror("opendir");
exit(EXIT_FAILURE);
}
// 准备读取目录项
struct dirent entry;
struct dirent *result;
int readdir_result;
// 循环读取目录项,直到读取完毕或发生错误
do {
// 重置 result 指针(readdir_r 需要)
result = NULL;
// 调用 readdir_r 读取下一个目录项
readdir_result = readdir_r(dir, &entry, &result);
// 检查 readdir_r 的返回值
if (readdir_result != 0) {
perror("readdir_r");
closedir(dir);
exit(EXIT_FAILURE);
}
// 如果 result 非空,表示成功读取了一个目录项
if (result != NULL) {
printf("Found file: %s\n", result->d_name);
}
} while (result != NULL); // 继续读取直到 result 为 NULL
// 关闭目录流
closedir(dir);
return 0;
}
参考文档:
https://www.cnblogs.com/zjdxr-up/p/17139374.html
https://inhann.top/2022/06/28/tomcat_random_lib/