為Go程序創建最小的Docker Image


本文將會介紹如何使用docker打包一個golang編寫的應用程序,最終的產物就是一個Dockerfile文件,可別小瞧這短短幾行代碼,涉及的知識點可不少,接下來我們就仔細剖析一下吧。

FROM golang:alpine

ADD src /go/src
RUN go install -v test 

ENTRYPOINT ["/go/bin/test"]
CMD ["-logtostderr"]

1.基礎鏡像選擇

第一行是指定一個基礎鏡像,在此基礎上創建我們的鏡像,此處使用的是golang:alpine版本,
這是一個相對較小的linux系統,砍掉了linux中的許多工具,預裝了golang, 包管理工具使用的是apk,可以把這個鏡像docker pull下來把玩一番,默認的shell是sh,執行命令docker run -t-i golang:alpine /bin/sh 進入命令行。進入后執行env查看環境變量,因為其GOPATH這個環境變量對后面的環境部署有用,可以看到環境變量GOPATH默認值為/go

2.映射代碼文件並安裝

使用 ADD src /go/src 將主機scr文件映射到/go/src目錄下,為什么非得是這個/go/src這個目錄吶?沒錯就是上面的GOPATH環境變量的路徑,因為我們后面需要執行go install命令進行安裝,否則的話就需要重新設置GOPATH才能編譯代碼。如下test是程序的主程序,glog是使用的開源日志庫,整個文件結構如下:

.
├── Dockerfile
└── src
    ├── github.com
    │   └── golang
    │       └── glog
    │           ├── glog_file.go
    │           ├── glog.go
    │           ├── glog_test.go
    │           ├── LICENSE
    │           └── README
    └── test
        └── main.go

此處glog庫沒有使用glide等包管理工具,直接使用git submodule來管理, 優勢是git push不會將glog代碼Push到遠程倉庫,只是添加一個對glog的引用,並且當glog庫中代碼被修改后可以只需要在glog的子目錄中git pull即可,也就是說在本地會拉取具體的代碼進行編譯等,但是在遠程倉庫只是保存引用。
可以通過命令生成glog這個子模塊: git submodule add https://github.com/golang/glog.git src/github.com/golang/glog。注意git submoule命令中被引用到的位置為src/github.com/golang而不是直接的src/ 中,因為執行該命令后本地代碼倉庫會clone glog這個代碼倉庫,將它的代碼拉下來,只是創建glog這個目錄,所以前面的一些父目錄需要自己創建。關於命令更多的介紹參見 Git
組織好文件結構就可以進行go install了,生成的可執行在$GOPATH/bin中,后面就是基本的指定入口程序和參數。通過docker build -t="name" . 生成鏡像

3.更進一步:提前編譯

上面的方式是將代碼拷貝進基礎鏡像並在其內部編譯,毫無疑問的是golang:alpine中包含一系列程序運行的依賴,程序運行會動態加載這些庫,我們可以用ldd命令查看所生成的二進制文件的依賴:

linux-vdso.so.1 =>  (0x00007ffc5b1e4000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f50a1f13000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f50a1b4a000)
/lib64/ld-linux-x86-64.so.2 (0x00005611a4b0a000)

那么問題來了? 如果將這些依賴靜態編譯至可執行文件中, 並且只將可執行文件添加到鏡像中, 那就不需要在鏡像中保存這些運行時依賴和源碼了,就可以創建一個更小的鏡像了。幸運的是無論是golang的編譯機制還是docker的基礎鏡像都提供這樣的實現:
使用命令生成靜態編譯的二進制文件:CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main test
此時用ldd查看生成的可執行文件的依賴,可以看到顯示not a dynamic executable,這里我們禁用CGO使其生成靜態二進制文件,同時設置系統為linux。我們將基礎鏡像設置為 scratch,這是一個空的鏡像,是用來構建其他基礎鏡像的, 無需下載即可使用。
重新編寫的Dockerfile如下:

FROM scratch
ADD main /
ENTRYPOINT ["/main"]
CMD ["-logtostderr"]

執行docker build -t example-scratch .生成鏡像,可以看到該鏡像的大小只有幾M,並且和二進制程序main的大小相同。
Dockerfile中FROM scratch並不會增加層數, 所以用此Dockerfile構建的鏡像只是三層,並且鏡像的大小和二進制文件的大小相同,可以通過docker image history查看這些信息

gaorong@gaorong-TM1604 % ls -lh main
-rwxrwxr-x 1 gaorong gaorong 2.4M 6月   9 11:59 main*

gaorong@gaorong-TM1604 % docker images example-scratch
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
example-scratch     latest              817e7d91c8c0        About an hour ago   2.42MB

gaorong@gaorong-TM1604 % docker image history example-scratch
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
817e7d91c8c0        About an hour ago   /bin/sh -c #(nop)  CMD ["-logtostderr"]         0B
323b904e4844        About an hour ago   /bin/sh -c #(nop)  ENTRYPOINT ["/main"]         0B
8216c95b5652        About an hour ago   /bin/sh -c #(nop) ADD file:6828257fa0b521333…   2.42MB

4.builder image

上述鏡像構建是需要提前編譯好二進制,然后才能拷貝到最終的鏡像中,可否將編譯的這一步驟也容器化了呢? 當然可以,可以寫一個Dockerfile.builder來進行build操作,

FROM golang:alpine
ENV CGO_ENABLED=0 GOOS=linux 
CMD ["go", "build", "-a", "-installsuffix", "cgo", "-o", "main", "test" ]

在執行的時候需要將當前文件目錄volume掛載進容器的/go目錄下: docker run -v `pwd`:/go builder
本文所使用的案例太過簡單,builder image意義不大,假如你參與一個大型項目,項目中有一個Makefile,其中定義了好多操作,例如生成項目的rpm包,生成rpm這些操作需要調用額外的程序執行,如果參與項目的每個人都配置這樣一個開發環境未免太麻煩,此時就可以利用builder image將所需要的環境全部打包進去,然后在builder image調用makefile即可。 著名的案例就是kubernetes,開發者在個人電腦上只需要安裝了docker就可以編譯生成kuberetes所有的binary,甚至golang也無需安裝。

5.分階段編譯(multi-stage builds)

可以利用docker 的分階段編譯將上述兩個操作合並起來,先在一個鏡像中構建,在另一個鏡像中執行。下面的Dockerfile摘自prometheus這個第三方監控的dmo中的Dockerfile,可以看到它是首先在builder鏡像中下載對應的依賴並且編譯程序,最后在scratch基礎鏡像中執行程序。

# This Dockerfile builds an image for a client_golang example.
#
# Use as (from the root for the client_golang repository):
#    docker build -f examples/$name/Dockerfile -t prometheus/golang-example-$name .

# Builder image, where we build the example.
FROM golang:1.9.0 AS builder
WORKDIR /go/src/github.com/prometheus/client_golang
COPY . .
WORKDIR /go/src/github.com/prometheus/client_golang/prometheus
RUN go get -d
WORKDIR /go/src/github.com/prometheus/client_golang/examples/simple
RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w'

# Final image.
FROM scratch
LABEL maintainer "The Prometheus Authors <prometheus-developers@googlegroups.com>"
COPY --from=builder /go/src/github.com/prometheus/client_golang/examples/simple .
EXPOSE 8080
ENTRYPOINT ["/simple"]

其實就是將一個鏡像作為builder鏡像,然后將build產物在另外一個鏡像中執行。

參考

Building Minimal Docker Containers for Go Applications


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM