故事背景
原先,我使用 Dockerfile 编译我的 Go 应用镜像,安装了基础的工具并拷贝编译好的二进制文件的镜像,竟然有 204MB(本篇讨论未压缩的镜像,在 Registry 存储会压缩)。这么大的体积对于每次应用更新发布,特别是自建小水管 Registry 来说,压力非常大,遂寻找方法减小镜像体积,最终结果是压缩至 20MB 以内,足足去除了 90% !
下面是我一开始的 Dockerfile:
# 打包依赖阶段使用golang作为基础镜像
FROM golang:1.20-alpine as builder
WORKDIR /workspace
RUN apk update && apk add --no-cache upx && rm -rf /var/cache/apk/*
# 启用go module
ENV GO111MODULE=on GOPROXY=https://goproxy.cn,direct
RUN go install github.com/go-delve/delve/cmd/dlv@latest
ARG MODULE=backend
ARG PORT=7001
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY . .
# CGO_ENABLED禁用cgo 然后指定OS等,并go build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags "all=-N -l" -ldflags "-s -w -X 'main.GO_VERSION=$(go version)' -X 'main.BUILD_TIME=`TZ=Asia/Shanghai date "+%F %T"`'" -o entry modules/$MODULE/main.go
FROM alpine
EXPOSE $PORT
ENV TZ=Asia/Shanghai
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk --no-cache add tzdata ca-certificates libc6-compat libgcc libstdc++ curl
COPY --from=builder /go/bin/dlv /
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert
COPY --from=builder --chmod=777 /workspace/entry .
## 需要运行的命令
#ENTRYPOINT ["/dlv","--listen=:2345","--headless=true","--accept-multiclient","--api-version=2","exec","/entry"]
ENTRYPOINT ["/entry"]
编译后的体积有 204MB:
分析一下
利用 Goland 的 Services 功能,打开该镜像,点击 SHOW LAYERS
,显示该镜像每一层的信息:
对应看一下 Command 栏,可以得出每一层的原因和 Size:
Command | Size |
---|---|
Ubuntu 操作系统 | 77.8MB |
ca-certifictes curl(大部分是 apt update) | 48.8MB |
tzdata | 4.5MB |
dlv(远程 DEBUG) | 18.1MB |
根证书 | 214KB |
entry(应用) | 27.5MB |
chmod(改权限) | 27.5MB |
接下来想各种办法,对这个 Dockerfile 进行优化。
COPY 同时改权限,-27.5MB
最后一层,光是 chmod 添加运行权限便占用了一倍的应用体积,这也是 Docker 镜像的特性导致的,解决办法就是将这两层合为一层:
COPY --from=builder /workspace/entry .
RUN chmod +x /entry
COPY --from=builder --chmod=777 /workspace/entry .
去除用不到的 dlv,-18.1MB
远程 debug,目前来看用不到,果断去除,在这一行的最前面加上注释:
#COPY --from=builder /go/bin/dlv /
压缩二进制文件,-20.94MB
编译出来的文件有 27.5MB,实在太大了,首先利用编译参数 -ldflags "-s -w"
,去除符号表和调试信息,其次利用 upx upx -6 entry
直接压缩二进制文件。
其中 -6
表示压缩级别,从 1 到 9 级别越高压缩比越低,根据实际测试 -6
已经可以获得足够的空间节约,且要比 -9
快上很多。
下面是两次使用 GitHub Actions 编译的日志,首先是使用 -9
的日志 https://github.com/bellis-daemon/bellis/actions/runs/6968124423/job/18961378304
#18 [builder 9/9] RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags "all=-N -l" -ldflags "-s -w -X 'main.GoVersion=$(go version)' -X 'main.BuildTime=`date "+%F %T"`'" -o entry modules/backend/main.go && upx -9 entry
#18 28.21 Ultimate Packer for eXecutables
#18 28.21 Copyright (C) 1996 - 2023
#18 55.16 UPX 4.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 30th 2023
#18 55.16
#18 55.16 File size Ratio Format Name
#18 55.16 -------------------- ------ ----------- -----------
#18 55.16 40747008 -> 11796480 28.95% linux/amd64 entry
#18 55.16
#18 55.16 Packed 1 file.
#18 DONE 55.2s
然后是使用 -6
的日志 https://github.com/bellis-daemon/bellis/actions/runs/6990813694/job/19020694895:
#18 [builder 9/9] RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags "all=-N -l" -ldflags "-s -w -X 'main.GoVersion=$(go version)' -X 'main.BuildTime=`date "+%F %T"`'" -o entry modules/backend/main.go && upx -6 entry
#18 28.90 Ultimate Packer for eXecutables
#18 28.90 Copyright (C) 1996 - 2023
#18 31.88 UPX 4.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 30th 2023
#18 31.88
#18 31.88 File size Ratio Format Name
#18 31.88 -------------------- ------ ----------- -----------
#18 31.88 40771584 -> 12364272 30.33% linux/amd64 entry
#18 31.88
#18 31.88 Packed 1 file.
#18 DONE 32.1s
可见 -9
的压缩等级相比于 -6
压缩比仅仅从 30.33%
降低到了 28.95%
,但编译和压缩的总时间从 32.1s
上升到了 55.2s
。
经 upx 压缩的二进制文件,会在运行时脱壳解压缩后执行,据官网数据,解压处理是非常快速的,影响基本可以忽略不计。
very fast decompression: more than 500 MB/sec on any reasonably modern machine
不过,如果应用需要频繁启停,例如部署在 Serverless Functions 中,可以考虑是否需要此步骤。
# CGO_ENABLED禁用cgo 然后指定OS等,并go build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -gcflags "all=-N -l" -ldflags "-s -w -X 'main.GO_VERSION=$(go version)' -X 'main.BUILD_TIME=`TZ=Asia/Shanghai date "+%F %T"`'" -o entry modules/$MODULE/main.go
# 这里是 alpine 的 upx 安装指令,具体根据系统而定
RUN apk update && apk add --no-cache upx && rm -rf /var/cache/apk/*
# CGO_ENABLED禁用cgo 然后指定OS等,并go build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -gcflags "all=-N -l" -ldflags "-s -w -X 'main.GO_VERSION=$(go version)' -X 'main.BUILD_TIME=`TZ=Asia/Shanghai date "+%F %T"`'" -o entry modules/$MODULE/main.go \
&& upx -6 entry
使用 alpine 替换 ubuntu,-73MB
alpine 是比 ubuntu 更轻量级的系统,需要将编译镜像和目标镜像都进行更换。
FROM golang:1.20 as builder
FROM ubuntu
FROM golang:1.20-alpine as builder
FROM alpine
需要注意的是,这两个操作系统的包管理器并不一样,所以安装依赖的指令也都需要对应更改,如下一节所示。
在 alpine 下安装依赖,-46.8MB
虽然 Go 编译的二进制文件几乎不需要环境依赖,但有些还是需要安装,例如 ca-certificates,否则出站 https 请求经常会遇到 x509 报错。
RUN apt-get -qq update \
&& apt-get -qq install -y --no-install-recommends ca-certificates curl
RUN DEBIAN_FRONTEND=noninteractive TZ=Asia/Shanghai apt-get -y install tzdata
## 使用了清华源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk --no-cache add tzdata ca-certificates libc6-compat libgcc libstdc++ curl
大功告成
最终的镜像体积只有一开始的十分之一:
这让我的小水管服务器如释重负,再在 Registry 里压缩一下,只有 13MB 左右的存储体积,对对象存储也比较友好。
用了对象存储为何还会担心带宽问题?
我虽然将自建 Registry 的存储设置为阿里云对象存储,但只有 Pull 镜像可以直连对象存储,Push 镜像还是需要走 Registry 服务器中转,压力过大的话容易超时。
最终的 Dockerfile 和 Layers 信息如下所示:
# 打包依赖阶段使用golang作为基础镜像
FROM golang:1.20-alpine as builder
WORKDIR /workspace
RUN apk update && apk add --no-cache upx && rm -rf /var/cache/apk/*
# 启用go module
ENV GO111MODULE=on GOPROXY=https://goproxy.cn,direct
ARG MODULE=backend
ARG PORT=7001
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY . .
# CGO_ENABLED禁用cgo 然后指定OS等,并go build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -gcflags "all=-N -l" -ldflags "-s -w -X 'main.GO_VERSION=$(go version)' -X 'main.BUILD_TIME=`TZ=Asia/Shanghai date "+%F %T"`'" -o entry modules/$MODULE/main.go \
&& upx -6 entry
FROM alpine
EXPOSE $PORT
ENV TZ=Asia/Shanghai
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk --no-cache add tzdata ca-certificates libc6-compat libgcc libstdc++ curl
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert
COPY --from=builder --chmod=777 /workspace/entry .
ENTRYPOINT ["/entry"]