微信搜索superit|邀请体验:大数据, 数据管理、OLAP分析与可视化平台 | 赞助作者:赞助作者

Golang build 填坑笔记

go aide_941 1℃ 0评论

从一个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库函数,在这里就又引出了两个问题:

  1. 为什么会动态链接C库
  2. 为什么编译的时候指定要链接库,但运行的时候却找不到

为什么会动态链接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系统中,解析域名有两种选项:

  1. 使用纯Go解析器直接发送DNS请求给/etc/resolv.conf文件中的服务器
  2. 使用基于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 总结

  1. 建议把go程序的运行环境与编译环境隔离,实现最小化交付(移除源代码、Golang环境)。镜像的pull/push更快,也避免了源代码的泄漏和安全问题。alpine镜像只有几十M,而golang镜像则可能有1G多。当然,也正是为了最小化交付才有了上述踩坑历程,不作就不会死~~~
  2. 编译环境和运行环境都使用alpine版本。如果要使用不同版本,则go build的时候禁用CGO(或–tags netgo),将依赖库打包到可执行程序中,实现静态编译。

转载请注明:SuperIT » Golang build 填坑笔记

喜欢 (0)or分享 (0)

您必须 登录 才能发表评论!