雲計算容器:Docker image 優化

我們一般都是使用 dockerfile 來製作鏡像。但是在鏡像製作中,也有着很多的注意點,比如優化構建後的體積,還要優化構建速度,時區,證書等等。

基本使用

整個文章的說明過程都是通過對實例的操作來展示,先看實例,我們假設要構建一個 http 服務。

package main

import (
 "fmt"
 "net/http"
 "time"

 "github.com/gin-gonic/gin"
)

func main() {
 fmt.Println("Server Ready")
 router := gin.Default()
 router.GET("/", func(c *gin.Context) {
  c.String(200, "hello world, this time is: "+time.Now().Format(time.RFC1123Z))
 })
 router.GET("/github", func(c *gin.Context) {
  _, err := http.Get("https://api.github.com/")
  if err != nil {
   c.String(500, err.Error())
   return
  }
  c.String(200, "access github api ok")
 })

 if err := router.Run(":9900"); err != nil {
  panic(err)
 }
}

這裏我們可以先試一試構建後包的體積

$ go build -o server
$ ls -alh | grep server
-rwxrwxrwx 1 eyas eyas  14.6M May 29 10:26 server

14.6MB,這是一個 http 服務的 hello world,當然這是因爲使用了 gin ,所以有些大,如果用標準包 net/http 寫的 hello world,體積大概是接近 7 MB。

鏡像優化過程

分層編譯

直接看 dockerfile,關於 dockerfile 的使用這邊就不多說了。

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

說明:

好了,下面開始構建鏡像

$ docker build -t server .
...
Successfully built 8d3b91210721
Successfully tagged server:latest

到了這一步,構建成功,看看鏡像大小

$ docker images
server          latest         8d3b91210721      1 minutes ago        11MB

11MB,還行,現在運行一下

$ docker run -p 9900:9900 server
standard_init_linux.go:211: exec user process caused "no such file or directory"

發現啓動報錯了,而且 main 函數的第一行打印語句都沒有出現,所以整個程序完全沒有運行。

解決報錯

上面的錯誤原因是缺少庫依賴文件。這其實是構建的 go 程序還依賴底層的 so 庫文件,不信可以在物理機編譯後看看它的依賴

$ go build -o server
$ ldd server
        linux-vdso.so.1 (0x00007ffcfb775000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9a8dc47000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a8d856000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9a8de66000)

這是因爲 go build 是默認啓用 CGO 的,可以通過命令 go env CGO_ENABLED查看,在 CGO 開啓情況下,無論代碼有沒有用 CGO,都會有庫依賴文件,解決方法也很簡單,手動指定關閉 CGO 就行,而且包體積並不會增加,正常還會減少。

CGO_ENABLED=0 go build -o server
$ ldd server
        not a dynamic executable

修改 dockerfile

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
-RUN go build -ldflags "-s -w" -o server
+RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

改動點:go build 前加了 CGO_ENABLED=0

$ docker build -t server .
...
Successfully built a81385160e25
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /github                   --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900

正常啓動了,我們訪問一下試試,訪問之前看看當前時間

$ date
Fri May 29 13:11:28 CST 2020

$ curl http://localhost:9900
hello world, this time is: Fri, 29 May 2020 05:18:28 +0000

$ curl http://localhost:9900/github
Get "https://api.github.com/": x509: certificate signed by unknown authority

發現有問題

解決問題

解決運行環境時區與證書問題

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
+  apk add --no-cache ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server

FROM scratch as runner
+COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

在 builder 階段,安裝了 ca-certificates tzdata 兩個庫,在 runner 階段,將時區配置和根證書複製了一份

$ docker build -t server .
...
Successfully built e0825838043d
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /github                   --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900

訪問一下試試

$ date
Fri May 29 13:27:16 CST 2020

$ curl http://localhost:9900
hello world, this time is: Fri, 29 May 2020 13:27:16 +0800

$ curl http://localhost:9900/github
access github api ok

一切正常了,看看當前鏡像大小

$ docker images
server          latest         e0825838043d      9 minutes ago        11.3MB

才 11.3MB,已經很小了,但是,還可以更小,就是把構建後的包再壓縮一次

進一步減小體積

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
-  apk add --no-cache ca-certificates tzdata
+  apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server
+RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\
+ upx --best server -o _upx_server && \
+ mv -f _upx_server server

FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

在 builder 階段,安裝了 upx ,並且 go build 完成後,使用 upx 壓縮了一下,執行一下構建,你會發現這個構建時間變長了,這是因爲我給 upx 設置的參數是 --best ,也就是最大壓縮級別,這樣壓縮出來的後會儘可能的小,如果嫌慢,可以降低壓縮級別從 -1-9 ,數字越大壓縮級別越高,也越慢。我使用 --best 構建完成後看看鏡像體積。

$ docker build -t server .
...
Successfully built 80c3f3cde1f7
Successfully tagged server:latest
$ docker images
server          latest         80c3f3cde1f7      1 minutes ago        4.26MB

這下子可小了,才 4.26MB,再去試試那兩個接口,一切正常,優化到此結束,但是我們正常還是很少壓縮的,一般都是解決上面幾個問題。

最終的 Dockerfile

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
  apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\
  upx --best server -o _upx_server && \
  mv -f _upx_server server

FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

總結

要減小鏡像體積,首先多階段構建這很重要,這樣就可以把編譯環境和運行環境分開。

另外,選擇 scratch 這個鏡像其實很不明智,它雖然很小,但是它太原始了,裏面什麼工具都沒有,程序啓動後,連容器都進不去,就算進去了什麼都做不了。所以就算一昧的追求儘可能小的鏡像體積,也不建議選擇 scratch 作爲運行環境,

無論是 alpine 還是 busybox ,他們都會上述時區和證書問題,同樣按照上面方法就能解決。

轉自:

kingjcy.github.io/post/cloud/paas/base/docker/docker-image/

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ipkpoz4u0skqfLUpahOpTg