clickhouse如何解决GLIBC不兼容问题--终篇
文章目录
在上一篇文章中我们介绍了如何将共享库与binary文件打包来解决clickhouse运行时GLIBC不兼容的问题。但是经过我们的实践发现这种方案其实是有副作用的,而这种副作用在生产环境是致命的,会造成clickhouse进程crash。所以本文推翻了上一篇文章中的方案,使用clickhouse自身的机制彻底解决了GLIBC不兼容问题。
共享库与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真是一个宝藏社区,在各种工程细节上做到了极致,我们想到的想不到的问题他们都有解决,或是在解决的路上。
参考
http://www.lightofdawn.org/wiki/wiki.cgi/NewAppsOnOldGlibc
更多精彩内容,请关注微信公众号:ClickHouse OS
文章作者 后端侠
上次更新 2021-12-03