前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Golang】CGO项目Docker镜像打包优化指南,来点新鲜不一样的多阶段构建

【Golang】CGO项目Docker镜像打包优化指南,来点新鲜不一样的多阶段构建

作者头像
编码如写诗
发布2025-01-08 12:43:42
发布2025-01-08 12:43:42
1090
举报
文章被收录于专栏:编码如写诗

可能是吃了细糠后吃不了糙米,以至于后来看到关于镜像瘦身之类的文档都是嗤之以鼻。千篇一律,还在用以前的多阶段构建,连缓存加速都没有,而且关于Golang的没有一个完整介绍cgo Dockerfile怎么写的。

目的

本文将介绍go语言Docker打包时镜像瘦身和Dockerfile优化,将镜像构建提速(提速至20秒,如果按**的对比方式,提速90倍)。主要介绍使用了CGO的项目打包时Dockerfile的编写并提供一些新的思路。为后续介绍Golang调用Oracle打包做准备。

普通Go应用打包

由于多阶段已经非常普及,本文不再过多介绍,若有不会的可参考上述好未来go-zero 微服务实践文章。本文只介绍些与其他文章不一样的。先看下本公众号关联小程序(公众号菜单可以体验)后端服务的Dockerfile

代码语言:javascript
复制
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操作。前两行定义了Golangalpine的版本,方便Golang版本升级时进行修改。

具体妙用可查看之前的文章 天行1st,公众号:编码如写诗基于Docker的交叉编译和打包多平台镜像

通过查看build截图,前两个构建记录为普通项目,后两个记录为cgo项目。两个项目都是20多秒构建完成,已经非常迅速。再查看log记录可以看到耗时主要为go build,其他copy和go mod耗时非常少

CGO应用打包

接下来开始介绍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镜像中就可以了。

代码语言:javascript
复制
# 多阶段构建
#构建一个 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。

代码语言:javascript
复制
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镜像不就可以了嘛,于是进行修改。

代码语言:javascript
复制
# 多阶段构建
#构建一个 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

最终优化

代码语言:javascript
复制
# 多阶段构建
#构建一个 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:该项目包含TCPHTTP服务,并非微服务架构,为单一应用,包含mysql,redis,prometheus相关操作等众多功能。

查看构建过程,也是主要耗时在go build 其他耗时都非常少,另外alpine so有些许耗时,由于较少,这里未在进行优化,若想要继续优化可关注后续文章介绍:Golang调用oracle镜像打包

总结

在使用CG0进行Golang项目打包时,优化镜像构建的过程至关重要。通过多阶段构建,我们可以有效地减少最终镜像的大小,同时提高构建速度。

在构建过程中,利用Docker的缓存机制,可以将依赖项的下载和编译过程进行缓存,避免重复操作。通过将C语言的动态库直接复制到最终镜像中,确保应用能够正常运行,避免因缺少依赖而导致的运行时错误。

此外,定期清理无用的dangling镜像,保持Docker环境的整洁避免磁盘空间被占满。可以将清理命令集成到自动化构建流程中,确保每次构建后环境的干净。

最终,通过这些优化措施,我们不仅能构建出小巧的镜像,还能提升开发效率,为后续的服务部署打下良好的基础。希望这些经验能为其他开发者在Golang项目的Docker化过程中提供帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-01-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编码如写诗 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目的
  • 普通Go应用打包
  • CGO应用打包
    • 第一次尝试
    • 第二次优化
    • 最终优化
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档