最近在重构一个非常古老的 build 流程(太多 bash script 了,准备重构到基于 golang:latest 来构建,干掉那些陈年依赖),程序是 golang 写的,构建出来 binary 之后,扔到服务器上一跑,直接挂了。
1 2 |
root@c2a4d003e0d6:/workspace/.build# ./spex_linux_amd64 ./spex_linux_amd64: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by ./spex_linux_amd64) |
这个太有意思了,Golang 不是把所有依赖都 static link 的吗?怎么会出来一个 dynamic link 的 Glibc 的依赖?
1 2 3 4 5 6 7 8 |
root@c2a4d003e0d6:/workspace/.build# ldd spex_linux_amd64 ./spex_linux_amd64: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by ./spex_linux_amd64) linux-vdso.so.1 => (0x00007ffdd4c82000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f8deae5c000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f8deab53000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f8dea94f000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8dea585000) /lib64/ld-linux-x86-64.so.2 (0x00007f8deb079000) |
更有意思的问题是,为什么老的 pipeline build 出来的 binary 没有这些静态链接的依赖呢?
下载了一个之前 build 的产物:
1 2 3 4 |
root@c2a4d003e0d6:/workspace/.build# file spex spex: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped root@c2a4d003e0d6:/workspace/.build# ldd spex not a dynamic executable |
研究了一通,发现这个 dynamic link 实际上是来自一个 kafka 的客户端 confluent-kafka-go,这个客户端是基于 c 语言写得 librdkafka 的,所以编译的时候要 CGO_ENABLED=1. 然后编译的时候就出来 dynamic link 了。
但是为什么原来的 pipeline build 的产物是静态链接的呢?
这里省略2万字的辛酸,我在这堆 bash 脚本里面一点点还原出来了 build 环境,最后竟然发现,即使是一模一样的环境,我 build 出来的产物竟然还是有动态链接的!而 pipeline build 的就没有!这真是太神奇了。
在怀疑人生的同时,我直接改了 CI,在编译之后加了两个 debug 的命令。神奇的事情又发生了,这个 CI build 出来的产物也是动态链接的!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
+ ldd .build/spex linux-vdso.so.1 => (0x00007ffd8d4a8000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f51d8697000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f51d838e000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f51d818a000) librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f51d7f82000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f51d7bb8000) /lib64/ld-linux-x86-64.so.2 (0x00007f51d88b4000) + objdump -p .build/spex + grep NEEDED NEEDED libpthread.so.0 NEEDED libm.so.6 NEEDED libdl.so.2 NEEDED librt.so.1 NEEDED libc.so.6 NEEDED ld-linux-x86-64.so.2 ... |
但是我从文件服务器上下载回来的 binary 明明就不是一个 dynamic link 的 executable!
编译之后的步骤就是 upload_binary
了,之前看它的名字只是上传,觉得应该很简单。但是现在隐约觉得并不简单,这个函数里面有什么猫腻。于是就开始阅读这里的脚本。
然后就发现了一个神奇的命令,这个脚本在编译完成之后,竟然 ssh 到文件服务器,去执行了一个 upx
命令。
upx 是一个压缩二进制的工具,如上图,经过压缩之后,这些 binary 的体积都减少了 46%。
但是 upx 压缩之后,会让这个 dynamic link 的 executable 看起来是 static link 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
root@c2a4d003e0d6:/workspace/.build# upx spex Ultimate Packer for eXecutables Copyright (C) 1996 - 2013 UPX 3.91 Markus Oberhumer, Laszlo Molnar & John Reiser Sep 30th 2013 File size Ratio Format Name -------------------- ------ ----------- ----------- 37517592 -> 19717828 52.56% linux/ElfAMD spex Packed 1 file. root@c2a4d003e0d6:/workspace/.build# file spex spex: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped root@c2a4d003e0d6:/workspace/.build# ldd spex not a dynamic executable |
所谓“看起来是”,就是在执行的时候,其实还是要“解压”,然后去 dynamic link 一些 lib。可以把依赖的 .so
文件给删掉测试一下。
发现就无法正常运行了:
1 2 3 |
root@c2a4d003e0d6:/workspace/.build# mv /lib/x86_64-linux-gnu/libm.so.6 /tmp root@c2a4d003e0d6:/workspace/.build# ./spex --version ./spex: error while loading shared libraries: libm.so.6: cannot open shared object file: No such file or directory |
所以,实际上新的 pipeline 构建出来的 binary 无法正常工作的根本原因是:
- 之前的 binary 也是 dynamic link 的,只不过 upx 过后看起来像是一个 static link 的 binary 了。但是由于之前的构建环境非常老,依赖的 glibc 版本很低,所以可以直接扔到服务器上去运行。
- 但是新的 pipeline 是基于
golang:latest
构建的,依赖的 glibc 的版本是 2.29, 服务器上没有,所以跑不了。
和 dev 沟通之后,这个 CGO 的依赖是必要的。接下来的解决方法有:
- 使用golang的 kafka lib:需要 dev 去修改代码,切换使用的 kafka sdk,可以作为备选方案。
- 降低 golang:latest 的 glibc 版本:发行版一般固定 glibc 去编译其他的工具链,替换 glibc 是不明智的。虽然也有一些工具,比如
yum downgrade glibc\*
可以辅助干这件事情。 - 换个更老的 glibc 版本的镜像:又避免不了陈年的一堆 bash 脚本。
- 将 c 语言写的依赖静态链接。
综上,还是打算使用最新版的 image 来编译,但是将依赖全部静态链接,做到一次编译,到处运行,下载下来就能跑。
静态链接 CGO 的依赖
如果使用 glibc 的是,是不能静态链接的:
1 2 3 4 5 6 |
root@f88271a666f9:/workspace# go build -ldflags "-linkmode external -extldflags '-static'" ./cmd/spex # git.garena.com/shopee/platform/spex/cmd/spex /usr/bin/ld: /go/pkg/mod/github.com/confluentinc/confluent-kafka-go@v1.5.2/kafka/librdkafka/librdkafka_glibc_linux.a(rddl.o): in function `rd_dl_open': (.text+0x1d): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /usr/bin/ld: /tmp/go-link-883441031/000004.o: in function `_cgo_26061493d47f_C2func_getaddrinfo': /tmp/go-build/cgo-gcc-prolog:58: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking |
因为 glibc 依赖了 libnss ,libnss 支持不同的 provider,必须被 dynamic link.
所以这里只有使用 musl 替换 glibc 了。librdkafka 和 golang 的封装 confluent-kafka-go 都支持 musl 构建。只要在构建的时候指定 --tags musl
即可。alpine 是基于 musl 的发行版,这里直接使用 alpine Linux 进行构建。
然后指定使用外部 ld,指定 flags 使用 -static
,编译出来的 binary 就完全是静态链接的了。编译过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ docker run -it -v $(pwd):/workspace -v /Users/xintao.lai/.netrc:/root/.netrc golang:alpine3.14 /go $ cd /workspace/ /workspace $ apk add git alpine-sdk fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz (1/37) Installing fakeroot (1.25.3-r3) (2/37) Installing openssl (1.1.1l-r0) (3/37) Installing libattr (2.5.1-r0) (4/37) Installing attr (2.5.1-r0) (5/37) Installing libacl (2.2.53-r0) (6/37) Installing tar (1.34-r0) (7/37) Installing pkgconf (1.7.4-r0) ... /workspace $ go build -ldflags "-linkmode external -extldflags '-static'" -tags musl ./cmd/spex /workspace $ ldd spex /lib/ld-musl-x86_64.so.1: spex: Not a valid dynamic program |
静态编译 CGO 的依赖可以参考这篇教程:Using CGO bindings under Alpine, CentOS and Ubuntu 和这个例子:go-static-linking.
所以这里只要使用 musl 替换 glibc 了 -> 所以这里只有使用 musl 替换 glibc 了?
谢谢,已经改正。
为啥不能在服务器上装新的 glibc 呢
glibc 一般是发行版维护的,发行版针对自带的 glibc 打包了各种软件,升级 glibc 是很危险的行为,会破坏很多软件的兼容性。一般来说,不升级 glibc, 而是直接升级发行版到更新的版本。
Pingback: golang编译优化之静态链接 - FranzKafka Blog
关于更换程序依赖的动态库,可以用 patchelf 这个工具,单独修改程序加载动态库的路径而不影响系统。
比如我的主力机是一台古老的 Linux 主机,系统的 libc.so 版本是 2.23。而我需要用的 nvim 依赖的又是 2.29。这时我可以编译好 2.29 版本的 libc,安装在 /opt/glibc/lib/ 目录。然后用 patchelf 执行:
patchelf –set-interpreter /opt/glibc/lib/ld-linux-x86-64.so.2 –set-rpath /opt/glibc/lib/ ./nvim
用 ldd 查看 nvim,可以看到已经把依赖改为指定的路径了。