构建 Golang 应用最小 Docker 镜像

转载自:https://studygolang.com/articles/28941?fr=sidebar

示例应用

首先贴出代码例子,我们假设要构建一个 http 服务

package main

import (
"fmt"
"net/http"
"time"

"github.com/gin-gonic/gin"
)

func main() {
	fmt.Println("Server Ready")
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		c.String(200, "hello world, this time is: "+time.Now().Format(time.RFC1123Z))
	})
	router.GET("/github", func(c *gin.Context) {
		_, err := http.Get("https://api.github.com/")
		if err != nil {
			c.String(500, err.Error())
			return
		}
		c.String(200, "access github api ok")
	})

	if err := router.Run(":9900"); err != nil {
		panic(err)
	}
}

说明:

这里选择 Gin 作为例子,是为了演示我们有第三方包条件下要优化构建速度
main函数第一行打印了一行字,为了演示后面启动时遇到的一个坑
跟路由打印了时间,为了演示后面遇到的关于时区的坑
路由 github 尝试访问 https://api.github.com,为了演示后面遇到的证书坑

这里我们可以先试一试构建后包的体积

$ go build -o server
$ ls -alh | grep server
-rwxrwxrwx 1 eyas eyas  14.6M May 29 10:26 server

14.6MB,这是一个http服务的 hello world,当然这是因为使用了 gin ,所以有些大,如果用标准包 net/http 写的 hello world,体积大概是接近 7 MB

Dockerfile 的进化

版本一,初步优化

先看看第一个版本

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

说明:

选择 golang:1.14-alpine 作为编译环境,是因为这是体积最小的golang编译环境
设置 GOPROXY 是为了提升构建速度
先复制 go.mod 和 go.sum ,然后 go mod download,是为了防止每次构建都会重新下载依赖包,利用docker构建缓存提升构建速度
go build 时加上 -ldflags "-s -w" 去除构建包的调试信息,减小go构建后程序体积,大概能减小 1/4 吧
使用了多阶段构建,也就是 FROM XXX as xxx ,在构建程序包的时候,使用带编译环境的镜像去构建,运行的时候其实完全不需要go的编译环境,所以在运行阶段使用docker的空镜像 scratch 去运行。这部是减小镜像体积最有效的方法了。

好了,下面开始构建镜像

$ docker build -t server .
...
Successfully built 8d3b91210721
Successfully tagged server:latest

到了这一步,构建成功,看看镜像大小

$ docker images
server          latest         8d3b91210721      1 minutes ago        11MB

11MB,还行,现在运行一下

$ docker run -p 9900:9900 server
standard_init_linux.go:211: exec user process caused "no such file or directory"

发现启动报错了,而且main函数的第一行打印语句都没有出现,所以整个程序完全没有运行。错误原因是缺少库依赖文件。这其实是构建的 go 程序还依赖底层的 so 库文件,不信可以在物理机编译后看看它的依赖

$ go build -o server
$ ldd server
        linux-vdso.so.1 (0x00007ffcfb775000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9a8dc47000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a8d856000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9a8de66000)

这是不是跟我们的认知有点出入呢,说好无依赖的呢,结果还是有几个依赖库文件呢,虽然这几个依赖都是最底层的,一般操作系统都会有,可谁叫我们选了 scratch,这个镜像里面除了linux内核以外真的什么都没了。

这是因为go build 是默认启用 CGO 的,不信你可以试试这个命令 go env CGO_ENABLED,在 CGO 开启情况下,无论代码有没有用CGO,都会有库依赖文件,解决方法也很简单,手动指定关闭CGO就行,而且包体积并不会增加哦,还会减少呢

$ CGO_ENABLED=0 go build -o server
$ ldd server
        not a dynamic executable

版本二,解决运行时报错

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

改动点: go build 前加了 CGO_ENABLED=0

$ docker build -t server .
...
Successfully built a81385160e25
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /github                   --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900

正常启动了,我们访问一下试试,访问之前看看当前时间

$ date
Fri May 29 13:11:28 CST 2020

$ curl http://localhost:9900       
hello world, this time is: Fri, 29 May 2020 05:18:28 +0000

$ curl http://localhost:9900/github
Get "https://api.github.com/": x509: certificate signed by unknown authority

发现有问题
当前系统时间是 13:11:28 ,但是根据由显示的时间是 05:11:53,其实是docker 容器内的时区不对,默认是 0 时区,可是我们国家是 东8区
尝试访问 https://api.github.com/ 这是 https 站点,报证书错误

解决问题
在容器放置根证书
设置容器时区

版本三,解决运行环境时区与证书问题

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk add --no-cache ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

在 builder 阶段,安装了 ca-certificates tzdata 两个库,在runner阶段,将时区配置和根证书复制了一份

$ docker build -t server .
...
Successfully built e0825838043d
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /github                   --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900

访问一下试试

$ date
Fri May 29 13:27:16 CST 2020

$ curl http://localhost:9900       
hello world, this time is: Fri, 29 May 2020 13:27:16 +0800

$ curl http://localhost:9900/github
access github api ok

一切正常了,看看当前镜像大小

$ docker images
server          latest         e0825838043d      9 minutes ago        11.3MB

才 11.3MB,已经很小了,但是,还可以更小,就是把构建后的包再压缩一次

版本四,进一步减小体积

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server && upx --best server -o _upx_server && mv -f _upx_server server

FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

在 builder 阶段,安装了 upx ,并且go build 完成后,使用 upx 压缩了一下,执行一下构建,你会发现这个构建时间变长了,这是因为我给 upx 设置的参数是 --best ,也就是最大压缩级别,这样压缩出来的后会尽可能的小,如果嫌慢,可以降低压缩级别从 -1-9 ,数字越大压缩级别越高,也越慢。我使用 --best 构建完成后看看镜像体积。

$ docker build -t server .
...
Successfully built 80c3f3cde1f7
Successfully tagged server:latest
$ docker images
server          latest         80c3f3cde1f7      1 minutes ago        4.26MB

这下子可小了,才 4.26MB,再去试试那两个接口,一切正常。优化到此结束。

最终的Dockerfile (这个仅供参考,具体看文章最后的)

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
ENV GO111MODULE=on
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o server && upx --best server -o _upx_server &&  mv -f _upx_server server

FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

总结

要减小镜像体积,首先多阶段构建这很重要,这样就可以把编译环境和运行环境分开。

另外,选择 scratch 这个镜像其实很不明智,它虽然很小,但是它太原始了,里面什么工具都没有,程序启动后,连容器都进不去,就算进去了什么都做不了。所以就算一昧的追求尽可能小的镜像体积,也不建议选择 scratch 作为运行环境,我暂时只踩到小部分的坑,后面还有更多坑没踩,我也没有兴趣继续踩 scratch 的坑。

建议选择 alpine ,alpine 的镜像大小是 5.61MB 这个大小其实还是镜像解压后的大小,实际上下载镜像的时候,只需要下载 2.68 MB 。还有,上文所有我说的镜像体积,全都是指解压后的镜像体积,和实际上传下载时的体积是不一样的,docker自己会压缩一次再传输镜像

还有个很小的镜像是 busybox,它的体积是 1.22MB,下载 705.6 KB ,有大部分的linux命令可用,但是运行环境还是很原始,有兴趣可以去尝试

无论是 alpine 还是 busybox ,他们都会上述时区和证书问题,同样按照上面方法就能解决,切换到 alpine 或者 busybox 也很简单,只需要修改 runner 基础镜像就行

FROM scratch as runner 替换为 FROM busybox as runner 或者是 FROM alpine as runner
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o server && upx --best server -o _upx_server &&  mv -f _upx_server server

FROM alpine as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
posted @   哈喽哈喽111111  阅读(189)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2019-12-06 配置 Nginx 反向代理 WebSocket
2019-12-06 ES7.3.0配置
2016-12-06 CentOS7.2设置zabbix
点击右上角即可分享
微信分享提示