最近在重构一个非常古老的 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 的依赖是必要的。接下来的解决方法有:

  1. 使用golang的 kafka lib:需要 dev 去修改代码,切换使用的 kafka sdk,可以作为备选方案。
  2. 降低 golang:latest 的 glibc 版本:发行版一般固定 glibc 去编译其他的工具链,替换 glibc 是不明智的。虽然也有一些工具,比如 yum downgrade glibc\* 可以辅助干这件事情。
  3. 换个更老的 glibc 版本的镜像:又避免不了陈年的一堆 bash 脚本。
  4. 将 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.