从一个bug说起
bug描述
在尝试用docker的alpine镜像运行从golang镜像中编译出来的可执行文件时出现如下的错误
standard_init_linux.go:211: exec user process caused "no such file or directory"
- 1
golang代码如下:
package main
import (
"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)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
Dockerfile文件如下:
FROM golang:1.13 AS builder
WORKDIR /go/src
ADD main.go .
RUN go build -o /go/bin/demo main.go
FROM alpine:3.10
COPY --from=builder /go/bin/demo /app/
CMD ["/app/demo"]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
编译镜像
docker build -t demo .
- 1
运行容器
docker run -it --rm demo
- 1
报上述错误
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")
}
- 1
- 2
- 3
推测 unix.Exec(name, ...)
是执行可执行文件时出错,返回了 exec user process caused ...
。
“no such file or directory”中的file是什么?
既然是执行可执行文件时报文件找不到错误,那么要找的文件是什么呢?
执行如下命令来运行容器并计入到容器的 sh 交互中
docker run -it --rm demo /bin/sh
- 1
发现编译出来的可执行文件 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)
- 1
- 2
- 3
- 4
查找发现 alpine
镜像中并没有这个库文件
/app # find /lib64/ld-linux-x86-64.so.2
find: /lib64/ld-linux-x86-64.so.2: No such file or directory
- 1
- 2
由此判定错误提示中的 “no such file or directory” 指的是 /lib64/ld-linux-x86-64.so.2
这个文件找不到。但是我的代码里并没有引用C库函数,在这里就又引出了两个问题:
- 为什么会动态链接C库
- 为什么编译的时候指定要链接库,但运行的时候却找不到
为什么会动态链接C库
既然我的代码中并没有调用C库,仅有可能的是我引用的包中有引用C库,于是就从引用的下面两个包入手查起
import (
"fmt"
"net/http"
)
- 1
- 2
- 3
- 4
在golang官网中对net
包(http://golang.org/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
- 1
查看当前的环境变量
root@ec1ebffb30e9:/go# go env | grep CGO_ENABLED
CGO_ENABLED="1"
root@ec1ebffb30e9:/go# env
HOSTNAME=ec1ebffb30e9
PWD=/go
HOME=/root
GOLANG_VERSION=1.13.5
TERM=xterm
SHLVL=1
PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GOPATH=/go
_=/usr/bin/env
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
发现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
- 1
在指定net包中使用纯Go后发现程序能在alpine镜像中正常运行。
按照golang官方的解释,如果禁用CGo,net包也不会使用C库,于是修改Dockerfile如下:
ENV CGO_ENABLED=0
RUN go build -o /go/bin/demo main.go
- 1
- 2
测试后发现编译后的可执行程序也能在alpine镜像中正常运行。
至此,可以实锤锅从net/http
降。
为什么会找不到C库
查明了为什么会动态链接C库的问题,那为什么在alpine镜像运行的时候报文件找不到的错误呢?
修改Dockerfile如下:
FROM golang:1.13 AS builder
WORKDIR /go/src
ADD main.go .
RUN go build -o /go/bin/demo main.go
- 1
- 2
- 3
- 4
- 5
直接在编译镜像中查看可执行文件的动态库链接情况如下:
root@baddb6aa6121:/go/bin# ldd /go/bin/demo
linux-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)
- 1
- 2
- 3
- 4
- 5
发现golang镜像使用的C标准库是gnu-libc。再在alpine镜像中执行ldd命令发现使用的C标准库是musl-libc。
>docker run -it --rm alpine:3.10 /bin/sh
/ # ldd
musl libc (x86_64)
Version 1.1.22
Dynamic Program Loader
Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname
- 1
- 2
- 3
- 4
- 5
- 6
所以Go编译的可执行程序在动态链接了C库后在不同的libc库上编译和运行,自然会出现文件找不到的问题。
为了验证这个问题,把Dockerfile中用于编译的镜像golang:1.13
改成golang:1.13-alpine3.10
,确保编译和运行处于相同的C库环境
FROM golang:1.13-alpine3.10 AS builder
WORKDIR /go/src
ADD main.go .
RUN go build -o /go/bin/demo main.go
FROM alpine:3.10
COPY --from=builder /go/bin/demo /app/
CMD ["/app/demo"]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
测试后发现程序能正常运行,文件找不到的问题也可以解释了。
golang build 总结
- 建议把go程序的运行环境与编译环境隔离,实现最小化交付(移除源代码、Golang环境)。镜像的pull/push更快,也避免了源代码的泄漏和安全问题。alpine镜像只有几十M,而golang镜像则可能有1G多。当然,也正是为了最小化交付才有了上述踩坑历程,不作就不会死~~~
- 编译环境和运行环境都使用alpine版本。如果要使用不同版本,则go build的时候禁用CGO(或–tags netgo),将依赖库打包到可执行程序中,实现静态编译。
转载请注明:SuperIT » Golang build 填坑笔记
转载请注明:SuperIT » Golang build 填坑笔记