為你的Go應用建立輕量級Docker映象?

youmen發表於2021-06-25

縮小Go二進位制檔案大小

環境

youmen@youmendeMacBook-Pro % gcc -dumpversion
12.0.5

youmen@youmendeMacBook-Pro % go version
go version go1.16.5 darwin/amd64

go build使用的是靜態編譯,會將程式的依賴一起打包,這樣一來編譯得到的可執行檔案可以直接在目標平臺執行,無需執行環境(例如 JRE)或動態連結庫(例如 DLL)的支援。
雖然 Go 的靜態編譯很方便,但也存在一個問題:打包生成的可執行檔案體積較大,畢竟相關的依賴都被打包進來了;

預設二進位制打包

package main

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

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run("0.0.0.0:8080")
}

# go build -o test1 main.go
# du -sh test1 
14M    test1

-ldflags

# go build -ldflags "-s -w" -o test2 main.go
# du -sh test2 
 11M    test2

下面假設我們將本地編譯好的 bluebell 二進位制檔案、配置檔案和靜態檔案等上傳到伺服器的/data/app/bluebell目錄下。
補充一點,如果嫌棄編譯後的二進位制檔案太大,可以在編譯的時候加上-ldflags "-s -w"引數去掉符號表和除錯資訊,一般能減小20%的大小;

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ./bin/bluebell
  • 在程式編譯的時候可以加上-ldflags "-s -w"引數來優化編譯,原理是通過去除部分連結和除錯等資訊來減小編譯生成的可執行程式體積,具體引數如下:
  • -a:強制編譯所有依賴包
  • -s:去掉符號表資訊,不過panic的時候stace trace就沒有任何檔名/行號資訊
  • -w:去掉DWARF除錯資訊,不過得到的程式就不能使用gdb進行除錯
  • 若對符號表無需求,-ldflags直接新增"-s"即可

:不建議-w和-s同時使用

UPX

brew/yum install upx
# upx test2 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
  11490768 ->   4063248   35.36%   macho/amd64   test2                         

Packed 1 file.
  
# upx --brute test2
  
# du -sh test2 
4.6M    test2

upx的壓縮選項

  • -o:指定輸出的檔名
  • -k:保留備份原檔案
  • -1:最快壓縮,共1-9九個級別
  • -9:最優壓縮,與上面對應
  • -d:解壓縮decompress,恢復原體積
  • -l:顯示壓縮檔案的詳情,例如upx -l main.exe
  • -t:測試壓縮檔案,例如upx -t main.exe
  • -q:靜默壓縮be quiet
  • -v:顯示壓縮細節be verbose
  • -f:強制壓縮
  • -V:顯示版本號
  • -h:顯示幫助資訊
  • --brute:嘗試所有可用的壓縮方法,slow
  • --ultra-brute:比樓上更極端,very slow

UPX的原理

upx 壓縮後的程式和壓縮前的程式一樣,無需解壓仍然能夠正常地執行,這種壓縮方法稱之為帶殼壓縮,壓縮包含兩個部分:

  • 在程式開頭或其他合適的地方插入解壓程式碼;
  • 將程式的其他部分壓縮;

執行時,也包含兩個部分:

  • 首先執行的是程式開頭的插入的解壓程式碼,將原來的程式在記憶體中解壓出來;
  • 再執行解壓後的程式;

也就是說,upx 在程式執行時,會有額外的解壓動作,不過這個耗時幾乎可以忽略。
如果對編譯後的體積沒什麼要求的情況下,可以不使用 upx 來壓縮。一般在伺服器端獨立執行的後臺服務,無需壓縮體積。

構建輕量級docker映象

這個Dockerfile中使用了兩次FROM指令,第二條FROM scratch行,它告訴Docker從一個全新的,完全空的容器映象重新開始,然後將上個階段編譯好的程式複製到其中。這個才是我們隨後將用於執行的Go應用程式的容器映象。
scratch映象是Docker專案預定義的最小的映象。 Docker用於Go程式的多階段構建很常見,使用scratch映象可以節省大量空間,因為我們實際上不需要Go工具或其他任何東西來執行我們的編譯好的程式,這可能也是Go在容器時代的一個優勢吧。
使用scratch映象製作的Go應用映象在執行時會有一個不識別時區的問題,這個也是我們最近專案往Kubernetes上遷移時遇到的第一個問題,不過還好經過Google和檢視Go載入時區的原始碼找到了解決方法

介紹

多階段允許在建立Dockerfile時使用多個from,它非常有用,因為它使我們能夠使用所有必需的工具構建應用程式。舉個例子,首先我們使用Golang的基礎映象,然後在第二階段的時候使用構建好的映象的二進位制檔案,最後階段構建出來的映象用於釋出到我們自己的倉庫或者是用於上線釋出。

在上述的案例中,我們總共有三個階段:
1 . build編譯階段
2 . certs(可選,可有可無)證照認證階段
3 . prod生產階段

在build階段主要是編譯我們的應用程式,證照認證階段將會安裝我們所需要的CA證照,最後的生產釋出階段會將我們構建好的映象推到映象倉庫中。而且釋出階段將會使用build階段編譯完畢的二進位制檔案和certs階段安裝的證照;


專案釋出的多個build階段


示例工程

main.go

[root@rabbitmq-2 gin_app]# cat /root/go/gin_app/main.go 
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", hello)
	server := &http.Server{
		Addr: ":8888",
	}
  fmt.Println("server startup...")
	if err := server.ListenAndServe(); err != nil {
		fmt.Printf("server startup failed, err:%v\n", err)
	}
}

func hello(w http.ResponseWriter, _ *http.Request) {
	w.Write([]byte("hello youmen.com!"))
}

編譯階段

Dockerfile

[root@rabbitmq-2 gin_app]# cat Dockerfile 
FROM golang:alpine AS build

# 為我們的映象設定必要的環境變數
ENV GO111MODULE=on \
    CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64 \
    GOPROXY="https://goproxy.io"

# 移動到工作目錄:/build
WORKDIR $GOPATH/src/gin_docker 

# 將程式碼複製到容器中
ADD . ./

# 將我們的程式碼編譯成二進位制可執行檔案 app
RUN go build -o app 

# 需要執行的命令
CMD ["./app"]
[root@rabbitmq-2 gin_app]# docker build -t gin_app -t gin_app . --target=build
[root@rabbitmq-2 gin_app]# docker images |grep gin_app
gin_app             latest              c35bb6310fce        10 minutes ago      321MB

[root@rabbitmq-2 gin_app]# docker run --rm -it -p 8888:8888 goweb_app
server startup...

[root@rabbitmq-2 ~]# curl localhost:8888
hello youmen.com!

生產階段

[root@rabbitmq-2 gin_app]# cat Dockerfile 
FROM golang:alpine AS build

# 為我們的映象設定必要的環境變數
ENV GO111MODULE=on \
    CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64 \
    GOPROXY="https://goproxy.io"

# 移動到工作目錄:/build
WORKDIR $GOPATH/src/gin_docker 

# 將程式碼複製到容器中
ADD . ./

# 將我們的程式碼編譯成二進位制可執行檔案 app
RUN go build -ldflags "-s -w"  -o app .

###################
# 接下來建立一個小映象
###################
FROM scratch As prod

# 從builder映象中把/go/src/gin_docker 拷貝到當前目錄
# 設定應用程式以非 root 使用者身份運
# User ID 65534 通常是 'nobody' 使用者.
# 映像的執行者仍應在安裝過程中指定一個使用者。

COPY --chown=65534:0  --from=build  /go/src/gin_docker . 
USER 65534
# 需要執行的命令
CMD ["./app"]

[root@rabbitmq-2 gin_app]# docker build -t gin_app -t gin_app . --target=pro
[root@rabbitmq-2 gin_app]# docker images |grep gin_app
gin_app             latest              592cd0dca666        32 seconds ago      4.42MB

相關文章