在上一篇文章中我们介绍了如何将共享库与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返回异常。

image-20211205114506800

新方案

原因分析

有了上面这个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过来重新编译即可。

image-20211205123202493

我们验证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