最近在重构一个非常古老的 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
upx

upx 是一个压缩二进制的工具,如上图,经过压缩之后,这些 binary 的体积都减少了 46%。

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
.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 无法正常工作的根本原因是:

golang:latest

和 dev 沟通之后,这个 CGO 的依赖是必要的。接下来的解决方法有:

yum downgrade glibc\*

综上,还是打算使用最新版的 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.

--tags musl
-static
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.