縮小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