10

构建最小的Go程序镜像

 3 years ago
source link: https://niyanchun.com/build-minimal-go-image.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

构建最小的Go程序镜像

2017-01-02 技术 Docker, Go 5272次阅读 9 条评论

我们知道构建一个Docker镜像的时候往往需要引入一些程序依赖的东西,最常见的就是引入一个基础操作系统镜像,但这样往往会使得编译出来的镜像特别大。但是对于go语言,我们可以使用静态编译的方式构建出超小的镜像。有人会问Go本身不就是静态编译吗?请接着往下看。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("http://baidu.com")
    check(err)
    body, err := ioutil.ReadAll(resp.Body)
    check(err)
    fmt.Println(len(body))
}

func check(err error) {
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

这个程序很简单,就是访问一个网页,然后打印body的大小。

构建docker镜像

使用golang:onbuild镜像

我们先简单介绍一下golang:onbuild镜像以及如何使用它。这个镜像是golang官方提供的一个用于编译运行go程序的镜像,它包含了很多ONBUILD触发器,可以涵盖大多数Go程序。其实它就是预置了一些自动执行的命令:

COPY . /go/src/app
RUN go get -d -v
RNU go install -vCOPY . /go/src/app

它使用起来很简单:创建一个目录,把你的go文件放到该目录。然后增加Dockerfile文件,内容就一行:

FROM golang:onbuild

然后执行docker build命令构建镜像。这个golang:onbuild镜像就会自动将这个目录下的所有文件拷贝过去,并编译安装。比如对于本文的例子,我创建了一个app-onbuild目录,里面是Dockerfile和app.go文件:

➜  app-onbuild ll
total 8.0K
-rw-r--r-- 1 Allan  20 Jan  2 12:21 Dockerfile
-rw-r--r-- 1 Allan 291 Jan  2 12:20 app.go
➜  app-onbuild cat Dockerfile
FROM golang:onbuild

然后我执行编译镜像的命令docker build -t app-onbuild .,整个过程如下:

➜  app docker build -t app-onbuild .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM golang:onbuild
onbuild: Pulling from library/golang

75a822cd7888: Already exists
57de64c72267: Pull complete
4306be1e8943: Pull complete
4ba540d49835: Pull complete
5b23b80eb526: Pull complete
981c210a3af4: Pull complete
73f7f7662eed: Pull complete
520a90f1995e: Pull complete
Digest: sha256:d3cbc855152e8672412fc32d7f19371816d686b0dfddedb8fce86245910b31ac
Status: Downloaded newer image for golang:onbuild
# Executing 3 build triggers...
Step 1 : COPY . /go/src/app
Step 1 : RUN go-wrapper download
 ---> Running in 4d183d7e1e8a
+ exec go get -v -d
Step 1 : RUN go-wrapper install
 ---> Running in 31b6371f1a4f
+ exec go install -v
app
 ---> 94cc8fb334ea
Removing intermediate container f13df1977590
Removing intermediate container 4d183d7e1e8a
Removing intermediate container 31b6371f1a4f
Successfully built 94cc8fb334ea

我们可以看到整个过程如前面所述。编译完以后,golang:onbuild镜像默认还包含CMD ["app"]执行来运行编译出来的镜像。当然如果你的程序有参数,我们可以在启动的时候加命令行参数。

最后让我们来看看golang:onbuild的Dockerfile吧(这里以目前最新的1.8为例):

FROM golang:1.8
RUN mkdir -p /go/src/app
WORKDIR /go/src/app
# this will ideally be built by the ONBUILD below ;)
CMD ["go-wrapper", "run"]
ONBUILD COPY . /go/src/app
ONBUILD RUN go-wrapper download
ONBUILD RUN go-wrapper install

我们可以看到其实golang:onbuild镜像其实引用的还是golang标准镜像,只不过封装了一些自动执行的动作,使用使用起来更加方便而已。接下里我们看看如何直接基于标准的golang镜像来构建我们自己的镜像。

使用golang:latest镜像

相比于使用golang:onbuild的便利性,golang:latest给了我们更多的灵活性。我们以构建app镜像为例:创建app-golang目录,目录内容如下所示:

➜  app-golang ll
total 8.0K
-rw-r--r-- 1 Allan 101 Jan  2 12:32 Dockerfile
-rw-r--r-- 1 Allan 291 Jan  2 12:32 app.go
➜  app-golang cat Dockerfile
FROM golang:latest

RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN go build -o main .
CMD ["/app/main"]

执行构建命令docker build -t app-golang .

➜  app-golang docker build -t app-golang .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM golang:latest
latest: Pulling from library/golang
75a822cd7888: Already exists
57de64c72267: Already exists
4306be1e8943: Already exists
4ba540d49835: Already exists
5b23b80eb526: Already exists
981c210a3af4: Already exists
73f7f7662eed: Already exists
Digest: sha256:5787421a0314390ca8da11b26885502b58837ebdffda0f557521790c13ddb55f
Status: Downloaded newer image for golang:latest
 ---> 6639f812dbc7
Step 2 : RUN mkdir /app
 ---> Running in a6f105ecc042
 ---> f73030d40507
Removing intermediate container a6f105ecc042
Step 3 : ADD . /app/
 ---> 3fcc194ce29d
Removing intermediate container 013c2192f90e
Step 4 : WORKDIR /app
 ---> Running in b8a2ca8d7ae0
 ---> 853dfe15c6cd
Removing intermediate container b8a2ca8d7ae0
Step 5 : RUN go build -o main .
 ---> Running in e0de5c273d7b
 ---> 28ef112e8c23
Removing intermediate container e0de5c273d7b
Step 6 : CMD /app/main
 ---> Running in 82c67389d9ab
 ---> 139ad10f61dc
Removing intermediate container 82c67389d9ab
Successfully built 139ad10f61dc

其实golang标准镜像还有一个非常方便的用途。比如我们需要开发go应用,但是又不想安装go环境或者还没有安装,那么我们可以直接使用golang标准镜像来在容器里面编译go程序。假设当前目录是我们的工程目录,那么我们可以使用如下命令来编译我们的go工程:

$ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest go build -v

这条命令的含义是把当前目录作为一个卷挂载到golang:latest镜像的/usr/src/myapp目录下,并将该目录设置为工作目录,然后在该目录下执行go build -v,这样就会在该目录下编译出一个名为myapp的可执行文件。当然默认编译出的是linux/amd64架构的二进制文件,如果我们需要编译其他系统架构的文件,可以加上相应的参数,比如我们要编译Windows下的32位的二进制文件,可执行如下命令:

$ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp -e GOOS=windows -e GOARCH=386 golang:latest go build -v

当然,我们也可以shell脚本一次编译出多种OS下的文件:

$ docker run --rm -it -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest bash
$ for GOOS in darwin linux windows; do
> for GOARCH in 386 amd64; do
> go build -v -o myapp-$GOOS-$GOARCH
> done
> done

OK,言归正传,我们继续来进行本文要讨论的话题。我们看一下上述两种方式编译出来的镜像的大小:

➜  app-golang docker images | grep -e golang -e app
app-golang                                      latest              139ad10f61dc        36 minutes ago      679.3 MB
app-onbuild                                     latest              94cc8fb334ea        39 minutes ago      679.3 MB
golang                                          onbuild             a422f764b58c        2 weeks ago         674 MB
golang                                          latest              6639f812dbc7        2 weeks ago         674 MB

可以看到,golang-onbuildgolang-latest两个基础镜像大小都为647MB,而我们编译出来的自己的镜像大小为679.3MB,也就是说我们自己的程序其实只有5.3MB。这是为什么呢?因为我们使用的两个基础镜像是通用镜像,他们包含了go依赖的所有东西。比如我们看一下golang-1.8的Dockerfile:

FROM buildpack-deps:jessie-scm
# gcc for cgo
RUN apt-get update && apt-get install -y --no-install-recommends \
        g++ \
        gcc \
        libc6-dev \
        make \
        pkg-config \
    && rm -rf /var/lib/apt/lists/*
ENV GOLANG_VERSION 1.8beta2
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
ENV GOLANG_DOWNLOAD_SHA256 4cb9bfb0e82d665871b84070929d6eeb4d51af6bedbc8fdd3df5766e937ef84c
RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
    && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
    && tar -C /usr/local -xzf golang.tar.gz \
    && rm golang.tar.gz
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
WORKDIR $GOPATH
COPY go-wrapper /usr/local/bin/

这样我们就不奇怪了,因为基础镜像里面包含了太多的东西,但其实我们的程序只使用了极少的一部分东西。下面我们就介绍一种方法可以只编译我们程序依赖的东西,这样编译出来的镜像非常的小。

其实在生产环境中,对于Go应用我们往往是现在本地编译出一个可执行文件,然后将这个文件打进容器里面,而不是在容器里面进行编译,这样可以做到容器里面只有我们需要的东西,而不会引入一些只在编译过程中需要的文件。这种方式一般也有两种操作方法:第一种就是利用go build编译出二进制文件,然后在Dockerfile里面引入一个操作系统镜像作为程序运行环境。这样打出的镜像也比较大,但是却不会有编译过程中依赖的一些文件,所以相比较之前的方式,这种方式打出来的镜像也会小很多。在镜像大小不是特别重要的场景下,我比较喜欢这种方式,因为基础的操作系统往往带了一些基础的命令,如果我们的程序有些异常什么的,我们可以登录到这些容器里面去查看。比如可以使用psnetstat等命令。但如果没有操作系统的话,这些命令就都没法使用了。当然,本文讨论的就是如何构建最小的镜像,所以接下来我们介绍第二种操作方法:利用scratch镜像构建最小的Go程序镜像。

scratch镜像其实是一个特殊的镜像,为什么特殊呢?因为它是一个空镜像。但是它却是非常重要的。我们知道Dockerfile文件必须以FROM开头,但如果我们的镜像真的是不依赖任何其他东西的时候,我们就可以FROM scratch。在Docker 1.5.0之后,FROM scratch已经变成一个空操作(no-op),也就是说它不会再单独占一层了。

OK,下面我们来进行具体的操作。我们先创建一个go-scratch目录,然后将先go build编译出来一个二进制文件拷贝到这个目录,并增加Dockerfile文件:

➜  app-scratch ll
total 5.1M
-rw-r--r-- 1 Allan   36 Jan  2 13:44 Dockerfile
-rwxr-xr-x 1 Allan 5.1M Jan  2 13:42 app
➜  app-scratch cat Dockerfile
FROM scratch
ADD app /
CMD ["/app"]

然后打镜像:

➜  app-scratch docker build -t app-scratch .
Sending build context to Docker daemon 5.281 MB
Step 1 : FROM scratch
 --->
Step 2 : ADD app /
 ---> 65d4b96cf3a3
Removing intermediate container 2a6498e02c75
Step 3 : CMD /app
 ---> Running in c8f2958f09e2
 ---> dcd05e331135
Removing intermediate container c8f2958f09e2
Successfully built dcd05e331135

可以看到,FROM scratch并没有单独占一层。然后我们运行构建出来的镜像:

➜  app-scratch docker images app-scratch
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
app-scratch         latest              7ef9c5620b4f        5 minutes ago       5.259 MB

只有5.259MB,和之前相比是不是超级小呢?

NB(不是牛逼,是nota bene):

我们知道Go的编译是静态的,但是如果你的Go版本是1.5之前的,那你编译你的程序时最好使用如下命令去编译:

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

因为Go 1.5之前Go依赖了一些C的一些库,Go编译的时候是动态链接这些库的。这样你使用scratch的方式打出来的镜像在运行时会因为找不到这些库而报错。CGO_ENABLED=0表示静态编译cgo,里面的GOOS改为你程序运行的目标机器的系统,-a的意思是重新编译所有包。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK