1. 程式人生 > >追求極簡:Docker映象構建演化史

追求極簡:Docker映象構建演化史

自從2013年dotCloud公司(現已改名為Docker Inc)釋出Docker容器技術以來,到目前為止已經有四年多的時間了。這期間Docker技術飛速發展,並催生出一個生機勃勃的、以輕量級容器技術為基礎的龐大的容器平臺生態圈。作為Docker三大核心技術之一的映象技術在Docker的快速發展之路上可謂功不可沒:映象讓容器真正插上了翅膀,實現了容器自身的重用和標準化傳播,使得開發、交付、運維流水線上的各個角色真正圍繞同一交付物,“test what you write, ship what you test”成為現實。

對於已經接納和使用Docker技術在日常開發工作中的開發者而言,構建Docker映象已經是家常便飯。但如何更高效地構建以及構建出Size更小的映象卻是很多Docker技術初學者心中常見的疑問,甚至是一些老手都未曾細緻考量過的問題。本文將從一個Docker使用者角度來闡述Docker映象構建的演化史,希望能起到一定的解惑作用。

1.映象:繼承中的創新

談映象構建之前,我們先來簡要說一下映象。

Docker技術從本質上說並不是一種新技術,而是將已有技術進行了更好地整合和包裝。核心容器技術以一種完整形態最早出現在Sun公司的Solaris作業系統上,Solaris是當時最先進的伺服器作業系統。2005年Sun釋出了Solaris Container技術,從此開啟了核心容器之門。

2008年,以Google公司開發人員為主導實現的Linux Container(即LXC)功能在被merge到Linux核心中。LXC是一種核心級虛擬化技術,主要基於Namespaces和Cgroups技術,實現共享一個作業系統核心前提下的程序資源隔離,為程序提供獨立的虛擬執行環境,這樣的一個虛擬的執行環境就是一個容器。本質上說,LXC容器與現在的Docker所提供容器是一樣的。Docker也是基於Namespaces和Cgroups技術之上實現的。但Docker的創新之處在於其基於Union File System技術定義了一套容器打包規範,真正將容器中的應用及其執行的所有依賴都封裝到一種特定格式的檔案中去,而這種檔案就被稱為映象(即image),原理見下圖(引自Docker官網):

640?wx_fmt=png

圖1:Docker映象原理

映象是容器的“序列化”標準,這一創新為容器的儲存、重用和傳輸奠定了基礎,並且容器映象“坐上了巨輪”傳播到世界每一個角落,助力了容器技術的飛速發展。

與Solaris Container、LXC等早期核心容器技術不同,Docker還為開發者提供了開發者體驗良好的工具集,這其中就包括了用於映象構建的Dockerfile以及一種用於編寫Dockerfil的領域特定語言。採用Dockerfile方式構建成為映象構建的標準方法,其可重複、可自動化、可維護以及分層精確控制等特點是採用傳統採用docker commit命令提交的映象所不能比擬的。

2.“映象是個筐”:初學者的認知

“映象是個筐,什麼都往裡面裝”- 這句俏皮話可能是大部分Docker初學者對映象最初認知的真實寫照。這裡我們用一個例子來生動地展示一下。 

我們現在將httpserver.go這個原始檔編譯為httpd程式並通過映象釋出。原始檔的內容如下:

//httpserver.go
package mainimport (        "fmt"
        "net/http")func main() {        fmt.Println("http daemon start")
        fmt.Println("  -> listen on port:8080")
        http.ListenAndServe(":8080", nil)}

接下來,我們來編寫用於構建目標映象的Dockerfile:

// Dockerfile
From ubuntu:14.04

RUN apt-get update \
      && apt-get install -y software-properties-common \
      && add-apt-repository ppa:gophers/archive \
      && apt-get update \
      && apt-get install -y golang-1.9-go \
                                 git \
      && rm -rf /var/lib/apt/lists/*

ENV GOPATH /root/goENV GOROOT /usr/lib/go-1.9ENV PATH="/usr/lib/go-1.9/bin:${PATH}"COPY ./httpserver.go /root/httpserver.goRUN go build -o /root/httpd /root/httpserver.go \
      && chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

執行映象構建:

# docker build -t repodemo/httpd:latest .//...構建輸出這裡省略...# docker imagesREPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              183dbef8eba6        2 minutes ago       550MB
ubuntu                           14.04               dea1945146b9        2 months ago        188MB

整個映象的構建過程因環境而定。如果您的網路速度一般,這個構建過程可能會花費你10多分鐘甚至更多。最終如我們所願,基於repodemo/httpd:latest這個映象的容器可以正常執行:

# docker run repodemo/httpdhttp daemon start
  -> listen on port:8080

一個Dockerfile產出一個映象。Dockerfile由若干Command組成,每個Command執行結果都會單獨形成一個層(layer)。我們來探索一下構建出來的映象:

# docker history 183dbef8eba6IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0Ba9d968c704f7        21 minutes ago      /bin/sh -c go build -o /root/httpd /root/h...   6.14MB... ...aef7700a9036        30 minutes ago      /bin/sh -c apt-get update       && apt-get...   356MB
.... ...<missing>           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB

我們去除掉那些Size為0或很小的layer,我們看到三個size佔比較大的layer,見下圖: 
640?wx_fmt=png

圖2:Docker映象分層探索

雖然Docker引擎利用快取機制可以讓同主機下非首次的映象構建執行得很快,但是在Docker技術熱情催化下的這種構建思路讓docker映象在儲存和傳輸方面的優勢蕩然無存,要知道一個ubuntu-server 16.04的虛擬機器ISO檔案的大小也就不過600多MB而已。

3.“理性的迴歸”:builder模式的崛起

Docker使用者在新技術接觸初期的熱情“冷卻”之後迎來了“理性的迴歸”。根據上面分層映象的圖示,我們發現最終映象中包含構建環境是多餘的,我們只需要在最終映象中包含足夠支撐httpd執行的執行環境即可,而base image自身就可以滿足。於是我們應該剔除不必要的中間層:

640?wx_fmt=png

圖3:去除不必要的分層

現在問題來了!如果不在同一映象中完成應用構建,那麼在哪裡、由誰來構建應用呢?至少有兩種方法:

  1. 在本地構建並COPY到映象中;

  2. 藉助構建者映象(builder image)構建。

不過方法1本地構建有很多侷限性,比如:本地環境無法複用、無法很好融入持續整合/持續交付流水線等。而藉助builder image進行構建已經成為Docker社群的一個最佳實踐,Docker官方為此也推出了各種主流程式語言的官方base image,包括go、java、nodejs、python以及ruby的等。藉助builder image進行映象構建的流程原理如下圖:

640?wx_fmt=png

圖4:藉助builder image進行映象構建的流程圖

通過原理圖,我們可以看到整個目標映象的構建被分為了兩個階段:

  1. 第一階段:構建負責編譯原始碼的構建者映象;

  2. 第二階段:將第一階段的輸出作為輸入,構建出最終的目標映象。

我們選擇golang:1.9.2作為builder base image,構建者映象的 Dockerfile.build如下:

// Dockerfile.buildFROM golang:1.9.2WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

執行構建:

# docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .

構建好的應用程式httpd放在了映象repodemo/httpd-builder中的/go/src目錄下,我們需要一些“膠水”命令來連線兩個構建階段,這些命令將httpd從構建者映象中取出並作為下一階段構建的輸入:

# docker create --name extract-httpserver repodemo/httpd-builder# docker cp extract-httpserver:/go/src/httpd ./httpd# docker rm -f extract-httpserver# docker rmi repodemo/httpd-builder

通過上面的命令,我們將編譯好的httpd程式拷貝到了本地。下面是目標映象的Dockerfile:

// Dockerfile.targetFrom ubuntu:14.04COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

接下來我們來構建目標映象:

# docker build -t repodemo/httpd:latest -f Dockerfile.target .

我們來看看這個映象的“體格”:

# docker imagesREPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              e3d009d6e919        12 seconds ago      200MB

200MB!目標映象的Size降為原來的1/2還多。

4.“像賽車那樣減去所有不必要的東西”:追求最小映象

前面我們構建出的映象的Size已經縮小到200MB,但這還不夠。200MB的“體格”在我們的網路環境下快取和傳輸仍然很難令人滿意。我們要為映象進一步減重,減到儘可能的小,就像賽車那樣,為了能減輕重量將所有不必要的東西都拆除掉:我們僅保留能支撐我們的應用執行的必要庫、命令,其餘的一律不納入目標映象。當然不僅僅是Size上的原因,小映象還有額外的好處,比如:記憶體佔用小,啟動速度快,更加高效;不會因其他不必要的工具、庫的漏洞而被攻擊,減少了“攻擊面”,更加安全等。

寧波整形醫院http://www.zuanno.com/