开端
今天在部署Impala时遇到一个NoClassDefFoundError的错误,本以为只是CLASSPATH的小问题,没想到调查了一天......
事情是这样的,编译Impala生成RPM,部署到另一台机器上后,statestored能正常启动,catalogd启动报错:
I0411 13:38:14.642241 113789 init.cc:599] OS distribution: CentOS Linux 8 (Core)
OS version: Linux version 5.4.243-1.el7.elrepo.x86_64 (mockbuild@Build64R7) (gcc version 9.3.1 20200408 (Red Hat 9.3.1-2) (GCC)) #1 SMP Tue May 16 10:43:45 EDT 2023
Clock: clocksource: 'tsc', clockid_t: CLOCK_MONOTONIC
I0411 13:38:14.642261 113789 init.cc:600] Process CGroup Info: memory.limit_in_bytes=9223372036854771712
I0411 13:38:14.642583 113789 init.cc:601] Process ID: 113789
I0411 13:38:14.642594 113789 init.cc:602] Default AES cipher mode for spill-to-disk: AES-GCM
I0411 13:38:14.746702 113789 init.cc:440] Using Java weigher sizeof
I0411 13:38:15.480756 113789 webserver.cc:395] Starting webserver on 0.0.0.0:25020
I0411 13:38:15.480811 113789 webserver.cc:411] Document root: /opt/impala
I0411 13:38:15.481134 113789 webserver.cc:548] Webserver started
I0411 13:38:15.609732 113789 jni-util.cc:302] java.lang.NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
at org.apache.hadoop.hive.conf.valcoersion.JavaIOTmpdirVariableCoercion.<clinit>(JavaIOTmpdirVariableCoercion.java:37)
at org.apache.hadoop.hive.conf.SystemVariables.<clinit>(SystemVariables.java:37)
at org.apache.hadoop.hive.conf.HiveConf$ConfVars.<init>(HiveConf.java:5736)
at org.apache.hadoop.hive.conf.HiveConf$ConfVars.<init>(HiveConf.java:5717)
at org.apache.hadoop.hive.conf.HiveConf$ConfVars.<clinit>(HiveConf.java:536)
at org.apache.hadoop.hive.conf.HiveConf.<clinit>(HiveConf.java:256)
at org.apache.impala.service.JniCatalog.<clinit>(JniCatalog.java:135)
I0411 13:38:15.609787 113789 status.cc:129] NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
@ 0x10a02f4
@ 0x1c21104
@ 0x1036151
@ 0xfd433d
@ 0xf5a43e
@ 0xe5b774
@ 0x7f81d2bf66a3
@ 0xf05c2e
F0411 13:38:15.611681 113789 catalog.cc:86] NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
. Impalad exiting.
Minidump with no thread info available.
Wrote minidump to /var/log/impala-minidumps/catalogd/86355b1b-4d2c-4195-61ac0d9e-68059041.dmp
catalogd.ERROR也没有更多有用的信息:
Log file created at: 2024/04/11 13:38:14
Running on machine: ccycloud-3.quanlong.root.comops.site
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
E0411 13:38:14.627285 113789 logging.cc:256] stderr will be logged to this file.
log4j:WARN No appenders could be found for logger (org.apache.hadoop.conf.Configuration).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
F0411 13:38:15.611681 113789 catalog.cc:86] NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
. Impalad exiting.
*** Check failure stack trace: ***
@ 0x391054d
@ 0x3912484
@ 0x390ff2c
@ 0x39129a9
@ 0x103625b
@ 0xfd433d
@ 0xf5a43e
@ 0xe5b774
@ 0x7f81d2bf66a3
@ 0xf05c2e
Minidump with no thread info available.
Wrote minidump to /var/log/impala-minidumps/catalogd/86355b1b-4d2c-4195-61ac0d9e-68059041.dmp
报错是 "NoClassDefFoundError: org.apache.hadoop.fs.FileSystem", 报的是 NoClassDefFoundError 而不是常见的 ClassNotFoundException。一开始没在意,先排查CLASSPATH的问题。
由于之前遇到过 IMPALA-12979 的问题,CLASSPATH里没有用通配符 *,而是写明了所有目录和jar包的绝对路径,这个没问题。
FileSystem这个类是在 hadoop-common 包中提供的,在CLASSPATH中确认了有这个包。为了进一步确认它被使用了,在环境变量 JAVA_TOOL_OPTIONS 里加 -verbose:class
,再次启动 catalogd,能在日志里看到 ClassLoader 加载这个类的日志:
[Loaded org.apache.hadoop.fs.FileSystem from file:/opt/impala/lib/jars/hadoop-common-3.1.1.7.2.18.0-369.jar]
这让我陷入了沉思......
转机
常用的debug方法就是在好坏两个重现之间找差异。同一台机器上还有CM部署的Impala,之前的配置就是从CM部署的配置复制过去的,所以配置内容是一样的。我把CDP parcel里的impalad可执行文件和jar包全都替换成我先前部署的版本,发现CM竟然可以启动catalogd! 这说明可执行文件和jar包没问题,可能还是CLASSPATH的问题。
于是我用CM的CLASSPATH,在前面加上 /opt/impala/conf(即我手动部署的配置目录),启动手动部署的catalogd,发现还是报一样的错。再度陷入沉思......
于是开始各种查 NoClassDefFoundError 的报错机制,发现它和 ClassNotFoundException 还是有区别的。ClassNotFoundException 是这个类在CLASSPATH里真的找不到,ClassLoader报的错。NoClassDefFoundError 可能是这个类找到了,但在加载的时候初始化 static 成员时报错,导致这个类无法被使用。网上很多只说了CLASSPATH有问题,这个回答才比较全面:https://stackoverflow.com/a/2213496/5996453
看报错堆栈,确实是在初始化static成员出的错:
I0411 13:38:15.609732 113789 jni-util.cc:302] java.lang.NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
at org.apache.hadoop.hive.conf.valcoersion.JavaIOTmpdirVariableCoercion.<clinit>(JavaIOTmpdirVariableCoercion.java:37)
对应的 JavaIOTmpdirVariableCoercion.java 代码是:
java
34 public class JavaIOTmpdirVariableCoercion extends VariableCoercion {
35 private static final Logger LOG = LoggerFactory.getLogger(JavaIOTmpdirVariableCoercion.class);
36 private static final String NAME = "system:java.io.tmpdir";
37 private static final FileSystem LOCAL_FILE_SYSTEM = new LocalFileSystem();
至于是初始化 LOCAL_FILE_SYSTEM
时的 new LocalFileSystem()
失败了,还是初始化 FileSystem
的静态成员失败了,还不得而知。奇怪的是 catalogd 日志里并没有其它的报错,难道是 JNI 相关的代码把报错忽略了?
又过了一下FileSystem.java的代码,可能初始化失败的static成员倒是有几个:
java
public static final Logger LOG = LoggerFactory.getLogger(FileSystem.class);
private static final Logger LOGGER =
LoggerFactory.getLogger(FileSystem.class);
/** FileSystem cache. */
static final Cache CACHE = new Cache(new Configuration());
但是没有日志真不好往下查,于是我又开始玩弄好坏两个重现,尽可能让它们相近。机缘巧合,发现完全用CM的CLASSPATH就没问题,但哪怕是前面加上 /opt/impala/conf 都不行!所以问题可能还是在配置文件上。
真相浮出水面
逐个对比core-site.xml、hdfs-site.xml、hive-site.xml,只有细微的差别(中途我用CM加了些DataNode节点导致的)。把它们的内容都改成CM部署的一样的内容,发现还是报错。
又一次机缘巧合,想到可能是权限的问题,于是检查了一下,发现我启动catalogd用的用户名("impala"),确实没法读我部署的配置文件!
这是我当时用的复制指令:
# cp /var/run/cloudera-scm-agent/process/46-impala-CATALOGSERVER/hadoop-conf/* /opt/impala/conf
这是源文件的权限:
# ls -l /var/run/cloudera-scm-agent/process/46-impala-CATALOGSERVER/hadoop-conf/
total 16
-rw-r----- 1 impala impala 5331 Apr 11 01:49 core-site.xml
-rw-r----- 1 impala impala 2455 Apr 11 01:49 hdfs-site.xml
-rw-r----- 1 impala impala 315 Apr 11 01:49 ssl-client.xml
这是目标文件的权限:
# ls -l /opt/impala/conf/core-site.xml
-rw-r----- 1 root root 5331 Apr 12 03:02 /opt/impala/conf/core-site.xml
因为我是用root用户复制的,文件的owner变成了root,而文件的权限在 other 部分是 0,这就导致 "impala" 用户名无法读取这个文件!
把所有配置文件都加上 other 的读权限后,问题就解决了
# chmod o+r /opt/impala/conf/*
# ls -l /opt/impala/conf/core-site.xml
-rw-r--r-- 1 root root 5331 Apr 12 03:02 /opt/impala/conf/core-site.xml
没想到这个 NoClassDefFoundError 重误的原因真不是CLASSPATH有问题,而是匹配文件的问题。
归因
还有一些问题没弄清楚:
- 为什么没有抛读不了core-site.xml文件的报错?
- core-site.xml读不了是怎么导致NoClassDefFoundError的?具体是FileSystem还是其它哪个类的static成员初始化出问题了?
回到本地的 impala 开发环境,我重现了问题,不光是core-site.xml读不了时会出这个问题,如果core-site.xml里有语法错误(即不是一个合法的XML文件)也会出这个问题。
有意思的是 catalogd.ERROR 居然在 NoClassDefFoundError 前有具体的报错,比如core-site.xml没权限读时:
24/04/11 20:42:52 ERROR conf.Configuration: error parsing conf core-site.xml
java.io.FileNotFoundException: /home/quanlong/workspace/Impala/fe/src/test/resources/core-site.xml (Permission denied)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:90)
at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:188)
at org.apache.hadoop.conf.Configuration.parse(Configuration.java:3023)
at org.apache.hadoop.conf.Configuration.getStreamReader(Configuration.java:3119)
at org.apache.hadoop.conf.Configuration.loadResource(Configuration.java:3077)
at org.apache.hadoop.conf.Configuration.loadResources(Configuration.java:3050)
at org.apache.hadoop.conf.Configuration.loadProps(Configuration.java:2928)
at org.apache.hadoop.conf.Configuration.getProps(Configuration.java:2910)
at org.apache.hadoop.conf.Configuration.get(Configuration.java:1264)
at org.apache.hadoop.conf.Configuration.getTrimmed(Configuration.java:1318)
at org.apache.hadoop.conf.Configuration.getInt(Configuration.java:1543)
at org.apache.hadoop.fs.FileSystem$Cache.<init>(FileSystem.java:3495)
at org.apache.hadoop.fs.FileSystem.<clinit>(FileSystem.java:194)
24/04/11 20:42:52 INFO util.JvmPauseMonitor: Starting JVM pause monitor
24/04/11 20:42:52 INFO conf.HiveConf: Found configuration file file:/home/quanlong/workspace/Impala/fe/src/test/resources/hive-site.xml
F0411 20:42:52.422523 16481 catalog.cc:86] NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
. Impalad exiting.
*** Check failure stack trace: ***
@ 0x3982bbd google::LogMessage::Fail()
@ 0x3984af4 google::LogMessage::SendToLog()
@ 0x398259c google::LogMessage::Flush()
@ 0x3985019 google::LogMessageFatal::~LogMessageFatal()
@ 0x103c3bd impala::Catalog::Catalog()
@ 0xffa5b9 impala::CatalogServer::Start()
@ 0xf3d8d7 CatalogdMain()
@ 0xf3c25f main
@ 0x7f7f21435c87 __libc_start_main
@ 0xf3c09a _start
Minidump with no thread info available.
Wrote minidump to /home/quanlong/workspace/Impala/logs/cluster/minidumps/catalogd/f13bee4f-b6fc-4a6a-392f1782-32ed878f.dmp
从第一个报错堆栈能清晰地看到,初始化 FileSystem 的 static 成员 CACHE
时,需要创建一个 Configuration
对象,这一步因为读不了 core-site.xml 而出错了。从而也就产生了 "NoClassDefFoundError: org.apache.hadoop.fs.FileSystem" 的报错。这回答了第二个问题。
为什么第一个报错没在部署的远程机器上打印出来?
- 难道是RELEASE build和DEBUG build的区别?我本地是DEBUG build,所以默认有打印C++的堆栈。重新编译成 RELEASE build试了一下,发现日志也是有这个报错的。
- 难道是系统差异导致的?远程机器是Redhat 8.9,本地开始环境是Ubuntu 18.04。于是试了下在Redhat 8.9里搭建impala开发环境并重现问题,发现日志也是有第一个报错的。
后来突然意识到,第一个报错FileNotFoundException的日志格式和第二个报错NoClassDefFoundError的日志格式是不同的:
24/04/11 20:42:52 ERROR conf.Configuration: error parsing conf core-site.xml
java.io.FileNotFoundException: ...
F0411 20:42:52.422523 16481 catalog.cc:86] NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
第二个报错的格式是glog配置的格式,而第一个的格式在impala日志中很少见,没有源码文件名和行号,也没有线程id。回看部署机器上的 catalogd.ERROR,开头其实有段 log4j 的warning:
log4j:WARN No appenders could be found for logger (org.apache.hadoop.conf.Configuration).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
F0411 13:38:15.611681 113789 catalog.cc:86] NoClassDefFoundError: org.apache.hadoop.fs.FileSystem
这应该是第一个报错想打印却打印不出来的warning!然后我发现,开发环境的配置文件里是有 log4j.properties 的,但部署环境的配置文件里没有。原来这就是真正的报错没打印的原因!在部署的远程机器里补上 log4j.properties 后,确实就能看到第一个报错的日志了。提了 IMPALA-12999 修复这个问题。
Impala的日志打印很特别,因为代码里即有C++又有Java的。C++代码使用glog打印日志,Java代码使用log4j打印日志,然后通过Impala实现的GlogAppender把 log4j 的日志导到 glog 里去打印。而 GlogAppender 的初始化是在 JniCatalog 的构造函数里的,这个发生在初始化 static 成员之后。因此第一个报错抛出来时,log4j还没被重定向到 glog 里,只是打印到了 stderr,从而出现在了 catalogd.ERROR(但不在catalogd.INFO)里。关于这部分代码的实现,之前梳理过一篇文章:Impala的log4j和glog配置
总结
许多事情回过头看都很简单,不过是 "当局者迷,旁观者清"。作为 "当局者",一定要发散思维,不要在一条路上死磕,比如坚信 NoClassDefFoundError 只能是 CLASSPATH 有问题导致的,那还要浪费很多时间。
知识点:
- Impala部署时也需要log4j.properties,以正常打印初始化时的Java日志。等log4j被重定向到glog后,这个配置就没用了。
- ClassNotFoundException 是 ClassLoader 无法在 CLASSPATH 中找到某个类,经常是编译时和运行时的CLASSPATH不一致导致的。
- NoClassDefFoundError 可能由 ClassNotFoundException 导致,也可能由其它原因如 static 成员初始化失败导致。