构建兼容旧 Linux 系统的 DuckDB jdbc

432 阅读8分钟

问题背景

在将包含 DuckDB 依赖的程序部署至 CentOS 7 上时,运行 DuckDB 时出现了如下报错:

java.lang.UnsatisfiedLinkError: /tmp/libduckdb_java6604742508229353124.so: 
/lib64/libm.so.6: version `GLIBC_2.23' not found (required by /tmp/libduckdb_java6604742508229353124.so)

为何会出现这个报错?DuckDB 是嵌入式内存数据库,在 DuckDB Jdbc 中其实也包含了 DuckDB 本身,我们可以在源码中找到一大堆 native 方法,这其实就是 DuckDB 在访问其 c++ 语言编译的程序。

这就导致在引入其 jdbc 依赖时会有对除 Java 以外环境的其他需求,如在 Linux 系统上运行时需要较高版本的 Glibc。然而我们公司的程序,特别是 Docker 环境是基于 CentOS 的,现存的 CentOS 包括版本 7 / 8 的 Glibc 版本都较低,完全无法满足 DuckDB 的需求,如何在 CentOS 上运行 DuckDB 就成了程序适配的最大阻碍。

事实上,由于 Java 本身的跨平台特性,在调研阶段根本不会想到这个问题 —— DuckDB 这个做法实际上违背了 Java 精神。而在开发阶段,由于本地运行 MacOS 完全满足 DuckDB 的需求,所以这个问题直到测试阶段在 Docker 上运行时才被发现。

解决方案

相对轻松的两个解决方案——

  1. 在 Docker File 基于 CentOS 构建容器时便一起更新 Glibc 版本。

❌ 由于 Glibc 是 Linux 系统运行的基本库,包括相当数量的命令,组件库都依赖这个运行,一旦更新,很可能出现系统崩溃、不稳定等问题,这对于已经在使用 CC 的用户来说是个“定时炸弹”,我们完全无法预料可能在哪个环节出现问题。

  1. 更换更高的 CentOS 版本,或兼容 CentOS 的版本

❌ 很遗憾所有稳定且免费的 CentOS 版本的 Glibc 版本都较低。最新的 CentOS Stream 9 似乎可以解决问题但他是面向开发者的非稳定版本,不建议在生产使用。 我尝试在兼容 CentOS 的 Rocky Linux 构建,但是实际上还是有些许不同,我们的 DockerFile 需要大批量改动,影响很大。

最后似乎只剩下一条路可以走了 —— 构建兼容旧 Linux 系统的 DuckDB Jdbc

我尝试向 Mother Duck 寻求帮助,然而由于时差问题,一次完整的问答至少需要 1 天时间,而且他们也不一定会协助我们解决问题。现在比较急急国王,等不了这么久,只能拿出中国老传统——自己动手,丰衣足食。

为何我们自己构建 DuckDB 能解决这个问题呢?

C++ 和 Java 不同,Java 构建出来的运行包只依赖于特定的 JDK,只要平台上能正常安装上对应版本的 JDK 就一定跑 Java,但是 C++ 构建出来的运行包会依赖原生系统,而 glibc 是 Linux 系统中最底层的 API,若是构建时的版本太高,低版本的 glibc 就无法兼容。换言之,如果通过低版本的 glibc 来构建运行包,就能解决这个问题。

构建 DuckDB Jdbc

由于需要让 duckdb 适配尽可能多的机器,我们需要准备两套不同架构(AMD/ARM)下的 Linux 系统。当然,建议先尝试使用 AMD 64 的服务器构建,ARM 的环境相对难搭。

环境准备[AMD64]

  • CentOS7 AND64 服务器

  • 手动下载 DuckDB Jdbc 1.2.2 源码 并上传(阿里 ECS 不能用魔法,只能本地施展并上传)

     # 附上 scp 命令示例
     scp ./duckdb-java-1.2.2.0.tar.gz root@xx.xx.xx.xx:~/
    
  • 安装 CMake cmake3
  • 安装 C++
  • 安装 Java

(安装意外的没碰到太大的问题,说实话我都准备一下午就跟他耗上了,以前每次装 C 环境都是大折磨)

很好还是碰到了(QAQ)

其他流程都相对轻松,重点关注 C++ 的安装

CMake

 yum install -y epel-release
 yum install -y cmake3
 ​
 which cmake3
 sudo ln -s /usr/bin/cmake3 /usr/bin/cmake

C++ 11

通过 yum install -y gcc-c++ 安装的 C++ 只有版本 4.8.5,版本太低,而官方要求 11 及以上,只能通过其他方式来,可以通过 scl 工具来切换。

 yum install -y centos-release-scl
 yum install -y scl-utils

这个是 RedHet 推出的一个能帮助切换高版本 C++ 的方便工具,但是由于停止维护了,在下载好后你会发现 yum 功能无法正常使用了,需要去修改 repo

在安装后会在 /etc/yum.repos.d 下发现新增了两个文件 CentOS-SCLo-scl.repo 和 CentOS-SCLo-scl-rh.repo(部分版本没有这个,但不影响)。

在 CentOS-SCLo-scl.repo 中修改 [centos-sclo-sclo] 中的内容

 [centos-sclo-sclo]
 name=CentOS-7 - SCLo sclo
 baseurl=https://mirrors.aliyun.com/centos/7/sclo/x86_64/sclo/
 # mirrorlist=http://mirrorlist.centos.org?arch=$basearch&release=7&repo=sclo-sclo
 gpgcheck=0
 enabled=1
 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-SCLo

在 CentOS-SCLo-scl-rh.repo 中修改(没有该文件则直接新加)[centos-sclo-rh] 中的内容

 [centos-sclo-rh]
 name=CentOS-7 - SCLo rh
 baseurl=https://mirrors.aliyun.com/centos/7/sclo/x86_64/rh/
 # mirrorlist=http://mirrorlist.centos.org?arch=$basearch&release=7&repo=sclo-rh
 gpgcheck=0
 enabled=1
 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-SCLo

完成后刷新 yum 缓存

 yum repolist
 yum clean all
 yum makecache

之后开始正常流程

 # 安装基础的 c++
 sudo yum install libstdc++-devel -y
 yum install -y gcc-c++
 # 查看版本 应该是老的 485
 g++ --version
 # 安装新版本并切换
 yum install -y devtoolset-11*
 scl enable devtoolset-11 bash
 # 再次查看版本
   g++ --version

Java

  • amd 架构 yum install -y java-1.8.0-openjdk-devel.x86_64

环境准备[ARM64]

CentOS7 在 ARM 架构的环境及其恶劣,AMD 至少还有阿里云有几乎完整的支持,ARM 连阿里云都缺胳膊少腿

  • CentOS7 ARM64(docker 版)

     docker pull centos:centos7.9.2009
     docker run -itd --name centos-arm centos:centos7.9.2009 bash
    

    强烈建议在自己机器上运行(可以方便的使用魔法,麻瓜理论上搭不起来环境)

    另:魔法“释放速度”建议用快的,否在可能会在下载卡很久

替换 yum 源

为何这里提一嘴?因为 arm 架构的 yum 源有点不一样,amd 的网上一搜一大堆,这个还真不一定能找到。

由于没有安装 wget,所以我们需要现在宿主机拉下 Repo, 并替换原生的

mkdir tmp
cd tmp
wget https://mirrors.aliyun.com/repo/Centos-altarch-7.repo
docker cp Centos-altarch-7.repo centos-arm:/etc/yum.repos.d/CentOS-Base.repo

备齐编译环境

由于阿里的 centos-release-scl 源中没有收录 arm 架构的 c++,我们只能通过源码编译的方式来安装 c++ 。

别问我为啥不试试网易,清华源——自从上游仓库停止支持后,他们也停止支持了,现在唯一能用的只剩下阿里云了......

 yum install libstdc++-devel -y
 yum install -y gcc-c++
 # 查看版本 应该是老的 485
 g++ --version
 ​
 yum install -y epel-release
 yum install -y cmake3
 ​
 sudo ln -s /usr/bin/cmake3 /usr/bin/cmake
 ​
 yum groupinstall "Development Tools"
 yum install -y bison flex texinfo gmp-devel mpfr-devel libmpc-devel
 # 对,没错,我们用 git 去下,官网里直接推荐用 git,咱入乡随俗(能省一事就省一事)
 # 这也是需要魔法的原因之一
 yum install -y git
 yum install -y make

C++ 11

 git clone git://gcc.gnu.org/git/gcc.git
 cd gcc
 # 与 amd64 版本保持一致
 git checkout releases/gcc-11.2.0
 ./configure --enable-languages=c,c++ --disable-multilib -prefix /usr
 make -j$(nproc)
 make install
 # 这时候应该就完成了,查看下版本
 gcc --version
 

关于 ./configure ...... -prefix /usr 的说明。

在不指定安装前缀 /usr 的情况下,默认会把程序安装至 /usr/local/bin 目录下。

这时你会发现,gcc --version/usr/bin/gcc --version 得到的结果不一致。

然而由于你的 Cmake 以及原先安装的 gcc 在 /usr/bin 目录下,并且对于 Cmake 来说 /usr/bin 下的 gcc 优先级更高,这会导致后续在编译 DuckDB 的时候,依旧会使用 4.8.5 老版本的 gcc,最终导致编译失败。

为啥提一嘴,搜出来的 configure 相关攻略里没有加 prefix ,也没有相关说明 QAQ,导致最终编译 DuckDB 一直失败,我一度以为是 gcc 的版本在 arm 架构下要求更高,直到我一路向上,换上了 2025 年版本的 gcc 才开始怀疑是我 gcc 版本的问题。

归功于这个命令 cmake --system-information | grep CMAKE_C_COMPILER

让我发现 cmake 在使用 cc 作为编译命令,然而使用 cc --version 时,发现其 base 的 gcc 版本是 485,最终排查出上面的结论

Java

 yum install -y java-1.8.0-openjdk-devel.aarch64

DuckDB

由于环境齐备,我们直接用 git 下 DuckDB 源码

 git clone -b v1.2.2.0 https://github.com/duckdb/duckdb-java.git
 ## 查看当前所在 tag
 cd duckdb-jdbc
 git describe --tags --exact-match

接下来就可以愉快的编译啦~

编译

  • 解压targz
tar -zxvf duckdb-java-1.2.2.0.tar.gz 
  • 打包(很久很久)
cd duckdb-java-1.2.2.0
make release

一般能在 build/release/duckdb_jdbc.jar 这个路径下找到打包后的结果

测试 & 安装

根据官方提供的命令测试 —— make test
(blob 阶段可能会等很久,没耐心可以略过)

ARM 64 架构 make test 时可能会遇到 undefined symbol: pthread_atfork 报错

这时需要调整 CMakeLists.txt 文件中的依赖

 vi CMakeLists.txt
 # vim 命令搜索 CMAKE_DL_LIBS 关键词
 /CMAKE_DL_LIBS
 # 在所在的括号中换行并加入下面的依赖
 pthread

最后 CMakeLists.txt 的那块代码应该长这样

 target_link_libraries(duckdb_java PRIVATE
   duckdb-native
   ${CMAKE_DL_LIBS}
   pthread
 )

重新编译

 make clean
 make release

打包完成后,重新进行 make test

将打好的包放到本地进行安装

# 从服务器把包捞回来
scp root@xx.xx.xx.xx:~/duckdb-java-1.2.2.0/build/release/duckdb_jdbc.jar ./
# 安装到 maven 仓库
mvn install:install-file -Dfile=./duckdb_jdbc.jar -DgroupId=org.duckdb -DartifactId=duckdb_jdbc -Dversion=1.2.2.0-glibc2.17 -Dpackaging=jar

打包进简易的项目中进行测试:

  • 1.2.2.0 依赖下的包运行会报错
[root@iZbp1045yp8jiys3baq2alZ ~]# java -jar ./duckdbUse-2.0-SNAPSHOT.jar 
Exception in thread "main" java.lang.UnsatisfiedLinkError: /tmp/libduckdb_java6604742508229353124.so: /lib64/libm.so.6: version `GLIBC_2.23' not found (required by /tmp/libduckdb_java6604742508229353124.so)
	at java.lang.ClassLoader$NativeLibrary.load(Native Method)
	at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1934)
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1817)
	at java.lang.Runtime.load0(Runtime.java:782)
	at java.lang.System.load(System.java:1100)
	at org.duckdb.DuckDBNative.<clinit>(DuckDBNative.java:58)
	at org.duckdb.DuckDBConnection.newConnection(DuckDBConnection.java:52)
	at org.duckdb.DuckDBDriver.connect(DuckDBDriver.java:48)
	at java.sql.DriverManager.getConnection(DriverManager.java:664)
	at java.sql.DriverManager.getConnection(DriverManager.java:208)
	at top.saycode.SimpleDuckDbUse.main(SimpleDuckDbUse.java:28)
  • 依赖 1.2.2.0-glibc2.17 的则一切正常
[root@iZbp1045yp8jiys3baq2alZ ~]# java -jar ./duckdbUse-1.0-SNAPSHOT.jar 
id

更优雅的方式

照常理这个话题到此应该就结束了,然而后续将项目中的依赖替换成我自建的包时又遇到了两个问题:

  • 替换后,由于该包只在 Linux amd64 上打的,Linux arm64 / MacOS / Windows 上将无法使用,虽然这个包一定程度上契合了客户,但是对后续的维护会带来很多麻烦
  • 在项目中复杂的架构/依赖下,我们自己打的包似乎依旧无法正常运行(java 程序总会从错误的地方寻找 native api 包)

我似乎应该寻找一个影响更小的方式

在仓库中找到 1.2.2.0 (官方版本)的 jar 包,解压,看看里面有些啥

# 仓库位置因人而异
 cd ~/.m2/repository/org/duckdb/duckdb_jdbc/1.2.2.0/
# 创建个临时文件夹 用于解压
mkdir tmp
cp duckdb_jdbc-1.2.2.0.jar ./tmp
jar -xf duckdb_jdbc-1.2.2.0.jar
# 查看
ls
# META-INF
# duckdb_jdbc-1.2.2.0.jar
# libduckdb_java.so_linux_amd64 
# libduckdb_java.so_linux_arm64
# libduckdb_java.so_osx_universal
# libduckdb_java.so_windows_amd64 
# org

从上方的查询结果不难看出,duckdb 对应的 c++ 的代码应该是在 libduckdb_java.so_linux_xxx 下

那么解决方案就明朗了,我只需要将在旧 Linux 系统下编译出 jar 包中的 libduckdb_java.so_linux_amd64 替换到官方 jar 包下就能解决上面的问题(其实在 build/release 目录下会直接提供 libduckdb_java.so_linux_amd64 文件)

由于所需的包和关键命令都已提到,替换过程略...

对替换好后的 jar 包重新打包并安装

jar -cfm duckdb_jdbc-1.2.2.0-glibc2.17.jar  META-INF/MANIFEST.MF *
mvn install:install-file -Dfile=./duckdb_jdbc-1.2.2.0-glibc2.17.jar -DgroupId=org.duckdb -DartifactId=duckdb_jdbc -Dversion=1.2.2.0-glibc2.17 -Dpackaging=jar

为保证用户能正常在 arm64 的系统架构上运行,应当在 arm 架构的 linux 系统上再一次构建 DuckDB jdbc 并替换上方的 libduckdb_java.so_linux_arm64

参考资料