使用Docker进行golang工程应用编译

一、准备

考虑如下的golang工程,包含一个Client和一个Server端,其中:

  • 客户端是用于编写、发送和 接收消息。
  • 服务器接收来自客户端的消息,翻译它们, 并将它们发送回客户端

源码

├── Dockerfile
├── cmd
│   ├── client
│   │   ├── main.go
│   │   ├── request.go
│   │   └── ui.go
│   └── server
│       ├── main.go
│       └── translate.go
├── go.mod
└── go.sum

Dockerfile

# syntax=docker/dockerfile:1
FROM golang:1.20-alpine  # 使用golang官方镜像的版本
WORKDIR /src             # 创建并指定工作目录
COPY . .               # 复制文件到工作目录
RUN go mod download      # 下载依赖
RUN go build -o /bin/client ./cmd/client    # 编译client
RUN go build -o /bin/server ./cmd/server    # 编译server
ENTRYPOINT [ "/bin/server" ]      # 启动server

其中:

  1. # syntax=docker/dockerfile:1
    此注释是 Dockerfile 解析器指令。 它指定要使用的 Dockerfile 语法版本。此文件使用 最佳实践的语法:它确保您拥有访问最新的 Docker 构建功能。可以参考此文了解这个解析器的功能:Custom Dockerfile syntax | Docker Documentation

编译

docker build  -t test:v1 .
build 镜像

二、Layer

dockerfile的每一条指令都会被记录为一个镜像层。通常情况下,docker引擎使用文件系统Unionfs来存储这些image layer。

From: http://docs.docker.com

运行build构建时,构建器会尝试重用早期构建中的层。 如果映像的某个layer未更改,则构建器将从构建缓存中选取该层。 如果自上次构建以来某个层发生了更改,则必须重新构建该层以及随后的所有层

在企业的内部流水线中,可以使用docker镜像层缓存的特性,来加速流水线的构建速度。

三、多级构建(Multi-Stage)

在上述案例中,我们把整个golang环境、源码、依赖都打包到了 test:v1 这个镜像。当build结束时,我们发现这个镜像的大小达到了415MB

镜像大小

臃肿的镜像会为构建效率、制品分发和环境资源带来不少的麻烦,并且存在源码泄露的风险。在实际的使用中,通常使用Docker的多级构建(Multi-Stage)特性,来解决上述问题。

添加Stage

使用多级构建(Multi-Stage),可以选择为构建和运行时环境使用不同的基本映像,将打包结果从Build Stage复制到Runtime Stage。

Dockerfile可以改为:

# Build Stage
# syntax=docker/dockerfile:1
FROM golang:1.20-alpine  
WORKDIR /src            
COPY . .                
RUN go env -w GOPROXY=https://goproxy.cn,direct  
RUN go mod download    
RUN go build -o /bin/client ./cmd/client   
RUN go build -o /bin/server ./cmd/server    

# 运行时 Stage
FROM scratch
COPY --from=0 /bin/client /bin/server /bin/
ENTRYPOINT [ "/bin/server" ]

通过上述dockerfile构建,可以看到 test:v2 镜像大小显著减小至 15.8MB

并行化构建(Parallelism)

通过多级构建,已经达到了减小镜像大小的目的。但是我们分析Dockerfile,发现在Build Stage中,client和server是串行执行的,这使得镜像构建时,本来就漫长的构建环节雪上加霜(不夸张哈,虽然这个demo挺快的,真实生产环境可不一定,CICD环境流程每提升1s都能节约不少资源)。

所以,我们可以进一步去优化编译的过程,使其并行化执行。

结合第二章的多级构建,Dockerfile可以改为:

# syntax=docker/dockerfile:1
FROM golang:1.20-alpine  AS base
WORKDIR /src            
COPY . .       
RUN go env -w GOPROXY=https://goproxy.cn,direct  
RUN go mod download -x

FROM base AS build-client
RUN go build -o /bin/client ./cmd/client

FROM base AS build-server
RUN go build -o /bin/server ./cmd/server

FROM scratch AS client
COPY --from=build-client /bin/client /bin/
COPY --from=build-server /bin/server /bin/
ENTRYPOINT [ "/bin/server" ]
并行化构建

可以看到,编译client和server步骤合并为一个,且同时运行

分目标(Target)构建

尽管已经通过多级构建和并行化来提升效率,同时降低镜像体积。但是,镜像还存在一个问题,client和server在同一个镜像内。在生产情况下,client和server需要分别部署,所以上面的流程明显是不合理的。需要将target拆分开来。

Dockerfile可以改为:

# syntax=docker/dockerfile:1
FROM golang:1.20-alpine  AS base
WORKDIR /src            
COPY . .       
RUN go env -w GOPROXY=https://goproxy.cn,direct  
RUN go mod download -x

FROM base AS build-client
RUN go build -o /bin/client ./cmd/client

FROM base AS build-server
RUN go build -o /bin/server ./cmd/server

FROM scratch AS client
COPY --from=build-client /bin/client /bin/
ENTRYPOINT [ "/bin/client" ]

FROM scratch AS server
COPY --from=build-server /bin/server /bin/
ENTRYPOINT [ "/bin/server" ]

构建命令:

# 构建client
docker build --tag=client:v1  --target=client .

# 构建server
docker build --tag=server:v1  --target=server . 
client和server分开构建

拆分后,镜像大小进一步降低。

四、本文总结

本文通过介绍Layer,一步步探索多级构建(Multi-Stage)的各种特性,实现对Docker的构建效率、制品体积的有效控制。

THE END