可能是吃了细糠后吃不了糙米,以至于后来看到关于镜像瘦身之类的文档都是嗤之以鼻。千篇一律,还在用以前的多阶段构建,连缓存加速都没有,而且关于Golang
的没有一个完整介绍cgo Dockerfile
怎么写的。
本文将介绍go
语言Docker
打包时镜像瘦身和Dockerfile
优化,将镜像构建提速(提速至20秒,如果按**的对比方式,提速90倍)。主要介绍使用了CGO的项目打包时Dockerfile
的编写并提供一些新的思路。为后续介绍Golang调用Oracle打包
做准备。
由于多阶段已经非常普及,本文不再过多介绍,若有不会的可参考上述好未来go-zero 微服务实践
文章。本文只介绍些与其他文章不一样的。先看下本公众号关联小程序(公众号菜单可以体验)后端服务的Dockerfile
ARG GO_VERSION=1.23.0
ARG ALPINE_VERSION=3.20
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build
WORKDIR /src
ENV GOPROXY https://goproxy.cn,direct
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download -x
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,target=. \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/server ./cmd/ha/main.go
FROM alpine:${ALPINE_VERSION}
COPY --from=build /bin/server /bin/
EXPOSE 9680
ENTRYPOINT [ "/bin/server" ]
可以看到整体很简洁,其中1,2,13,16
行还可以省略。与大多数文档不同,这里的第一阶段构建Golang二进制程序并没有使用copy代码到golang镜像,然后在镜像中下载依赖再打包的方式。这里使用缓存挂载,将依赖缓存起来,避免每次打包都下载依赖,而源代码也没有进行copy操作。前两行定义了Golang
和alpine
的版本,方便Golang
版本升级时进行修改。
具体妙用可查看之前的文章 天行1st,公众号:编码如写诗基于Docker的交叉编译和打包多平台镜像
通过查看build截图,前两个构建记录为普通项目,后两个记录为cgo项目。两个项目都是20多秒构建完成,已经非常迅速。再查看log记录可以看到耗时主要为go build,其他copy和go mod耗时非常少
接下来开始介绍CGO应用的打包,CGO由于Golang
中调用了C的代码,so库等,导致编译时需要有gcc环境,而且最终的二进制程序可能也需要有依赖的静态库,所以相比较于普通应用,CGO的打包要麻烦很多。
大概2年半以前第一次写CGO程序时,遇到Docker打包问题,当时在网上翻了好多博客最终发现在米开朗基杨
的文章里找到蛛丝马迹,今天写文章回看才发现,竟然是米开朗基杨
大神写的,膜拜。
https://cloud.tencent.com/developer/article/1632733
当时的难点主要在于使用golang
基础镜像构建出了二进制可执行程序mck
后,将其放入alpine
镜像后一直无法运行,后来在alpine容器中通过ldd mck
命令发现缺少so库。找到了问题就好解决了,于是在alpine
系统中,将C的代码重新编译后,打出alpine
的so,后面直接将so放入alpine
镜像中就可以了。
# 多阶段构建
#构建一个 builder 镜像,目的是在其中编译出可执行文件
#构建时需要将此文件放到代码根目录下
FROM golang:alpine as builder
ENV GOOS=linux
ENV GOPROXY=https://goproxy.cn
#安装编译需要的环境gcc等
RUN apk add build-base
WORKDIR /build
#将上层整个文件夹拷贝到/build
ADD . /build/src
WORKDIR /build/src
#交叉编译,需要制定CGO_ENABLED=1,默认是关闭的
#去掉了调试信息 -ldflags="-s -w" 以减小镜像尺寸
RUN GOOS=linux CGO_ENABLED=1 GOARCH=amd64 go build -ldflags="-s -w" -installsuffix cgo -o mck ./cmd/mck/main.go
#编译
FROM alpine
RUN apk update --no-cache && apk add --no-cache tzdata
#设置本地时区,这样我们在日志里看到的是北京时间了
ENV TZ Asia/Shanghai
WORKDIR /app
#从第一个镜像里 copy 出来可执行文件
COPY --from=builder /build/src/mck /app/mck
COPY ./config/alpine/libgcc_s.so.1 /usr/lib/
COPY ./config/alpine/libstdc++.so.6.0.28 /usr/lib/libstdc++.so.6.0.28
RUN ln -s /usr/lib/libstdc++.so.6.0.28 /usr/lib/libstdc++.so.6
#VOLUME ["/home/tianxing/project/mck/mck-service-core/config/config.yml","/app/config/config.yml"]
#CMD ["./mck"]
EXPOSE 9008
EXPOSE 9080
# 构建镜像:docker build -t mckserver .
# 运行容器:docker run -itd --name mckserver -v /app/mck/mck.log:/app/mck.log -p 9008:9008 -p 9080:9080 mckserver
搞定,打包后不到30M。
root@DESKTOP-BB0KRFQ:/mnt/e# docker images | grep mck-s
mck-server v3.0.0 764c83bc959d 5 days ago 29.1MB
mck-server 2.15.0 e060e64c206e 7 months ago 27.3MB
然而同事吐槽说,每次打包太慢了而且在他的vm虚拟机,打几次之后磁盘满了。ps:因为每次基础镜像都需要下载安装gcc,即使后来配置了阿里云的加速依然很慢,而且每次打包都会产生一个dangling
镜像,大概1个G,他的虚拟机一共50G,确实很容易满。
既然基础镜像需要gcc,那就把带gcc的golang
基础镜像重新做一个cgo-mck
镜像不就可以了嘛,于是进行修改。
# 多阶段构建
#构建一个 builder 镜像,目的是在其中编译出可执行文件mck
#构建时需要将此文件放到代码根目录下
FROM cgo-mck:mck as builder
ENV GOOS=linux
ENV GOPROXY=https://goproxy.cn,direct
#安装编译需要的环境gcc等,使用阿里云加速
#RUN apk add --repository https://mirrors.aliyun.com/alpine/v3.18/main build-base
#安装编译需要的环境gcc等
#RUN apk add build-base
#WORKDIR /build
#将上层整个文件夹拷贝到/build
ADD . /build/src
WORKDIR /build/src
#交叉编译,需要制定CGO_ENABLED=1,默认是关闭的
#去掉了调试信息 -ldflags="-s -w" 以减小镜像尺寸
RUN go env -w GO111MODULE=on \
&& go mod tidy \
&& go env -w CGO_ENABLED=1 \
&& go build -ldflags="-s -w" -o mck ./cmd/mck/main.go
#编译
FROM alpine:mck
#RUN apk update --no-cache && apk add --no-cache tzdata
#设置本地时区,这样我们在日志里看到的是北京时间了
#ENV TZ Asia/Shanghai
WORKDIR /app
#从第一个镜像里 copy 出来可执行文件
COPY --from=builder /build/src/mck /app/mck
COPY ./config/alpine/libgcc_s.so.1 /usr/lib/
COPY ./config/alpine/libstdc++.so.6.0.28 /usr/lib/libstdc++.so.6.0.28
RUN ln -s /usr/lib/libstdc++.so.6.0.28 /usr/lib/libstdc++.so.6
#VOLUME ["/home/tianxing/project/mck/mck-service-core/config/config.yml","/app/config/config.yml"]
#CMD ["./mck"]
EXPOSE 9008
EXPOSE 9080
# 构建镜像:docker build -t mckserver .
# 运行容器:docker run -itd --name mckserver2.4 -v /app/mck/config:/app/config -p 19008:9008 -p 19080:9080 mckserver:v2.4.0
# 导出镜像:docker save -o mck_server.tar mckserver:latest
修改后,打包终于变快了,一般不到2分钟就可以了。然后关于dangling
镜像的,使用命令:docker image prune
就可以清除了,可以将该命令加入到Makefile
或者自动打包的CI脚本中。
若有需要cgo镜像的也可以直接拉取:docker pull gjing1st/cgo:1.22.0-alpine3.19
# 多阶段构建
#构建一个 builder 镜像,目的是在其中编译出可执行文件
#构建时需要将此文件放到代码根目录下
FROM cgo-mck:mck as builder
ENV GOOS=linux
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /build/src
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download -x
#交叉编译,需要制定CGO_ENABLED=1
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,target=. \
CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o /bin/mck ./cmd/mck/main.go
#编译
FROM alpine:mck
WORKDIR /app
#从第一个镜像里 copy 出来可执行文件
COPY --from=builder /bin/mck /app/mck
COPY ./config/alpine/libgcc_s.so.1 /usr/lib/
COPY ./config/alpine/libstdc++.so.6.0.28 /usr/lib/libstdc++.so.6.0.28
RUN ln -s /usr/lib/libstdc++.so.6.0.28 /usr/lib/libstdc++.so.6
CMD ["./mck"]
EXPOSE 9008
EXPOSE 9080
构建测试,总耗时:19.1s
对比以前的将近半个小时现在不到20秒,已经可以用极速构建来形容了。
PS:该项目包含TCP
和HTTP
服务,并非微服务架构,为单一应用,包含mysql,redis,prometheus
相关操作等众多功能。
查看构建过程,也是主要耗时在go build
其他耗时都非常少,另外alpine so
有些许耗时,由于较少,这里未在进行优化,若想要继续优化可关注后续文章介绍:Golang调用oracle镜像打包
在使用CG0进行Golang
项目打包时,优化镜像构建的过程至关重要。通过多阶段构建,我们可以有效地减少最终镜像的大小,同时提高构建速度。
在构建过程中,利用Docker
的缓存机制,可以将依赖项的下载和编译过程进行缓存,避免重复操作。通过将C语言的动态库直接复制到最终镜像中,确保应用能够正常运行,避免因缺少依赖而导致的运行时错误。
此外,定期清理无用的dangling
镜像,保持Docker
环境的整洁避免磁盘空间被占满。可以将清理命令集成到自动化构建流程中,确保每次构建后环境的干净。
最终,通过这些优化措施,我们不仅能构建出小巧的镜像,还能提升开发效率,为后续的服务部署打下良好的基础。希望这些经验能为其他开发者在Golang
项目的Docker
化过程中提供帮助。