You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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),再调用系统接口加载该临时文件完成初始化,后续所有压缩解压操作都复用这个加载好的库。
异常触发的完整链路:

  1. 服务刚启动时第一次发Snappy压缩的Kafka消息,snappy-java正常解压so到/tmp并加载,此时所有消息发送都正常
  2. Linux系统默认开启临时目录清理服务(systemd-tmpfiles-clean或者tmpwatch),默认会删除/tmp目录下超过指定时间未访问的文件,很多运维场景会把清理周期从默认的10天改到1天甚至更短,snappy解压出来的so文件长时间没被访问就会被删掉
  3. 当出现类加载器切换(比如Spring事务上下文切换、线程池线程上下文类加载器重建、热加载触发类重新初始化)时,Snappy类的静态块会重新执行,尝试加载native库。这时候发现之前解压的so文件已经被删了,旧版本snappy-java(1.1.7及更早,也就是大部分Kafka客户端默认绑定的版本)存在两个并发bug:
    • 多线程同时触发库加载时,解压so文件的文件锁逻辑有缺陷,会出现多个线程同时写临时文件、互相覆盖的情况,部分线程会拿到无效的文件路径
    • 一旦临时文件加载失败,代码不会重试解压流程,直接跳到java.library.path路径下查找全局安装的snappy库,找不到就直接抛出UnsatisfiedLinkError
  4. 因为每次加载失败都会重新生成一个带随机UUID的so临时文件,所以会观察到/tmp下堆积几十上百个snappy开头的so文件。重启服务时JVM完全重新初始化,第一次加载没有并发冲突,所以异常会暂时消失,直到下次临时文件被清理+并发加载的场景出现。

稳定复现方式

不需要等系统自动清理临时文件,按以下步骤可以稳定复现:

  1. 保持当前有bug的snappy-java依赖版本(1.1.7及更早)不变,启动服务
  2. 发送几条Kafka消息确认Snappy压缩功能正常
  3. 手动执行命令删除所有snappy临时文件:rm -f /tmp/snappy-*-libsnappyjava.so
  4. 用压测工具并发发起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目录:
    1. 在服务部署目录下创建专门存储snappy库的目录,比如/data/app/snappy-lib,给服务启动账号分配该目录的读写权限
    2. JVM启动参数添加以下两个配置:
      -Dorg.xerial.snappy.tempdir=/data/app/snappy-lib
      -Dorg.xerial.snappy.lib.path=/data/app/snappy-lib
      
    3. 将该目录加入系统临时清理任务的白名单,避免被定时任务误删
  • 方案3(容器环境适用):直接在Docker镜像中预装系统级的snappy库,snappy-java会优先加载系统全局安装的库,完全不会再解压临时文件到/tmp目录:
    CentOS基础镜像在Dockerfile中添加以下配置:
    RUN yum install -y snappy snappy-devel && yum clean all
    
    Debian/Ubuntu基础镜像在Dockerfile中添加以下配置:
    RUN apt-get update && apt-get install -y libsnappy-dev && rm -rf /var/lib/apt/lists/*
    

内容的提问来源于stack exchange,提问作者pain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 21:57:23