Golang build 填坑笔记
从一个bug说起
bug描述
在尝试用docker的alpine镜像运行从golang镜像中编译出来的可执行文件时出现如下的错误
standard_init_linux.go:211: exec user process caused "no such file or directory"
golang代码如下:
package mainimport ("fmt""net/http")func hello(w http.ResponseWriter, req *http.Request) {fmt.Fprintf(w, "hello\n")}func main() {http.HandleFunc("/hello", hello)http.ListenAndServe("0.0.0.0:8080", nil)}
Dockerfile文件如下:
FROM golang:1.13 AS builderWORKDIR /go/src ADD main.go .RUN go build -o /go/bin/demo main.goFROM alpine:3.10COPY --from=builder /go/bin/demo /app/CMD ["/app/demo"]
编译镜像
docker build -t demo .
运行容器
docker run -it --rm demo
报上述错误
bug定位
standard_init_linux.go
是从哪里来的?
搜索后发现 opencontainers/runc 项目里有同名的文件,根据项目介绍得知该项目是用于根据 OCI 规范生成和运行容器的命令行工具,而这个bug也是在docker容器中运行出现的,由此推测该错误输出源于这里。
查看standard_init_linux.go
文件的211行有如下代码
if err := unix.Exec(name, l.config.Args[0:], os.Environ()); err != nil {return newSystemErrorWithCause(err, "exec user process")}
推测unix.Exec(name, ...)
是执行可执行文件时出错,返回了exec user process caused ...
。
"no such file or directory"中的file是什么?
既然是执行可执行文件时报文件找不到错误,那么要找的文件是什么呢?
执行如下命令来运行容器并计入到容器的sh交互中
docker run -it --rm demo /bin/sh
发现编译出来的可执行文件demo是在/app/目录下的,于是怀疑编译出来的可执行程序动态依赖了其它共享库,通过ldd
命令发现其依赖如下:
/app # ldd demo/lib64/ld-linux-x86-64.so.2 (0x7f192d310000)libpthread.so.0 => /lib64/ld-linux-x86-64.so.2 (0x7f192d310000)libc.so.6 => /lib64/ld-linux-x86-64.so.2 (0x7f192d310000)
查找发现alpine
镜像中并没有这个库文件
/app # find /lib64/ld-linux-x86-64.so.2find: /lib64/ld-linux-x86-64.so.2: No such file or directory
由此判定错误提示中的“no such file or directory”指的是/lib64/ld-linux-x86-64.so.2
这个文件找不到。但是我的代码里并没有引用C库函数,在这里就又引出了两个问题:
为什么会动态链接C库为什么编译的时候指定要链接库,但运行的时候却找不到
为什么会动态链接C库
既然我的代码中并没有调用C库,仅有可能的是我引用的包中有引用C库,于是就从引用的下面两个包入手查起
import ("fmt""net/http")
在golang官网中对net
包(/pkg/net)关于域名解析有如下解释:
On Unix systems, the resolver has two options for resolving names. It can use a pure Go resolver that sends DNS requests directly to the servers listed in /etc/resolv.conf, or it can use a cgo-based resolver that calls C library routines such as getaddrinfo and getnameinfo.
By default the pure Go resolver is used, because a blocked DNS request consumes only a goroutine, while a blocked C call consumes an operating system thread. When cgo is available, the cgo-based resolver is used instead under a variety of conditions: on systems that do not let programs make direct DNS requests (OS X), when the LOCALDOMAIN environment variable is present (even if empty), when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, when the ASR_CONFIG environment variable is non-empty (OpenBSD only), when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the Go resolver does not implement, and when the name being looked up ends in .local or is an mDNS name.
大致意思是,在Unix系统中,解析域名有两种选项:
使用纯Go解析器直接发送DNS请求给/etc/resolv.conf文件中的服务器使用基于CGo的解析器调用C库程序,例如getaddrinfo和getnameinfo
默认使用纯Go解析器,因为对于调用一个阻塞的DNS请求,Go仅需要消耗一个goroutine,而C程序需要消耗一个操作系统线程。但当CGo可用时(CGO_ENABLE=1),则会使用基于CGo的解析器,除非有如下情况:
系统不允许程序直接发起DNS请求(OS X)LOCALDOMAIN
环境变量存在,即使值为空RES_OPTIONS
或者HOSTALIASES
环境变量不为空ASR_CONFIG
环境变量不为空(仅OpenBSD)Go解析器没有实现 /etc/resolv.conf 或 /etc/nsswitch.conf 中指定的特性名称以.local
结尾,或是一个 mDNS 名称
于是执行一下命令进入到golang:1.13镜像的bash交互中
docker run -it --rm golang:1.13 bash
查看当前的环境变量
root@ec1ebffb30e9:/go# go env | grep CGO_ENABLEDCGO_ENABLED="1"root@ec1ebffb30e9:/go# envHOSTNAME=ec1ebffb30e9PWD=/goHOME=/rootGOLANG_VERSION=1.13.5TERM=xtermSHLVL=1PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binGOPATH=/go_=/usr/bin/env
发现golang:1.13镜像中默认CGo可用,也没有上面列到的特殊情况,因此推测代码中net/http
包调用列C库。
继续看golang官网对net包的解释还有如下一段话:
The decision can also be forced while building the Go source tree by setting the netgo or netcgo build tag.
也就是说在go build
的时候可以通过-tags netgo
或-tags netcgo
来指定net包使用纯Go还是CGo。于是在Dockerfile中的go build
指令中添加-tags netgo
参数如下:
RUN go build -tags netgo -o /go/bin/demo main.go
在指定net包中使用纯Go后发现程序能在alpine镜像中正常运行。
按照golang官方的解释,如果禁用CGo,net包也不会使用C库,于是修改Dockerfile如下:
ENV CGO_ENABLED=0RUN go build -o /go/bin/demo main.go
测试后发现编译后的可执行程序也能在alpine镜像中正常运行。
至此,可以实锤锅从net/http
降。
为什么会找不到C库
查明了为什么会动态链接C库的问题,那为什么在alpine镜像运行的时候报文件找不到的错误呢?
修改Dockerfile如下:
FROM golang:1.13 AS builderWORKDIR /go/src ADD main.go .RUN go build -o /go/bin/demo main.go
直接在编译镜像中查看可执行文件的动态库链接情况如下:
root@baddb6aa6121:/go/bin# ldd /go/bin/demolinux-vdso.so.1 (0x00007ffe233b4000)libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f8f32f57000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8f32d96000)/lib64/ld-linux-x86-64.so.2 (0x00007f8f32f7e000)
发现golang镜像使用的C标准库是gnu-libc。再在alpine镜像中执行ldd命令发现使用的C标准库是musl-libc。
>docker run -it --rm alpine:3.10 /bin/sh/ # lddmusl libc (x86_64)Version 1.1.22Dynamic Program LoaderUsage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname
所以Go编译的可执行程序在动态链接了C库后在不同的libc库上编译和运行,自然会出现文件找不到的问题。
为了验证这个问题,把Dockerfile中用于编译的镜像golang:1.13
改成golang:1.13-alpine3.10
,确保编译和运行处于相同的C库环境
FROM golang:1.13-alpine3.10 AS builderWORKDIR /go/src ADD main.go .RUN go build -o /go/bin/demo main.goFROM alpine:3.10COPY --from=builder /go/bin/demo /app/CMD ["/app/demo"]
测试后发现程序能正常运行,文件找不到的问题也可以解释了。