Golang 的一个动态链接依赖问题

最近在重构一个非常古老的 build 流程(太多 bash script 了,准备重构到基于 golang:latest 来构建,干掉那些陈年依赖),程序是 golang 写的,构建出来 binary 之后,扔到服务器上一跑,直接挂了。

这个太有意思了,Golang 不是把所有依赖都 static link 的吗?怎么会出来一个 dynamic link 的 Glibc 的依赖?

更有意思的问题是,为什么老的 pipeline build 出来的 binary 没有这些静态链接的依赖呢?

下载了一个之前 build 的产物:

研究了一通,发现这个 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 出来的产物也是动态链接的!

但是我从文件服务器上下载回来的 binary 明明就不是一个 dynamic link 的 executable!

编译之后的步骤就是 upload_binary 了,之前看它的名字只是上传,觉得应该很简单。但是现在隐约觉得并不简单,这个函数里面有什么猫腻。于是就开始阅读这里的脚本。

然后就发现了一个神奇的命令,这个脚本在编译完成之后,竟然 ssh 到文件服务器,去执行了一个 upx 命令。

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

但是 upx 压缩之后,会让这个 dynamic link 的 executable 看起来是 static link 的

所谓“看起来是”,就是在执行的时候,其实还是要“解压”,然后去 dynamic link 一些 lib。可以把依赖的 .so文件给删掉测试一下。

发现就无法正常运行了:

所以,实际上新的 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 的是,是不能静态链接的

因为 glibc 依赖了 libnss ,libnss 支持不同的 provider,必须被 dynamic link.

所以这里只有使用 musl 替换 glibc 了。librdkafka 和 golang 的封装 confluent-kafka-go 都支持 musl 构建。只要在构建的时候指定 --tags musl 即可。alpine 是基于 musl 的发行版,这里直接使用 alpine Linux 进行构建。

然后指定使用外部 ld,指定 flags 使用 -static,编译出来的 binary 就完全是静态链接的了。编译过程如下:

静态编译 CGO 的依赖可以参考这篇教程:Using CGO bindings under Alpine, CentOS and Ubuntu  和这个例子:go-static-linking.



Golang 的一个动态链接依赖问题”已经有6条评论

    • glibc 一般是发行版维护的,发行版针对自带的 glibc 打包了各种软件,升级 glibc 是很危险的行为,会破坏很多软件的兼容性。一般来说,不升级 glibc, 而是直接升级发行版到更新的版本。

  1. 关于更换程序依赖的动态库,可以用 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,可以看到已经把依赖改为指定的路径了。

Leave a comment

您的电子邮箱地址不会被公开。 必填项已用 * 标注