不可思議的快!加速 Docker 中構建 Golang 應用
這些天我在工作中正在進行一個 GoLang 項目。這與我們通常使用的 Java 和 Spring Boot 應用程序有很大不同, 感覺很不錯:)。
和我們所有的其他組件一樣, 這個 GoLang 項目也需要被封裝在一個容器中, 才能在 Kubernetes 集羣中執行。所以我編寫了一個 Dockerfile
:
# 構建階段
FROM golang:1.22.1-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .
# 最終階段
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.3
WORKDIR /
COPY --from=build /app/app /
CMD ["./app"]
我對此很滿意。這是一個多階段構建, 因此最終鏡像只包含編譯後的應用程序, 不需要 Go。
使用 Docker buildx, 我可以爲 amd64
和 arm64
架構生成 Docker 鏡像。
docker buildx create --name builder --driver docker-container --bootstrap
docker buildx use builder
docker buildx build --platform linux/amd64,linux/arm64 -t ${CONTAINER_REGISTRY}/${CONTAINER_IMAGE_NAME}:latest --push .
在我的 M1 Mac 上, 鏡像在一分鐘內生成。不算快, 但也可以接受。
然後是在 GitLab 管道中運行它。在那裏它花了超過 30 分鐘!
GoLang 構建需要很長時間
這很奇怪。爲什麼構建需要這麼長時間? 深入研究, 發現是 go build
命令花費了大部分時間。 amd64
的構建相對較快 (3 分鐘), 而 arm64
則花了近 30 分鐘! 注意, 管道是在 AMD 機器上運行的。
#21 [linux/amd64 build 6/6] RUN CGO_ENABLED=0 GOOS=linux go build -a -o app .
#21 DONE 179.8s
#25 [linux/arm64 build 6/6] RUN CGO_ENABLED=0 GOOS=linux go build -a -o app .
#25 DONE 1793.4s
這是怎麼回事?
優化 #1 — 多架構構建: 模擬 vs 交叉編譯
經過一番搜索, 我發現了這篇 Docker 博客。它解釋了構建多架構 Docker 的兩種不同方式。默認情況下, 當構建與當前機器架構不同的鏡像時, 會使用軟件模擬器。這總是比原生執行慢。
我們能運行原生執行嗎? 是的, 通過交叉編譯方法, 可以通過將 FROM
指令更新爲 FROM--platform=$BUILDPLATFORM golang:1.22.1-alpine AS build
來實現。這將導致構建階段始終使用底層機器的架構, 即原生。
但是, 這會導致指令 RUN CGO_ENABLED=0GOOS=linux go build-o app.
失敗, 因爲它沒有指定目標架構。這可以通過使用內置參數 TARGETOS
和 TARGETARCH
來解決。
這是新的 Dockerfile
(更新的行已高亮):
# 構建階段
FROM --platform=$BUILDPLATFORM golang:1.22.1-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -o app .
# 最終階段
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.3
WORKDIR /
COPY --from=build /app/app /
CMD ["./app"]
現在, 構建在 4 分鐘內完成!
顯著提高了構建時間!
go build
命令只用了 1.5 分鐘左右完成兩個架構。太棒了!
#17 [linux/amd64 build 6/6] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o app .
#17 DONE 97.6s
#18 [linux/amd64->arm64 build 6/6] RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -o app .
#18 DONE 97.6s
注意上面的 linux/amd64->arm64
表示交叉編譯。
有了 20 倍的提升, 我們可以到此爲止了。但還有一個優化可以做。
優化 #2 — 利用 Go 構建緩存
每次在 docker buildx 環境中運行 go mod download
和 go build
時, 都會下載依賴包。這顯然是低效的。我們如何在 docker buildx 環境中啓用訪問 Go 緩存和之前下載的包?
在我的 M1 Mac 上, Go 將下載的包存儲在 $HOME/go/pkg/mod
中, 緩存在 $HOME/Library/Cache/go-build
中。你可以在自己的環境中運行以下命令找到路徑。
❯ go env GOCACHE GOMODCACHE GOPATH
/Users/abhinavsonkar/Library/Caches/go-build
/Users/abhinavsonkar/go/pkg/mod
/Users/abhinavsonkar/go
如果我們可以讓 docker buildx 訪問這些路徑, 那麼應該會加快構建速度。這可以通過 docker 掛載來實現。
更新後的 Dockerfile
(更新的行已高亮):
# 構建階段
FROM --platform=$BUILDPLATFORM golang:1.22.1-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
ARG GOMODCACHE GOCACHE
RUN --mount=type=cache,target="$GOMODCACHE" go mod download
ARG TARGETOS TARGETARCH
COPY . .
RUN --mount=type=cache,target="$GOMODCACHE" \
--mount=type=cache,target="$GOCACHE" \
CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" go build -o /out/app .
# 最終階段
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.3
WORKDIR /
COPY --from=build /app/app /
CMD ["./app"]
以及用於傳遞正確路徑的 docker buildx 命令:
GOCACHE=${HOME}/Library/Caches/go-build
GOMODCACHE=${HOME}/go/pkg/mod
docker buildx create --name builder --driver docker-container --bootstrap
docker buildx use builder
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg GOCACHE=${GOCACHE} \
--build-arg GOMODCACHE=${GOMODCACHE} \
-t ${CONTAINER_REGISTRY}/${CONTAINER_IMAGE_NAME}:latest \
--push \
.
這在 M1 Mac 上啓用了重用 Go 緩存和包。但要在 GitLab 管道中使用, 稍微有點棘手。
GitLab 支持跨管道緩存, 使用 cache
關鍵字。但只有在檢出的項目下的文件夾才能被緩存。所以我們需要先修改 GOPATH
、 GOCACHE
和 GOMODCACHE
爲項目下的路徑。在 GitLab 管道中, 項目目錄可通過內置環境變量 $CI_PROJECT_DIR
訪問。
以下是一個 GitLab 作業片段, 可以正確利用 Go 緩存。編譯:
compile:
services:
- docker:25.0.4-dind-alpine3.19
variables: # (1)
GOPATH: "$CI_PROJECT_DIR/.go"
GOCACHE: "$CI_PROJECT_DIR/.cache/go-build"
GOMODCACHE: "$GOPATH/pkg/mod"
before_script:
- mkdir -p .go/pkg/mod .cache/go-build
stage: build
cache: # (2)
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .go/pkg/mod/
- .cache/go-build/
script: # (3)
- |
docker buildx create --name builder --driver docker-container --bootstrap
docker buildx use builder
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg GOCACHE=${GOCACHE} \
--build-arg GOMODCACHE=${GOMODCACHE} \
-t ${CONTAINER_REGISTRY}/${CONTAINER_IMAGE_NAME}:latest \
--push \
.
-
Go 的環境變量被設置爲項目內部的目錄。
-
GitLab 緩存被定義爲選定的路徑。鍵
$CI_COMMIT_REF_SLUG
將爲每個分支維護一個唯一的緩存。 -
Docker buildx 命令與在 M1 Mac 上本地使用的命令相同。
通過上述更改, arm64
的 go build
命令完成時間縮短至 15 秒!
#21 [linux/amd64->arm64 build 6/6] RUN --mount=type=cache,target="/builds/project/.go/pkg/mod" --mount=type=cache,target="/builds/project/.cache/go-build" CGO_ENABLED=0 GOOS="linux" GOARCH="arm64" go build -o /out/app .
#21 DONE 14.5s
#22 [linux/amd64 build 6/6] RUN --mount=type=cache,target="/builds/project/.go/pkg/mod" --mount=type=cache,target="/builds/project/.cache/go-build" CGO_ENABLED=0 GOOS="linux" GOARCH="amd64" go build -o /out/app .
#22 DONE 5.4s
從 30 分鐘到 15 秒, 性能提升了 120 倍! 太棒了, 對嗎?
額外 - 利用 Docker 緩存
我使用的 Dockerfile 有一些其他指令來下載 RPM 包和創建用戶。每次管道運行時, 這些指令都會執行。但是一個簡單的技巧在這裏描述將重複使用 Docker 緩存, 並跳過每次下載。
docker pull ${CONTAINER_REGISTRY}/${CONTAINER_IMAGE_NAME}:latest || true
docker buildx create --name builder --driver docker-container --bootstrap
docker buildx use builder
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg GOCACHE=${GOCACHE} \
--build-arg GOMODCACHE=${GOMODCACHE} \
-t ${CONTAINER_REGISTRY}/${CONTAINER_IMAGE_NAME}:latest \
--push \
.
這個技巧是先拉取我們打算構建的 Docker 鏡像 (即使鏡像不存在也不會失敗)。這將重複使用未更改的 Docker 緩存層。
最初, 管道的構建階段需要 31 分鐘。經過所有優化和上述 Docker 緩存之後, 構建階段在 52 秒內完成。
結論
我正在學習 GoLang, 這些改進給我帶來了巨大的滿足感。你是否正在爲你的 GoLang 應用程序運行管道? 那麼請檢查你的管道是否可以從本博客中描述的改進中獲益。如果你有其他建議, 請告訴我!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/VdQ2b-6KyZvfq3UEpzTvgg