共享库与binary文件打包的副作用
test_getaddrinfo.cpp
#include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <string.h> #include <stdio.h> int main() { struct addrinfo hints; struct addrinfo *res; memset(&hints, 0, sizeof hints); hints.ai_flags = 1024; hints.ai_family = 0; hints.ai_socktype = 1; hints.ai_protocol = 6; hints.ai_addrlen = 0; hints.ai_addr = 0x0; hints.ai_canonname = 0x0; hints.ai_next = 0x0; int err = ::getaddrinfo("localhost", "8000", &hints, &res); printf("%d", err); return 0; }
按照上一篇文章 中的方法,我们在ubuntu20环境中编译test_getaddrinfo.cpp文件,连同其依赖的共享库一块打包部署到ubuntu16环境,具体步骤如下
- u20环境中,依次进行编译、复制共享库、修改ELF信息等操作
$ g++ test_getaddrinfo.cpp -o test_getaddrinfo $ ldd ./test_getaddrinfo linux-vdso.so.1 (0x00007ffc539ea000) libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007fc38ae95000) /lib64/ld-linux-x86-64.so.2 (0x00007fc38b08e000) $ copylib.sh ./test_getaddrinfo $ patchelf --set-rpath /home/liyang/lib test_getaddrinfo $ patchelf --set-interpreter /home/liyang/lib/ld-linux-x86-64.so.2 test_getaddrinfo
- 将get_getaddrinfo连同lib文件一块复制到u16运行环境中。
- 在u16环境中运行test_getaddrinfo, 可以看到程序异常, 因为
::getaddrinfo
返回的不是零
$ ./test_getaddrinfo -11
那么问题在哪里呢?作为对照组,我们直接在u16环境中编译运行test_getaddrinfo.cpp文件,返回为零。
分别strace这两个binary的运行过程,左右分别对应u16和u20编译的bianry。可以看到右边会多加载很多.so文件,而这些.so文件并不能通过copylib工具打包。个人猜想这个原因导致了test_getaddrinfo运行时没有彻底去除对u20内核的依赖,进而导致::getaddrinfo
返回异常。
新方案
原因分析
有了上面这个bad case, 随binary打包lib的方案显然是走不通了。我们回溯到最初是的问题:u20编译的clickhouse, 在u16环境下启动时会报错:version GLIBC_2.27 not found
我们看看u16支持哪些GLIBC版本? 2.27确实不在u16支持之列
$ strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC_ GLIBC_2.2.5 GLIBC_2.2.6 GLIBC_2.3 GLIBC_2.3.2 GLIBC_2.3.3 GLIBC_2.3.4 GLIBC_2.4 GLIBC_2.5 GLIBC_2.6 GLIBC_2.7 GLIBC_2.8 GLIBC_2.9 GLIBC_2.10 GLIBC_2.11 GLIBC_2.12 GLIBC_2.13 GLIBC_2.14 GLIBC_2.15 GLIBC_2.16 GLIBC_2.17 GLIBC_2.18 GLIBC_2.22 GLIBC_2.23 GLIBC_PRIVATE
那clickhouse中glibc 2.27到底是哪个动态库引入的呢?
$ objdump -x ./clickhouse Version References: required from libdl.so.2: 0x09691a75 0x00 02 GLIBC_2.2.5 required from libpthread.so.0: 0x09691a75 0x00 03 GLIBC_2.2.5 0x09691974 0x00 04 GLIBC_2.3.4 required from libm.so.6: 0x09691a75 0x00 05 GLIBC_2.2.5 0x06969187 0x00 06 GLIBC_2.27
libm库与数学计算相关,那么到底其中哪个symbol依赖了glibc 2.27呢?
$ readelf -s clickhouse | grep "GLIBC_2.27" 148: 0000000000000000 0 FUNC GLOBAL DEFAULT UND powf@GLIBC_2.27 (3)
原来是powf函数引入了对GLIBC的依赖。联想到clickhouse中有一个模块专门处理glibc不兼容问题
base/glibc-compatibility/glibc-compatibility.c
/** Allows to build programs with libc 2.27 and run on systems with at least libc 2.4, * such as Ubuntu Hardy or CentOS 5. * * Also look at http://www.lightofdawn.org/wiki/wiki.cgi/NewAppsOnOldGlibc */
base/glibc-compatibility如何解决不兼容问题呢?原理很简单,重新override clickhouse依赖的glibc函数即可。但是clickhouse 20.3版本的base/glibc-compatibility中并没有override powf函数,因此编译ck时就依赖了u20本地的libm共享库, 也就引入了GLIBC 2.27的依赖。
解决方案
原因明确之后,解决方案便一目了然了:在base/glibc-compatibility中override powf的实现,好在clickhouse最新版中已经override了powf, 直接将相关代码copy过来重新编译即可。
我们验证override powf之后的clickhouse binary是否还依赖GLIBC 2.27?
$ objdump -x ./clickhouse | grep "GLIBC_2.27" $ readelf -s clickhouse | grep powf 372223: 0000000028bb549c 727 FUNC GLOBAL DEFAULT 15 powf 460362: 0000000000000000 0 FILE LOCAL DEFAULT ABS powf.c 460380: 0000000000000000 0 FILE LOCAL DEFAULT ABS powf_data.c 460381: 00000000124f7900 296 OBJECT LOCAL HIDDEN 11 __powf_log2_data 1274523: 0000000028bb549c 727 FUNC GLOBAL DEFAULT 15 powf
clickhouse中的symbol: powf已经不依赖GLIBC_2.27了。
总结
在本文中我们首先证明了文章中的方案对于clickhouse行不通,然后分析了clickhouse为何会引入GLIBC 2.27的依赖,最后我们通过在base/glibc-compatibility中补全对powf的override,彻底解决了GLIBC <=2.27时兼容性问题。
顺便一提,社区最近的一个PR 已经实现了Hermetic builds, 使得编译clickhouse时只依赖clickhouse中的libc库,彻底去除了对编译环境中libc的依赖,从根本上解决了GLIBC > 2.27时的兼容性问题。到此不由感叹clickhouse真是一个宝藏社区,在各种工程细节上做到了极致,我们想到的想不到的问题他们都有解决,或是在解决的路上。