故事背景

原先,我使用 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"]

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注