Go 语言镜像精简

为了精简镜像,一般会加载两个镜像,第一层是编译环境,来根据源码编译出可执行的二进制文件。然后拷贝可执行文件到 scratch 镜像中,作为最终的镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM xxx:lastest AS builder
ARG SVC
ARG ARCH
ARG GOARM

WORKDIR
COPY . .

RUN make ARCH=$ARCH $SVC && mv build/$ARCH/$SVC /exe

FROM scratch
COPY --from=builder /exe /
# ENTRYPOINT ["/exe"]

二进制文件的动态链接和静态链接导致的问题

Golang 语言程序编译时会将所有必须的依赖编译到二进制文件中,但也不完全使用的是静态链接,因为 Golang 的某些包是依赖系统标准库的,例如使用到 DNS 解析的包。只要代码中导入了这些包,编译的二进制文件就需要调用到某些系统库,Golang 中使用 CGO,以允许 Golang 调用 C 代码,这样编译好的二进制文件就可以调用系统库。

也就是说,如果 Go 程序使用了 net 包,就会生成一个动态的二进制文件,没有那些动态链接库,镜像就不能够正常工作。

会出现以下的错误:

1
exec user rocess caused "no such file or directory"

解决

如果想让镜像能够正常工作,有几种方法:

  1. 禁用 CGO:过设置环境变量 CGO_ENABLED=0 来禁用 CGO,此时程序会使用内置的实现来替代系统库(例如使用内置的 DNS 解析器)。这种情况下生成的二进制文件是不依赖外部动态链接库的,可以通过 ldd 命令查看。

  2. 静态链接:通过添加编译选项 -ldflags "-extldflags '-static'" 实现编译时,把需要的链接库都打包,这样生成的二进制文件就是静态链接的,可以直接在 scratch 中执行。

  3. 必须将需要的库文件复制到镜像中;

  4. 直接使用 busybox:glibc 镜像;

Golang 域名解析

GolangDNS 解析对协程支持很好, 即 DNS 解析时不会阻塞执行线程,只会阻塞当前协程。

根据官方文档中关于域名解析的描述:

域名解析会间接调用 Dial 函数或者直接使用 LokupHostLookupAddr 函数。不同操作系统有不同的实现方式。在 Unix 类系统中有两种方法进行域名解析:

1) 纯 Golang 语言实现的域名解析:从 /etc/resolv.conf 中取出本地 dns server 列表,然后发送 UDP 报文(DNS请求) 来获得解析结果;

2) 使用 CGO 实现,调用到 C 标准库的 getaddrinfogetnameinfo 函数(不建议使用,因为对协程不友好)

关于 cgo dns 解析的坑 参照以下链接:

https://jira.mongodb.org/browse/MGO-41

https://github.com/golang/go/issues/8602#issuecomment-66098142

CGO 编译导致的问题

默认会使用纯 Golang 实现的域名解析。但是会有一种情况,程序必须打开 CGO 编译且使用静态链接 (通过 -ldflags "-extldflags '-static'" 来实现静态链接)。这时候就会出现程序静态链接了 CGODNS 解析库,打包进 scratch 之类的镜像之后,就会出现解析域名服务失败的问题。

解决

编译的时候,指定标签 -tags='netgo',来使用指定使用内置纯 Golang 实现的 netgo 库。