一次诊断 org.xerial.snappy.Snappy NoClassDefFoundError

某一天, 发现有些 Java web 应用突然自己重启, 有些时候重启完不能正常工作. 查看应用日志, 发现日志中出现下面的错误:

2020-04-22 01:11:39,369 ERROR [squbs-akka.actor.default-dispatcher-5] ActorSystemImpl ActorSystem(squbs) Uncaught error from thread [squbs-cal-publishing-dispatcher-18326] shutting down JVM since 'akka.jvm-exit-on-fatal-error' is enabled
java.lang.NoClassDefFoundError: Could not initialize class org.xerial.snappy.Snappy
    at com.tianxiaohui.java.BufferedWriteChannel.flush(BufferedWriteChannel.java:264)

从上面日志可以看出, 这个应用使用的是 Squbs 框架, 它其实是封装的 Akka. 当 Akka 遇到严重的出错(这里是 NoClassDefFoundError)时, 如果还设置了 "akka.jvm-exit-on-fatal-error" 选项, 那么就会迫使 JVM 系统就会自动重启.

那么问题是: 为什么这个类定义不能被找到呢?
首先, 我们查看了这个 snappy-java.xxx.jar 是在该应用的 classpath 的; 同时, 该应用在发生第一次自重启之前已经运行了一段时间.

仔细查看这个 Snappy.java 类, 就会发现其实它就是一个不做任何事情的 Java 代理类, 它的所有功能都是 native 代码实现的. 所以, 它的方法都是 static 方法. 关键的是, 它有一段静态块 (static block) 代码, 这个代码初始化 native 相关的功能实现. 也就是说, 如果这段静态块无法完成, 就会导致该类无法初始化完成, 导致 NoClassDefFoundError.

Snappy.png

虽然这段静态块包装了原始异常 (Exception), 并且抛出新异常, 可是在我们日志中, 并没有发现有关的信息. 为了搞清楚这里的具体出错, 我们做了如下测试代码:

import org.xerial.snappy.Snappy;

public class SnappyTest {
    public static void main(String[] args) {
        System.out.println("before ");
        try {
            Snappy.maxCompressedLength(4);
        } catch (Throwable t) {
            System.out.print(t.getClass());
            t.printStackTrace();
        }
        System.out.println("middle ");
        try {
            Snappy.maxCompressedLength(4);
        } catch (Exception t) {
            t.printStackTrace();
        }
        System.out.println("after ");
    }
}

测试结果:

java -cp snappy-java-1.1.0.jar:. SnappyTest
before
java.lang.UnsatisfiedLinkError: /tmp/snappy-1.1.0-cc87f5bb-c0a2-483b-9aed-1bf409ab2b94-libsnappyjava.so: /tmp/snappy-1.1.0-cc87f5bb-c0a2-483b-9aed-1bf409ab2b94-libsnappyjava.so: failed to map segment from shared object
                at java.lang.ClassLoader$NativeLibrary.load(Native Method)
                at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
                at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1824)
                at java.lang.Runtime.load0(Runtime.java:809)
                at java.lang.System.load(System.java:1086)
                at org.xerial.snappy.SnappyLoader.loadNativeLibrary(SnappyLoader.java:166)
                at org.xerial.snappy.SnappyLoader.load(SnappyLoader.java:145)
                at org.xerial.snappy.Snappy.<clinit>(Snappy.java:47)
                at SnappyTest.main(SnappyTest.java:7)
middle
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class org.xerial.snappy.Snappy at SnappyTest.main(SnappyTest.java:13)
end 

从上面的测试可以看出, 当加载该类的时候, ClassLoader 抛出了 UnsatisfiedLinkError, 这是一个 Error 类型, 导致Akka 重启 JVM.

另外从 heap dump 我们可以看到, 虽然该类没有加载成功, 但是这个类以及在内存中, 只不过必要的字段应该有值, 不过现在是空.
SnappyDebug.png

从上面的截图中看到 impl 这个本应该在静态块中初始化的值 现在却是空值. 另外, 我加了 2 个 Throwable 的字段用来存放内部抛出的异常, 和当前新建的异常. 并且之前抛出的异常部分要把原来代码的 Exception 改为 Throwable, 否则无法捕获.

另外从 heap dump 中的 SnappyLoader 的文件字段, 可以看到具体的文件存在什么地方. 我们出问题的文件就是上面日志中提到的:
/tmp/snappy-1.1.0-cc87f5bb-c0a2-483b-9aed-1bf409ab2b94-libsnappyjava.so

去查看这个文件, 发现文件缺失存在的, 进一步检查, 发现问题出在 /tmp 目录的挂载上面(mount).

:~$ mount | grep /tmp
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec)
:~$ sudo mount -o remount,rw,nosuid,nodev tmpfs  /tmp
:~$ mount | grep /tmp
tmpfs on /tmp type tmpfs (rw,nosuid,nodev)

通过上面的查看和修复可以看到, 一开始挂载的 /tmp 目录包含有 noexec 属性, 重新挂载, 去掉这个属性, 一切就回复正常了.

标签: none

添加新评论