Kafka发送消息偶发Snappy UnsatisfiedLinkError异常排查
Kafka发送Snappy压缩消息触发UnsatisfiedLinkError问题处理
异常报错堆栈
org.apache.kafka.common.KafkaException: java.lang.UnsatisfiedLinkError: no snappyjava in java.library.path at org.apache.kafka.common.record.CompressionType$3.wrapForOutput(CompressionType.java:83) at org.apache.kafka.common.record.MemoryRecordsBuilder.<init>(MemoryRecordsBuilder.java:130) at org.apache.kafka.common.record.MemoryRecordsBuilder.<init>(MemoryRecordsBuilder.java:166) at org.apache.kafka.common.record.MemoryRecords.builder(MemoryRecords.java:507) at org.apache.kafka.common.record.MemoryRecords.builder(MemoryRecords.java:489) at org.apache.kafka.common.record.MemoryRecords.builder(MemoryRecords.java:437) at org.apache.kafka.clients.producer.internals.RecordAccumulator.recordsBuilder(RecordAccumulator.java:254) at org.apache.kafka.clients.producer.internals.RecordAccumulator.append(RecordAccumulator.java:230) at org.apache.kafka.clients.producer.KafkaProducer.doSend(KafkaProducer.java:942) at org.apache.kafka.clients.producer.KafkaProducer.send(KafkaProducer.java:865) at org.springframework.transaction.support.TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationUtils.java:135) at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerAfterCommit(TransactionSynchronizationUtils.java:123) at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerAfterCommit(AbstractPlatformTransactionManager.java:936) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:782) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152))) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) at org.springframework.web.servlet.FrameworkServlet.doDelete(FrameworkServlet.java:931) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: java.lang.UnsatisfiedLinkError: no snappyjava in java.library.path at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867) at java.lang.Runtime.loadLibrary0(Runtime.java:870) at java.lang.System.loadLibrary(System.java:1122) at org.xerial.snappy.SnappyLoader.loadNativeLibrary(SnappyLoader.java:182) at org.xerial.snappy.SnappyLoader.loadSnappyApi(SnappyLoader.java:154) at org.xerial.snappy.Snappy.<clinit>(Snappy.java:47) at org.xerial.snappy.SnappyOutputStream.<init>(SnappyOutputStream.java:99) at org.xerial.snappy.SnappyOutputStream.<init>(SnappyOutputStream.java:91) at org.xerial.snappy.SnappyOutputStream.<init>(SnappyOutputStream.java:81) at org.apache.kafka.common.record.CompressionType$3.wrapForOutput(CompressionType.java:81) ... 32 common frames omitted
已知异常特征
- 服务启动初期Snappy压缩的Kafka消息可正常发送,运行一段时间后随机触发报错
- 重新构建Docker镜像重启服务后异常暂时消失
- 容器/宿主机
/tmp目录下堆积大量命名格式为snappy-1.1.7-<随机UUID>-libsnappyjava.so的临时动态库文件
根因分析
这个问题是snappy-java旧版本的经典bug,和Kafka本身逻辑无关,核心触发逻辑如下:
snappy-java默认的native库加载逻辑是:首次调用压缩/解压方法时,把jar包内置的对应平台libsnappyjava.so动态库解压到JVM临时目录(默认是/tmp),再调用系统接口加载该临时文件完成初始化,后续所有压缩解压操作都复用这个加载好的库。
异常触发的完整链路:
- 服务刚启动时第一次发Snappy压缩的Kafka消息,snappy-java正常解压so到/tmp并加载,此时所有消息发送都正常
- Linux系统默认开启临时目录清理服务(
systemd-tmpfiles-clean或者tmpwatch),默认会删除/tmp目录下超过指定时间未访问的文件,很多运维场景会把清理周期从默认的10天改到1天甚至更短,snappy解压出来的so文件长时间没被访问就会被删掉 - 当出现类加载器切换(比如Spring事务上下文切换、线程池线程上下文类加载器重建、热加载触发类重新初始化)时,Snappy类的静态块会重新执行,尝试加载native库。这时候发现之前解压的so文件已经被删了,旧版本snappy-java(1.1.7及更早,也就是大部分Kafka客户端默认绑定的版本)存在两个并发bug:
- 多线程同时触发库加载时,解压so文件的文件锁逻辑有缺陷,会出现多个线程同时写临时文件、互相覆盖的情况,部分线程会拿到无效的文件路径
- 一旦临时文件加载失败,代码不会重试解压流程,直接跳到
java.library.path路径下查找全局安装的snappy库,找不到就直接抛出UnsatisfiedLinkError
- 因为每次加载失败都会重新生成一个带随机UUID的so临时文件,所以会观察到/tmp下堆积几十上百个snappy开头的so文件。重启服务时JVM完全重新初始化,第一次加载没有并发冲突,所以异常会暂时消失,直到下次临时文件被清理+并发加载的场景出现。
稳定复现方式
不需要等系统自动清理临时文件,按以下步骤可以稳定复现:
- 保持当前有bug的snappy-java依赖版本(1.1.7及更早)不变,启动服务
- 发送几条Kafka消息确认Snappy压缩功能正常
- 手动执行命令删除所有snappy临时文件:
rm -f /tmp/snappy-*-libsnappyjava.so - 用压测工具并发发起30次以上的Kafka消息发送请求,就会直接抛出和上述堆栈一致的错误,同时能看到/tmp目录下瞬间生成大量新的snappy临时so文件。
永久解决方案
按优先级从高到低选择,任选其一即可彻底解决:
- 方案1(最省事,推荐):直接升级snappy-java版本到1.1.8.4及以上,新版本已经完全修复了临时文件删除后的并发加载bug,也不会反复生成无效临时文件。如果是Maven项目,直接在pom.xml中显式指定snappy-java依赖版本,覆盖Kafka客户端传递引入的旧版本即可:
<dependency> <groupId>org.xerial.snappy</groupId> <artifactId>snappy-java</artifactId> <version>1.1.10.5</version> </dependency>
Gradle项目添加一行配置即可:implementation 'org.xerial.snappy:snappy-java:1.1.10.5'
- 方案2(无法升级依赖时使用):JVM启动时指定snappy的解压目录到不会被系统清理的固定路径,不要使用默认的/tmp目录:
- 在服务部署目录下创建专门存储snappy库的目录,比如
/data/app/snappy-lib,给服务启动账号分配该目录的读写权限 - JVM启动参数添加以下两个配置:
-Dorg.xerial.snappy.tempdir=/data/app/snappy-lib -Dorg.xerial.snappy.lib.path=/data/app/snappy-lib - 将该目录加入系统临时清理任务的白名单,避免被定时任务误删
- 在服务部署目录下创建专门存储snappy库的目录,比如
- 方案3(容器环境适用):直接在Docker镜像中预装系统级的snappy库,snappy-java会优先加载系统全局安装的库,完全不会再解压临时文件到/tmp目录:
CentOS基础镜像在Dockerfile中添加以下配置:
Debian/Ubuntu基础镜像在Dockerfile中添加以下配置:RUN yum install -y snappy snappy-devel && yum clean allRUN apt-get update && apt-get install -y libsnappy-dev && rm -rf /var/lib/apt/lists/*
内容的提问来源于stack exchange,提问作者pain
相关产品推荐
相关产品推荐

