現(xiàn)在很多開發(fā)者都會慢慢習(xí)慣在開發(fā)環(huán)境通過Docker
來構(gòu)建開發(fā)環(huán)境姥卢,有時(shí)候可能會有環(huán)境移植的問題摔蓝,所以需要我們寫好一套Dockerfile
來構(gòu)建相關(guān)的開發(fā)鏡像,既然說到鏡像盏触,那我想問問大家了解Docker
鏡像的演變史嗎渗蟹?我們現(xiàn)在就來回顧一下吧。
自從2013年dotCloud公司(現(xiàn)已改名為Docker Inc)發(fā)布Docker容器技術(shù)以來赞辩,到目前為止已經(jīng)有四年多的時(shí)間了雌芽。這期間Docker技術(shù)飛速發(fā)展,并催生出一個生機(jī)勃勃的辨嗽、以輕量級容器技術(shù)為基礎(chǔ)的龐大的容器平臺生態(tài)圈世落。作為Docker三大核心技術(shù)之一的鏡像技術(shù)在Docker
的快速發(fā)展之路上可謂功不可沒:鏡像讓容器真正插上了翅膀,實(shí)現(xiàn)了容器自身的重用和標(biāo)準(zhǔn)化傳播糟需,使得開發(fā)屉佳、交付谷朝、運(yùn)維流水線上的各個角色真正圍繞同一交付物,“test what you write, ship what you test”
成為現(xiàn)實(shí)武花。
對于已經(jīng)接納和使用Docker技術(shù)
在日常開發(fā)工作中的開發(fā)者而言圆凰,構(gòu)建Docker鏡像
已經(jīng)是家常便飯。但如何更高效地構(gòu)建以及構(gòu)建出Size
更小的鏡像卻是很多Docker
技術(shù)初學(xué)者心中常見的疑問髓堪,甚至是一些老手都未曾細(xì)致考量過的問題送朱。本文將從一個Docker
用戶角度來闡述Docker
鏡像構(gòu)建的演化史,希望能起到一定的解惑作用干旁。
一驶沼、鏡像:繼承中的創(chuàng)新
談鏡像構(gòu)建之前,我們先來簡要說下鏡像争群。
Docker技術(shù)本質(zhì)上并不是新技術(shù)回怜,而是將已有技術(shù)進(jìn)行了更好地整合和包裝。內(nèi)核容器技術(shù)以一種完整形態(tài)最早出現(xiàn)在Sun公司的Solaris操作系統(tǒng)上换薄,Solaris是當(dāng)時(shí)最先進(jìn)的服務(wù)器操作系統(tǒng)玉雾。2005年Sun發(fā)布了Solaris Container技術(shù),從此開啟了內(nèi)核容器之門轻要。
2008年复旬,以Google公司開發(fā)人員為主導(dǎo)實(shí)現(xiàn)的Linux Container(即LXC)功能在被merge到Linux內(nèi)核中。LXC是一種內(nèi)核級虛擬化技術(shù)冲泥,主要基于Namespaces和Cgroups技術(shù)驹碍,實(shí)現(xiàn)共享一個操作系統(tǒng)內(nèi)核前提下的進(jìn)程資源隔離,為進(jìn)程提供獨(dú)立的虛擬執(zhí)行環(huán)境凡恍,這樣的一個虛擬的執(zhí)行環(huán)境就是一個容器志秃。本質(zhì)上說,LXC容器與現(xiàn)在的Docker所提供容器是一樣的嚼酝。Docker也是基于Namespaces和Cgroups技術(shù)之上實(shí)現(xiàn)的浮还,Docker的創(chuàng)新之處在于其基于Union File System技術(shù)定義了一套容器打包規(guī)范,真正將容器中的應(yīng)用及其運(yùn)行的所有依賴都封裝到一種特定格式的文件中去闽巩,而這種文件就被稱為鏡像(即image)钧舌,原理見下圖(引自Docker官網(wǎng)):
圖1:Docker鏡像原理
鏡像是容器的“序列化”標(biāo)準(zhǔn),這一創(chuàng)新為容器的存儲涎跨、重用和傳輸?shù)於嘶A(chǔ)洼冻。并且“坐上了巨輪”的容器鏡像可以傳播到世界每一個角落,這無疑助力了容器技術(shù)的飛速發(fā)展六敬。
與Solaris Container碘赖、LXC
等早期內(nèi)核容器技術(shù)不同,Docker
為開發(fā)者提供了開發(fā)者體驗(yàn)良好的工具集,這其中就包括了用于鏡像構(gòu)建的Dockerfile
以及一種用于編寫Dockerfile
領(lǐng)域特定語言普泡。采用Dockerfile
方式構(gòu)建成為鏡像構(gòu)建的標(biāo)準(zhǔn)方法播掷,其可重復(fù)、可自動化撼班、可維護(hù)以及分層精確控制等特點(diǎn)是采用傳統(tǒng)采用docker commit
命令提交的鏡像所不能比擬的歧匈。
二、“鏡像是個筐”:初學(xué)者的認(rèn)知
“鏡像是個筐砰嘁,什么都往里面裝” – 這句俏皮話可能是大部分Docker
初學(xué)者對鏡像最初認(rèn)知的真實(shí)寫照件炉。這里我們用一個例子來生動地展示一下。我們將httpserver.go
這個源文件編譯為httpd
程序并通過鏡像發(fā)布矮湘,考慮到被編譯的源碼并非本文重點(diǎn)斟冕,這里使用了一個極簡的demo
代碼:
//httpserver.go
package main
import (
"fmt"
"net/http"
)
func main() {
fmt.Println("http daemon start")
fmt.Println(" -> listen on port:8080")
http.ListenAndServe(":8080", nil)
}
接下來,我們來編寫一個用于構(gòu)建目標(biāo)image
的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/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"
COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
&& chmod +x /root/httpd
WORKDIR /root
ENTRYPOINT ["/root/httpd"]
構(gòu)建這個Image:
# docker build -t repodemo/httpd:latest .
//...構(gòu)建輸出這里省略...
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd latest 183dbef8eba6 2 minutes ago 550MB
ubuntu 14.04 dea1945146b9 2 months ago 188MB
整個鏡像的構(gòu)建過程因環(huán)境而定缅阳。如果您的網(wǎng)絡(luò)速度一般磕蛇,這個構(gòu)建過程可能會花費(fèi)你10多分鐘甚至更多。最終如我們所愿十办,基于repodemo/httpd:latest
這個鏡像的容器可以正常運(yùn)行:
# docker run repodemo/httpd
http daemon start
-> listen on port:8080
一個Dockerfile最終生產(chǎn)出一個鏡像秀撇。Dockerfile由若干Command組成,每個Command執(zhí)行結(jié)果都會單獨(dú)形成一個layer向族。我們來探索一下構(gòu)建出來的鏡像:
# docker history 183dbef8eba6
IMAGE CREATED CREATED BY SIZE COMMENT
183dbef8eba6 21 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["/root/httpd"] 0B
27aa721c6f6b 21 minutes ago /bin/sh -c #(nop) WORKDIR /root 0B
a9d968c704f7 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
我們?nèi)コ裟切㏒ize為0或很小的layer呵燕,我們看到三個size占比較大的layer,見下圖:
圖2:Docker鏡像分層探索
雖然Docker引擎利用r緩存機(jī)制可以讓同主機(jī)下非首次的鏡像構(gòu)建執(zhí)行得很快件相,但是在Docker技術(shù)熱情催化下的這種構(gòu)建思路讓docker鏡像在存儲和傳輸方面的優(yōu)勢蕩然無存再扭,要知道一個ubuntu-server 16.04的虛擬機(jī)ISO文件的大小也就不過600多MB而已。
三适肠、”理性的回歸”:builder模式的崛起
Docker使用者在新技術(shù)接觸初期的熱情“冷卻”之后迎來了“理性的回歸”霍衫。根據(jù)上面分層鏡像的圖示候引,我們發(fā)現(xiàn)最終鏡像中包含構(gòu)建環(huán)境是多余的侯养,我們只需要在最終鏡像中包含足夠支撐httpd運(yùn)行的運(yùn)行環(huán)境即可,而base image自身就可以滿足澄干。于是我們應(yīng)該去除不必要的中間層:
圖3:去除不必要的分層
現(xiàn)在問題來了逛揩!如果不在同一鏡像中完成應(yīng)用構(gòu)建,那么在哪里麸俘、由誰來構(gòu)建應(yīng)用呢辩稽?至少有兩種方法:
- 在本地構(gòu)建并COPY到鏡像中;
- 借助構(gòu)建者鏡像(builder image)構(gòu)建从媚。
不過方法1本地構(gòu)建有很多局限性逞泄,比如:本地環(huán)境無法復(fù)用、無法很好融入持續(xù)集成/持續(xù)交付流水線等。借助builder image進(jìn)行構(gòu)建已經(jīng)成為Docker社區(qū)的一個最佳實(shí)踐喷众,Docker官方為此也推出了各種主流編程語言的官方base image各谚,比如:go、java到千、node昌渤、python以及ruby等。借助builder image進(jìn)行鏡像構(gòu)建的流程原理如下圖:
圖4:借助builder image進(jìn)行鏡像構(gòu)建的流程圖
通過原理圖憔四,我們可以看到整個目標(biāo)鏡像的構(gòu)建被分為了兩個階段:
第一階段:構(gòu)建負(fù)責(zé)編譯源碼的構(gòu)建者鏡像膀息;
第二階段:將第一階段的輸出作為輸入,構(gòu)建出最終的目標(biāo)鏡像了赵。
我們選擇golang:1.9.2作為builder base image潜支,構(gòu)建者鏡像的Dockerfile.build如下:
// Dockerfile.build
FROM golang:1.9.2
WORKDIR /go/src
COPY ./httpserver.go .
RUN go build -o httpd ./httpserver.go
執(zhí)行構(gòu)建:
# docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .
構(gòu)建好的應(yīng)用程序httpd放在了鏡像repodemo/httpd-builder中的/go/src目錄下,我們需要一些“膠水”命令來連接兩個構(gòu)建階段柿汛,這些命令將httpd從構(gòu)建者鏡像中取出并作為下一階段構(gòu)建的輸入:
# 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程序拷貝到了本地。下面是目標(biāo)鏡像的Dockerfile:
//Dockerfile.target
From ubuntu:14.04
COPY ./httpd /root/httpd
RUN chmod +x /root/httpd
WORKDIR /root
ENTRYPOINT ["/root/httpd"]
接下來我們來構(gòu)建目標(biāo)鏡像:
# docker build -t repodemo/httpd:latest -f Dockerfile.target .
我們來看看這個鏡像的“體格”:
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd latest e3d009d6e919 12 seconds ago 200MB
200MB苛茂!目標(biāo)鏡像的Size降為原來的 1/2 還多已烤。
四、“像賽車那樣減去所有不必要的東西”:追求最小鏡像
前面我們構(gòu)建出的鏡像的Size已經(jīng)縮小到200MB妓羊,但這還不夠胯究。200MB的“體格”在我們的網(wǎng)絡(luò)環(huán)境下緩存和傳輸仍然很難令人滿意。我們要為鏡像進(jìn)一步減重躁绸,減到盡可能的小裕循,就像賽車那樣,為了能減輕重量將所有不必要的東西都拆除掉:我們僅保留能支撐我們的應(yīng)用運(yùn)行的必要庫净刮、命令剥哑,其余的一律不納入目標(biāo)鏡像。當(dāng)然不僅僅是Size上的原因淹父,小鏡像還有額外的好處株婴,比如:內(nèi)存占用小,啟動速度快暑认,更加高效困介;不會因其他不必要的工具、庫的漏洞而被攻擊蘸际,減少了“攻擊面”座哩,更加安全。
圖5:目標(biāo)鏡像還能更小些嗎粮彤?
一般應(yīng)用開發(fā)者不會從scratch鏡像從頭構(gòu)建自己的base image以及目標(biāo)鏡像的根穷,開發(fā)者會挑選適合的base image姜骡。一些“蠅量級”甚至是“草量級”的官方base image的出現(xiàn)為這種情況提供了條件。
圖6:一些base image的Size比較(來自imagelayers.io截圖)
從圖中看屿良,我們有兩個選擇:busybox和alpine溶浴。
單從image的size上來說,busybox更小管引。不過busybox默認(rèn)的libc實(shí)現(xiàn)是uClibc士败,而我們通常運(yùn)行環(huán)境使用的libc實(shí)現(xiàn)都是glibc,因此我們要么選擇靜態(tài)編譯程序褥伴,要么使用busybox:glibc鏡像作為base image谅将。
而 alpine image 是另外一種蠅量級 base image,它使用了比 glibc 更小更安全的 musl libc 庫重慢。 不過和 busybox image 相比饥臂,alpine image 體積還是略大。除了因?yàn)?musl比uClibc 大一些之外似踱,alpine還在鏡像中添加了自己的包管理系統(tǒng)apk隅熙,開發(fā)者可以使用apk在基于alpine的鏡像中添 加需要的包或工具。因此核芽,對于普通開發(fā)者而言囚戚,alpine image顯然是更佳的選擇。不過alpine使用的libc實(shí)現(xiàn)為musl轧简,與基于glibc上編譯出來的應(yīng)用程序不兼容驰坊。如果直接將前面構(gòu)建出的httpd應(yīng)用塞入alpine,在容器啟動時(shí)會遇到下面錯誤哮独,因?yàn)榧虞d器找不到glibc這個動態(tài)共享庫文件:
standard_init_linux.go:185: exec user process caused "no such file or directory"
對于Go應(yīng)用來說拳芙,我們可以采用靜態(tài)編譯的程序,但一旦采用靜態(tài)編譯皮璧,也就意味著我們將失去一些libc提供的原生能力舟扎,比如:在linux上,你無法使用系統(tǒng)提供的DNS解析能力悴务,只能使用Go自實(shí)現(xiàn)的DNS解析器睹限。
我們還可以采用基于alpine的builder image,golang base image就提供了alpine 版本惨寿。 我們就用這種方式構(gòu)建出一個基于alpine base image的極小目標(biāo)鏡像邦泄。
圖7:借助 alpine builder image 進(jìn)行鏡像構(gòu)建的流程圖
我們新建兩個用于 alpine 版本目標(biāo)鏡像構(gòu)建的 Dockerfile:Dockerfile.build.alpine 和Dockerfile.target.alpine:
//Dockerfile.build.alpine
FROM golang:alpine
WORKDIR /go/src
COPY ./httpserver.go .
RUN go build -o httpd ./httpserver.go
// Dockerfile.target.alpine
From alpine
COPY ./httpd /root/httpd
RUN chmod +x /root/httpd
WORKDIR /root
ENTRYPOINT ["/root/httpd"]
構(gòu)建builder鏡像:
# docker build -t repodemo/httpd-alpine-builder:latest -f Dockerfile.build.alpine .
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd-alpine-builder latest d5b5f8813d77 About a minute ago 275MB
執(zhí)行“膠水”命令:
# docker create --name extract-httpserver repodemo/httpd-alpine-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-alpine-builder
構(gòu)建目標(biāo)鏡像:
# docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine .
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd-alpine latest 895de7f785dd 13 seconds ago 16.2MB
16.2MB删窒!目標(biāo)鏡像的Size降為不到原來的十分之一裂垦。我們得到了預(yù)期的結(jié)果。
五肌索、“要有光蕉拢,于是便有了光”:對多階段構(gòu)建的支持
至此,雖然我們實(shí)現(xiàn)了目標(biāo)Image的最小化,但是整個構(gòu)建過程卻是十分繁瑣晕换,我們需要準(zhǔn)備兩個Dockerfile午乓、需要準(zhǔn)備“膠水”命令、需要清理中間產(chǎn)物等闸准。作為Docker用戶益愈,我們希望用一個Dockerfile就能解決所有問題,于是就有了Docker引擎對多階段構(gòu)建(multi-stage build)的支持夷家。注意:這個特性非常新蒸其,只有Docker 17.05.0-ce及以后的版本才能支持。
現(xiàn)在我們就按照“多階段構(gòu)建”的語法將上面的Dockerfile.build.alpine和Dockerfile.target.alpine合并到一個Dockerfile中:
//Dockerfile
FROM golang:alpine as builder
WORKDIR /go/src
COPY httpserver.go .
RUN go build -o httpd ./httpserver.go
From alpine:latest
WORKDIR /root/
COPY --from=builder /go/src/httpd .
RUN chmod +x /root/httpd
ENTRYPOINT ["/root/httpd"]
Dockerfile的語法還是很簡明和易理解的库快。即使是你第一次看到這個語法也能大致猜出六成含義摸袁。與之前Dockefile最大的不同在于在支持多階段構(gòu)建的Dockerfile中我們可以寫多個“From baseimage”的語句了,每個From語句開啟一個構(gòu)建階段义屏,并且可以通過“as”語法為此階段構(gòu)建命名(比如這里的builder)靠汁。我們還可以通過COPY命令在兩個階段構(gòu)建產(chǎn)物之間傳遞數(shù)據(jù),比如這里傳遞的httpd應(yīng)用闽铐,這個工作之前我們是使用“膠水”代碼完成的蝶怔。
構(gòu)建目標(biāo)鏡像:
# docker build -t repodemo/httpd-multi-stage .
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd-multi-stage latest 35e494aa5c6f 2 minutes ago 16.2MB
我們看到通過多階段構(gòu)建特性構(gòu)建的Docker Image與我們之前通過builder模式構(gòu)建的鏡像在效果上是等價(jià)的。
六兄墅、來到現(xiàn)實(shí)
沿著時(shí)間的軌跡添谊,Docker 鏡像構(gòu)建走到了今天。追求又快又小的鏡像已成為了 Docker 社區(qū) 的共識察迟。社區(qū)在自創(chuàng) builder 鏡像構(gòu)建的最佳實(shí)踐后終于迎來了多階段構(gòu)建這柄利器斩狱,從此構(gòu)建 出極簡的鏡像將不再困難。
七扎瓶、總結(jié)
所以所踊,我們看過了Docker鏡像構(gòu)建的這個過程,也了解到了我們?nèi)绾我徊讲綁嚎s鏡像體積的方法概荷,不過對于正式環(huán)境來說秕岛,鏡像越小并不意味著越好,因?yàn)樾◇w積的鏡像是有可能會導(dǎo)致一些語言的適配性不是很好误证,這點(diǎn)還是需要大量測試之后才能正式使用继薛。