構(gòu)建Golang應(yīng)用最小Docker鏡像的實(shí)現(xiàn)
我通常使用docker運(yùn)行我的 golang 程序,在這里分享一下我構(gòu)建 docker 鏡像的經(jīng)驗(yàn)。我構(gòu)建 docker 鏡像不僅優(yōu)化構(gòu)建后的體積,還要優(yōu)化構(gòu)建速度。
示例應(yīng)用
首先貼出代碼例子,我們假設(shè)要構(gòu)建一個(gè) http 服務(wù)
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) } }
說(shuō)明:
- 這里選擇 Gin 作為例子,是為了演示我們有第三方包條件下要優(yōu)化構(gòu)建速度
- main函數(shù)第一行打印了一行字,為了演示后面啟動(dòng)時(shí)遇到的一個(gè)坑
- 跟路由打印了時(shí)間,為了演示后面遇到的關(guān)于時(shí)區(qū)的坑
- 路由 github 嘗試訪問(wèn) https://api.github.com,為了演示后面遇到的證書(shū)坑
這里我們可以先試一試構(gòu)建后包的體積
$ go build -o server $ ls -alh | grep server -rwxrwxrwx 1 eyas eyas 14.6M May 29 10:26 server
14.6MB,這是一個(gè)http服務(wù)的 hello world,當(dāng)然這是因?yàn)槭褂昧?gin ,所以有些大,如果用標(biāo)準(zhǔn)包 net/http 寫(xiě)的 hello world,體積大概是接近 7 MB
Dockerfile 的進(jìn)化
版本一,初步優(yōu)化
先看看第一個(gè)版本
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"]
說(shuō)明:
- 選擇 golang:1.14-alpine 作為編譯環(huán)境,是因?yàn)檫@是體積最小的golang編譯環(huán)境
- 設(shè)置 GOPROXY 是為了提升構(gòu)建速度
- 先復(fù)制 go.mod 和 go.sum ,然后 go mod download,是為了防止每次構(gòu)建都會(huì)重新下載依賴(lài)包,利用docker構(gòu)建緩存提升構(gòu)建速度
- go build 時(shí)加上 -ldflags "-s -w" 去除構(gòu)建包的調(diào)試信息,減小go構(gòu)建后程序體積,大概能減小 1/4 吧
- 使用了多階段構(gòu)建,也就是 FROM XXX as xxx ,在構(gòu)建程序包的時(shí)候,使用帶編譯環(huán)境的鏡像去構(gòu)建,運(yùn)行的時(shí)候其實(shí)完全不需要go的編譯環(huán)境,所以在運(yùn)行階段使用docker的空鏡像 scratch 去運(yùn)行。這部是減小鏡像體積最有效的方法了。
好了,下面開(kāi)始構(gòu)建鏡像
$ docker build -t server . ... Successfully built 8d3b91210721 Successfully tagged server:latest
到了這一步,構(gòu)建成功,看看鏡像大小
$ docker images server latest 8d3b91210721 1 minutes ago 11MB
11MB,還行,現(xiàn)在運(yùn)行一下
$ docker run -p 9900:9900 server standard_init_linux.go:211: exec user process caused "no such file or directory"
發(fā)現(xiàn)啟動(dòng)報(bào)錯(cuò)了,而且main函數(shù)的第一行打印語(yǔ)句都沒(méi)有出現(xiàn),所以整個(gè)程序完全沒(méi)有運(yùn)行。錯(cuò)誤原因是缺少庫(kù)依賴(lài)文件。這其實(shí)是構(gòu)建的 go 程序還依賴(lài)底層的 so 庫(kù)文件,不信可以在物理機(jī)編譯后看看它的依賴(lài)
$ 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)
這是不是跟我們的認(rèn)知有點(diǎn)出入呢,說(shuō)好無(wú)依賴(lài)的呢,結(jié)果還是有幾個(gè)依賴(lài)庫(kù)文件呢,雖然這幾個(gè)依賴(lài)都是最底層的,一般操作系統(tǒng)都會(huì)有,可誰(shuí)叫我們選了 scratch,這個(gè)鏡像里面除了linux內(nèi)核以外真的什么都沒(méi)了。
這是因?yàn)間o build 是默認(rèn)啟用 CGO 的,不信你可以試試這個(gè)命令 go env CGO_ENABLED,在 CGO 開(kāi)啟情況下,無(wú)論代碼有沒(méi)有用CGO,都會(huì)有庫(kù)依賴(lài)文件,解決方法也很簡(jiǎn)單,手動(dòng)指定關(guān)閉CGO就行,而且包體積并不會(huì)增加哦,還會(huì)減少呢
$ CGO_ENABLED=0 go build -o server $ ldd server not a dynamic executable
版本二,解決運(yùn)行時(shí)報(bào)錯(cuò)
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"]
改動(dòng)點(diǎn): 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
正常啟動(dòng)了,我們?cè)L問(wèn)一下試試,訪問(wèn)之前看看當(dāng)前時(shí)間
$ 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
發(fā)現(xiàn)有問(wèn)題
- 當(dāng)前系統(tǒng)時(shí)間是 13:11:28 ,但是根據(jù)由顯示的時(shí)間是 05:11:53,其實(shí)是docker 容器內(nèi)的時(shí)區(qū)不對(duì),默認(rèn)是 0 時(shí)區(qū),可是我們國(guó)家是 東8區(qū)
- 嘗試訪問(wèn) https://api.github.com/ 這是 https 站點(diǎn),報(bào)證書(shū)錯(cuò)誤
解決問(wèn)題
- 在容器放置根證書(shū)
- 設(shè)置容器時(shí)區(qū)
版本三,解決運(yùn)行環(huán)境時(shí)區(qū)與證書(shū)問(wèn)題
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 兩個(gè)庫(kù),在runner階段,將時(shí)區(qū)配置和根證書(shū)復(fù)制了一份
$ 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
訪問(wèn)一下試試
$ 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
一切正常了,看看當(dāng)前鏡像大小
$ docker images server latest e0825838043d 9 minutes ago 11.3MB
才 11.3MB,已經(jīng)很小了,但是,還可以更小,就是把構(gòu)建后的包再壓縮一次
版本四,進(jìn)一步減小體積
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 壓縮了一下,執(zhí)行一下構(gòu)建,你會(huì)發(fā)現(xiàn)這個(gè)構(gòu)建時(shí)間變長(zhǎng)了,這是因?yàn)槲医o upx 設(shè)置的參數(shù)是 --best ,也就是最大壓縮級(jí)別,這樣壓縮出來(lái)的后會(huì)盡可能的小,如果嫌慢,可以降低壓縮級(jí)別從 -1 到 -9 ,數(shù)字越大壓縮級(jí)別越高,也越慢。我使用 --best 構(gòu)建完成后看看鏡像體積。
$ docker build -t server . ... Successfully built 80c3f3cde1f7 Successfully tagged server:latest $ docker images server latest 80c3f3cde1f7 1 minutes ago 4.26MB
這下子可小了,才 4.26MB,再去試試那兩個(gè)接口,一切正常。優(yōu)化到此結(jié)束。
最終的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"]
總結(jié)
要減小鏡像體積,首先多階段構(gòu)建這很重要,這樣就可以把編譯環(huán)境和運(yùn)行環(huán)境分開(kāi)。
另外,選擇 scratch 這個(gè)鏡像其實(shí)很不明智,它雖然很小,但是它太原始了,里面什么工具都沒(méi)有,程序啟動(dòng)后,連容器都進(jìn)不去,就算進(jìn)去了什么都做不了。所以就算一昧的追求盡可能小的鏡像體積,也不建議選擇 scratch 作為運(yùn)行環(huán)境,我暫時(shí)只踩到小部分的坑,后面還有更多坑沒(méi)踩,我也沒(méi)有興趣繼續(xù)踩 scratch 的坑。
建議選擇 alpine ,alpine 的鏡像大小是 5.61MB 這個(gè)大小其實(shí)還是鏡像解壓后的大小,實(shí)際上下載鏡像的時(shí)候,只需要下載 2.68 MB 。還有,上文所有我說(shuō)的鏡像體積,全都是指解壓后的鏡像體積,和實(shí)際上傳下載時(shí)的體積是不一樣的,docker自己會(huì)壓縮一次再傳輸鏡像
還有個(gè)很小的鏡像是 busybox,它的體積是 1.22MB,下載 705.6 KB ,有大部分的linux命令可用,但是運(yùn)行環(huán)境還是很原始,有興趣可以去嘗試
無(wú)論是 alpine 還是 busybox ,他們都會(huì)上述時(shí)區(qū)和證書(shū)問(wèn)題,同樣按照上面方法就能解決,切換到 alpine 或者 busybox 也很簡(jiǎn)單,只需要修改 runner 基礎(chǔ)鏡像就行
-FROM scratch as runner +FROM alpine as runner
或者
-FROM scratch as runner +FROM busybox as runne
到此這篇關(guān)于構(gòu)建Golang應(yīng)用最小Docker鏡像的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Golang構(gòu)建最小Docker鏡像內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang使用cobra實(shí)現(xiàn)命令行程序的示例代碼
Cobra 是 Go 語(yǔ)言中一個(gè)強(qiáng)大的命令行應(yīng)用庫(kù),它提供了創(chuàng)建命令行工具所需的基本結(jié)構(gòu)和功能,被許多開(kāi)發(fā)者用于構(gòu)建各種命令行工具和應(yīng)用程序,本文將給大家介紹Golang使用cobra實(shí)現(xiàn)命令行程序,文中通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-02-02Go?modules?replace解決Go依賴(lài)引用問(wèn)題
這篇文章主要為大家介紹了Go?modules?replace解決Go依賴(lài)引用問(wèn)題,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06創(chuàng)建第一個(gè)Go語(yǔ)言程序Hello,Go!
這篇文章主要介紹了創(chuàng)建第一個(gè)Go語(yǔ)言程序Hello,Go!本文詳細(xì)的給出項(xiàng)目創(chuàng)建、代碼編寫(xiě)的過(guò)程,同時(shí)講解了GOPATH、Go install等內(nèi)容,需要的朋友可以參考下2014-10-10Go語(yǔ)言基礎(chǔ)入門(mén)應(yīng)用簡(jiǎn)介及常用命令
這篇文章主要為大家介紹了Go語(yǔ)言基礎(chǔ)入門(mén)應(yīng)用簡(jiǎn)介及常用命令,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-11-11執(zhí)行g(shù)o?build報(bào)錯(cuò)go:?go.mod?file?not?found?in?current?dir
本文主要為大家介紹了執(zhí)行g(shù)o build報(bào)錯(cuò)go:?go.mod?file?not?found?in?current?directory?or?any?parent?directory解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Go語(yǔ)言中使用 buffered channel 實(shí)現(xiàn)線(xiàn)程安全的 pool
這篇文章主要介紹了Go語(yǔ)言中使用 buffered channel 實(shí)現(xiàn)線(xiàn)程安全的 pool,因?yàn)镚o語(yǔ)言自帶的sync.Pool并不是很好用,所以自己實(shí)現(xiàn)了一線(xiàn)程安全的 pool,需要的朋友可以參考下2014-10-10