原文地址:Docker Images : Part I - Reducing Image Size
介紹
在開(kāi)始使用容器時(shí)突雪,我們很容易對(duì)生成的鏡像大小感到震驚擦秽。在不犧牲開(kāi)發(fā)人員和操作人員的便利性的前提下,我們將回顧多種減少鏡像大小的技術(shù)。在第一部分中,我們將討論多階段構(gòu)建挂捻,因?yàn)槿魏稳讼胍獪p小鏡像大小,都應(yīng)該從這里開(kāi)始船万。我們還將說(shuō)明靜態(tài)鏈接和動(dòng)態(tài)鏈接之間的區(qū)別刻撒,以及我們?yōu)槭裁匆P(guān)注這些。這也是介紹Alpine的機(jī)會(huì)耿导。
在第二部分中声怔,我們將看到與各種流行語(yǔ)言相關(guān)的一些特殊性。我們將討論Go碎节,以及Java捧搞,Node抵卫,Python狮荔,Ruby和Rust胎撇。我們還將討論有關(guān)Alpine的更多信息,以及如何全面利用Alpine殖氏。
在第三部分中晚树,我們將介紹一些與大多數(shù)語(yǔ)言和框架相關(guān)的模式(和反模式!)雅采,例如使用通用基本鏡像爵憎,剝離二進(jìn)制文件并減小大小。我們將總結(jié)一些更奇特的或高級(jí)的方法婚瓜,例如Bazel宝鼓,Distroless,DockerSlim或UPX巴刻。我們將看到其中的一些在某些情況下會(huì)適得其反愚铡,但在某些特定情況下可能會(huì)有用。
請(qǐng)注意胡陪,示例代碼以及此處提到的所有Dockerfile沥寥,都可以在公共GitHub存儲(chǔ)庫(kù)中方便地獲得,所有鏡像都帶有用來(lái)構(gòu)建的Compose文件柠座,并可以輕松比較它們的大小邑雅。
我們要解決的問(wèn)題
我敢打賭,每個(gè)構(gòu)建了第一個(gè)Docker鏡像并編譯了一些代碼的人都對(duì)該鏡像的大新杈(不是很好)感到驚訝淮野。
看一下用C編寫(xiě)的這個(gè)“ hello world”程序:
/* hello.c */
int main () {
puts("Hello, world!");
return 0;
}
我們可以使用以下Dockerfile構(gòu)建它:
FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]
…但是生成的鏡像將超過(guò)1 GB,因?yàn)樗鼘麄€(gè)gcc鏡像狂塘!
如果使用Ubuntu鏡像录煤,安裝C編譯器并構(gòu)建程序,則會(huì)得到300 MB的鏡像荞胡;看起來(lái)更好了妈踊,但對(duì)于小于20 kB的二進(jìn)制文件而言,仍然太多了:
$ ls -l hello
-rwxr-xr-x 1 root root 16384 Nov 18 14:36 hello
與等效的Go程序的情況相同:
package main
import "fmt"
func main () {
fmt.Println("Hello, world!")
}
使用該golang鏡像構(gòu)建此代碼泪漂,即使hello程序只有2 MB 廊营,生成的鏡像仍為800 MB:
$ ls -l hello
-rwxr-xr-x 1 root root 2008801 Jan 15 16:41 hello
一定有更好的方法!
讓我們看看如何大幅度減小這些鏡像的大小萝勤。在某些情況下露筒,我們可以實(shí)現(xiàn)99.8%的尺寸減小(但是敌卓,這么大的減幅并不總是一個(gè)好事)慎式。
提示:為了輕松比較鏡像的大小,我們將使用相同的鏡像名稱(chēng),但使用不同的標(biāo)簽瘪吏。舉例來(lái)說(shuō)癣防,我們的鏡像會(huì)叫做hello:gcc,hello:ubuntu掌眠,hello:thisweirdtrick等蕾盯,我們就可以運(yùn)行docker images hello,它會(huì)列出所有標(biāo)簽為hello的鏡像蓝丙,并標(biāo)記它們的大小级遭,在我們自己的Docker引擎上不會(huì)被其他的鏡像干擾。
多階段構(gòu)建
這是減小鏡像尺寸的第一步(也是最有效的一步)渺尘。不過(guò)挫鸽,我們需要小心,因?yàn)槿绻幚聿徽_鸥跟,可能會(huì)導(dǎo)致鏡像難以繼續(xù)操作(甚至可能完全損壞)掠兄。
多階段構(gòu)建來(lái)自一個(gè)簡(jiǎn)單的想法:“我不需要在最終的鏡像中包括C或Go編譯器以及整個(gè)構(gòu)建工具鏈。我只想傳輸二進(jìn)制文件锌雀!”
我們通過(guò)在Dockerfile中添加另一行FROM來(lái)獲得多階段構(gòu)建蚂夕。看下面的例子:
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=mybuildstage hello .
CMD ["./hello"]
我們使用gcc鏡像來(lái)構(gòu)建我們的hello.c程序腋逆。然后婿牍,我們使用該ubuntu鏡像開(kāi)始一個(gè)新階段(我們稱(chēng)為“運(yùn)行階段”)。我們從上一階段復(fù)制hello二進(jìn)制文件惩歉。最終鏡像為64 MB等脂,而不是1.1 GB,因此大小減少了約95%:
$ docker images minimage
REPOSITORY TAG ... SIZE
minimage hello-c.gcc ... 1.14GB
minimage hello-c.gcc.ubuntu ... 64.2MB
還不錯(cuò)吧撑蚌?我們可以做得更好上遥。但是首先來(lái)了解一些技巧和警告。
在聲明構(gòu)建階段時(shí)争涌,不必使用AS關(guān)鍵字粉楚。從上一個(gè)階段復(fù)制文件時(shí),你只需指明該構(gòu)建階段的編號(hào)(從零開(kāi)始)亮垫。
換句話說(shuō)模软,以下兩行是等效的:
COPY --from=mybuildstage hello .
COPY --from=0 hello .
就個(gè)人而言,我認(rèn)為在構(gòu)建階段中饮潦,對(duì)于較短的Dockerfile(例如燃异,少于10行)使用數(shù)字是很好的,但是當(dāng)Dockerfile變長(zhǎng)(并且可能更復(fù)雜继蜡,具有多個(gè)構(gòu)建階段)回俐,最好用明確的命名逛腿。這將有助于你的團(tuán)隊(duì)成員進(jìn)行維護(hù)(以及未來(lái)幾個(gè)月后你可能也會(huì)回來(lái)檢查)。
警告:使用經(jīng)典鏡像
我強(qiáng)烈建議在“運(yùn)行”階段使用經(jīng)典鏡像仅颇■猓“經(jīng)典”是指CentOS,Debian灵莲,F(xiàn)edora,Ubuntu等一些熟悉的鏡像殴俱。你可能聽(tīng)說(shuō)過(guò)Alpine政冻,并很想使用它。千萬(wàn)不要线欲!至少現(xiàn)在還不是時(shí)候明场。稍后我們將討論Alpine,并解釋為什么我們需要謹(jǐn)慎使用Alpine李丰。
警告:COPY --from 使用絕對(duì)路徑
從上一階段復(fù)制文件時(shí)苦锨,路徑被解釋為相對(duì)于上一階段的根目錄的相對(duì)路徑。
一旦我們使用帶有WORKDIR的構(gòu)建器鏡像(例如golang鏡像)趴泌,問(wèn)題就會(huì)出現(xiàn)舟舒。
如果我們嘗試構(gòu)建此Dockerfile:
FROM golang
COPY hello.go .
RUN go build hello.go
FROM ubuntu
COPY --from=0 hello .
CMD ["./hello"]
我們會(huì)得到與以下錯(cuò)誤類(lèi)似的錯(cuò)誤:
COPY failed: stat /var/lib/docker/overlay2/1be...868/merged/hello: no such file or directory
這是因?yàn)镃OPY命令嘗試復(fù)制/hello,但是由于WORKDIR在 golang是/go嗜憔,所以程序路徑實(shí)際上是/go/hello秃励。
如果我們?cè)跇?gòu)建中使用正式(或非常穩(wěn)定)的鏡像,則可以指定完整的絕對(duì)路徑吉捶。
但是夺鲜,如果將來(lái)我們的構(gòu)建或運(yùn)行鏡像可能會(huì)更改,我建議在構(gòu)建映像中指定一個(gè)WORKDIR呐舔。這將確保文件在期望的位置币励,即使未來(lái)用于構(gòu)建的基礎(chǔ)鏡像發(fā)生更改。
遵循此原則珊拼,用于構(gòu)建Go程序的Dockerfile如下所示:
FROM golang
WORKDIR /src
COPY hello.go .
RUN go build hello.go
FROM ubuntu
COPY --from=0 /src/hello .
CMD ["./hello"]
如果想知道Golang多階段構(gòu)建的效率食呻,它可以從800 MB的鏡像下降到66 MB的鏡像:
$ docker images minimage
REPOSITORY TAG ... SIZE
minimage hello-go.golang ... 805MB
minimage hello-go.golang.ubuntu-workdir ... 66.2MB
使用 FROM scratch
回到我們的“ Hello World”程序。C版本為16 kB澎现,Go版本為2 MB搁进。我們可以得到這么大的鏡像嗎?
我們可以?xún)H使用二進(jìn)制文件而不用其他文件來(lái)構(gòu)建鏡像嗎昔头?
可以! 我們要做的就是使用多階段構(gòu)建饼问,然后選擇scratch作為我們的運(yùn)行鏡像。scratch是虛擬鏡像揭斧,不能拉取或運(yùn)行它莱革,因?yàn)樗耆强盏木摺_@就是為什么Dockerfile以FROM scratch開(kāi)頭的原因,這意味著我們是從頭開(kāi)始構(gòu)建的盅视,沒(méi)有使用任何預(yù)先存在的成分捐名。
這為我們提供了以下Dockerfile:
FROM golang
COPY hello.go .
RUN go build hello.go
FROM scratch
COPY --from=0 /go/hello .
CMD ["./hello"]
如果我們構(gòu)建該鏡像,則其大小恰好是二進(jìn)制文件的大心只鳌(2 MB)镶蹋,并且可以正常工作!
但是赏半,在scratch用作基礎(chǔ)時(shí)贺归,需要牢記一些注意事項(xiàng)。
沒(méi)有shell
該scratch鏡像沒(méi)有命令解析器外殼断箫。這意味著我們不能將字符串語(yǔ)法與CMD(或RUN)一起使用拂酣。考慮以下Dockerfile:
...
FROM scratch
COPY --from=0 /go/hello .
CMD ./hello
如果嘗試docker run生成結(jié)果鏡像仲义,則會(huì)收到以下錯(cuò)誤消息:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown.
它的顯示方式不是很清楚婶熬,但是核心信息在這里:鏡像中缺少/bin/sh。
發(fā)生這種情況是因?yàn)楫?dāng)我們將字符串語(yǔ)法與CMD或RUN 一起使用時(shí)埃撵,參數(shù)將傳遞給/bin/sh赵颅。這意味著我們CMD ./hello上面會(huì)執(zhí)行/bin/sh -c "./hello",因?yàn)樵趕cratch鏡像中不存在/bin/sh暂刘,進(jìn)而導(dǎo)致失敗性含。
解決方法很簡(jiǎn)單:在Dockerfile中使用JSON語(yǔ)法。CMD ./hello改為CMD ["./hello"]鸳惯。當(dāng)Docker檢測(cè)到JSON語(yǔ)法時(shí)商蕴,它將直接運(yùn)行參數(shù),而無(wú)需使用shell芝发。
沒(méi)有調(diào)試工具
根據(jù)scratch
定義绪商,該鏡像為空;因此它沒(méi)有任何幫助我們查詢(xún)?nèi)萜鲉?wèn)題的方法辅鲸。無(wú)shell(正如我們?cè)谏弦欢握f(shuō)的)也沒(méi)什么ls
格郁,ps
,ping
独悴,等等例书。這意味著我們無(wú)法向容器輸入內(nèi)容(使用docker exec
或kubectl exec
進(jìn)行查看)。
(請(qǐng)注意刻炒,嚴(yán)格來(lái)說(shuō)决采,有一些方法可以對(duì)我們的容器進(jìn)行故障追蹤。我們可以docker cp
用來(lái)從容器中取出文件坟奥;我們可以docker run --net container:
用來(lái)與網(wǎng)絡(luò)堆棧進(jìn)行交互树瞭;像nsenter
這樣的低級(jí)工具可能非常強(qiáng)大拇厢。 Kubernetes最近的版本具有臨時(shí)容器的概念,但它仍然處于alpha狀態(tài)晒喷。請(qǐng)記住孝偎,所有這些技術(shù)肯定會(huì)使我們的工作變得更加復(fù)雜,尤其是當(dāng)我們有很多事情要做的時(shí)候A骨谩)
這里的一個(gè)解決辦法是使用類(lèi)似busybox
或alpine
的鏡像代替scratch
衣盾。當(dāng)然,它們更大(分別為1.2 MB和5.5 MB)爷抓,但是在龐大的程序中势决,如果將其與原始圖像的數(shù)百兆字節(jié)或千兆字節(jié)進(jìn)行比較,付出的代價(jià)其實(shí)很小废赞。
沒(méi)有l(wèi)ibc
這是一個(gè)更加棘手的問(wèn)題。我們?cè)贕o中使用簡(jiǎn)單的“ hello world”可以很好地工作叮姑,但是唉地,如果我們嘗試在scratch鏡像中放置C程序,或者是在更復(fù)雜的Go程序(例如传透,使用網(wǎng)絡(luò)類(lèi)庫(kù)的任何程序)耘沼,則會(huì)收到以下錯(cuò)誤消息:
standard_init_linux.go:211: exec user process caused "no such file or directory"
某些文件似乎丟失了。但這并不能告訴我們確切缺少哪個(gè)文件朱盐。
丟失的文件是運(yùn)行我們的程序所必需的動(dòng)態(tài)庫(kù)群嗤。
什么是動(dòng)態(tài)庫(kù),為什么我們需要它兵琳?
程序編譯后狂秘,將與所使用的庫(kù)鏈接。(很簡(jiǎn)單躯肌,我們的“ hello world”程序也在使用庫(kù)者春,就是puts函數(shù)。)很久以前(90年代之前)清女,我們主要使用靜態(tài)鏈接钱烟,這意味著所使用的所有庫(kù)將包含在二進(jìn)制文件中。當(dāng)從軟盤(pán)或磁帶執(zhí)行軟件時(shí)嫡丙,或者根本沒(méi)有標(biāo)準(zhǔn)庫(kù)時(shí)拴袭,這是完美的選擇。但是曙博,在像Linux這樣的分時(shí)系統(tǒng)上拥刻,我們運(yùn)行許多并發(fā)程序,這些程序存儲(chǔ)在硬盤(pán)上父泳。這些程序幾乎總是使用標(biāo)準(zhǔn)的C庫(kù)泰佳。
在這種情況下盼砍,使用動(dòng)態(tài)鏈接會(huì)變得更加有利。使用動(dòng)態(tài)鏈接逝她,最終的二進(jìn)制文件不包含它使用的所有庫(kù)的代碼浇坐。相反,它包含這些庫(kù)的引用黔宛,如“這個(gè)程序需要的功能cos
和sin
和tan
來(lái)自libtrigonometry.so
近刘。執(zhí)行程序時(shí),系統(tǒng)會(huì)查找libtrigonometry.so
并將其與程序一起加載臀晃,以便程序可以調(diào)用這些函數(shù)觉渴。
動(dòng)態(tài)鏈接具有多個(gè)優(yōu)點(diǎn)。
- 由于不再需要復(fù)制通用庫(kù)徽惋,因此可以節(jié)省磁盤(pán)空間案淋。
- 由于這些庫(kù)可以從磁盤(pán)加載一次,然后在多個(gè)程序之間共享险绘,因此可以節(jié)省內(nèi)存踢京。
- 這使維護(hù)更加容易,因?yàn)樵诟聨?kù)時(shí)宦棺,我們不需要使用該庫(kù)重新編譯所有程序瓣距。
(如果我們想更透徹一點(diǎn),內(nèi)存節(jié)省不是動(dòng)態(tài)庫(kù)的結(jié)果代咸,而是共享庫(kù)的結(jié)果蹈丸。也就是說(shuō),兩者通衬沤妫可以并存逻杖。你知道嗎,在Linux上思瘟,動(dòng)態(tài)庫(kù)文件通常具有擴(kuò)展名.so
弧腥,即代表共享庫(kù)(share object)。在Windows上是.DLL
潮太,它代表動(dòng)態(tài)鏈接庫(kù)(Dynamic-link library管搪。
回過(guò)頭看我們的程序:默認(rèn)情況下,C程序是動(dòng)態(tài)鏈接的铡买。對(duì)于某些包更鲁,Go程序也是如此。我們的特定程序使用標(biāo)準(zhǔn)的C庫(kù)奇钞,該庫(kù)在最新的Linux系統(tǒng)上將在libc.so.6文件中澡为。因此,要運(yùn)行我們的程序景埃,需要將該文件放到在容器鏡像中媒至。如果使用scratch顶别,則顯然沒(méi)有該文件。如果我們使用busybox或alpine情況是相同的拒啰,因?yàn)閎usybox它不包含標(biāo)準(zhǔn)庫(kù)驯绎,alpine正在使用另一個(gè)不兼容的庫(kù)。稍后我們將詳細(xì)介紹谋旦。
我們?cè)撊绾谓鉀Q剩失?至少有3種方案。
構(gòu)建靜態(tài)二進(jìn)制文件
我們可以告訴我們的工具鏈制作一個(gè)靜態(tài)二進(jìn)制文件册着。有多種方法可以實(shí)現(xiàn)這一目標(biāo)(取決于我們起初構(gòu)建程序的方式)拴孤,但是如果使用gcc,我們需要添加-static到命令行中:
gcc -o hello hello.c -static
現(xiàn)在生成的二進(jìn)制文件是760 kB(在我的系統(tǒng)上)甲捏,而不是16 kB演熟。當(dāng)然,我們將庫(kù)嵌入到了二進(jìn)制文件中司顿,因此它要大得多芒粹。但是該二進(jìn)制文件現(xiàn)在可以在scratch鏡像中正確運(yùn)行。
如果使用Alpine構(gòu)建靜態(tài)二進(jìn)制文件免猾,則可以得到更小的圖像是辕。結(jié)果小于100 kB囤热!
將庫(kù)添加到我們的鏡像
我們可以使用ldd
工具找出程序需要哪些庫(kù):
$ ldd hello
linux-vdso.so.1 (0x00007ffdf8acb000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ff897ef6000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff8980f7000)
我們可以看到程序所需的庫(kù)猎提,以及系統(tǒng)找到它們的實(shí)際路徑。
在上面的示例中旁蔼,唯一的“真實(shí)”庫(kù)是libc.so.6
锨苏。linux-vdso.so.1
與一種稱(chēng)為VDSO(虛擬動(dòng)態(tài)共享對(duì)象)的機(jī)制有關(guān),該機(jī)制可以加速某些系統(tǒng)調(diào)用棺聊。讓我們假裝它不在那里伞租。至于ld-linux-x86-64.so.2
,它實(shí)際上是動(dòng)態(tài)鏈接器本身限佩。(從技術(shù)上講葵诈,我們的hello
二進(jìn)制文件包含的信息表述:“嘿,這是一個(gè)動(dòng)態(tài)程序祟同,并且知道如何將其所有部分放在一起作喘,這就是ld-linux-x86-64.so.2
”。)
如果我們?cè)敢?/em>晕城,可以將上面ldd
列出的所有文件手動(dòng)添加到鏡像中泞坦。這將是相當(dāng)繁瑣且難以維護(hù)的,尤其是對(duì)于程序有很多依賴(lài)的情況砖顷。對(duì)于我們小的hello world程序贰锁,我們可以這么做赃梧。但是對(duì)于更復(fù)雜的程序,例如使用DNS的程序豌熄,我們會(huì)遇到另一個(gè)問(wèn)題授嘀。GNU C庫(kù)(在大多數(shù)Linux系統(tǒng)上使用)通過(guò)相當(dāng)復(fù)雜的稱(chēng)為名稱(chēng)服務(wù)開(kāi)關(guān)(簡(jiǎn)稱(chēng)為NSS )的機(jī)制實(shí)現(xiàn)DNS(以及其他一些功能)。該機(jī)制需要一個(gè)配置文件/etc/nsswitch.conf
和其他庫(kù)房轿。但是這些庫(kù)沒(méi)有在ldd
中展現(xiàn)粤攒,因?yàn)樗鼈兩院髸?huì)在程序運(yùn)行時(shí)加載。如果我們希望DNS解析正常工作囱持,我們?nèi)匀恍枰ㄋ鼈儯夯接。ㄟ@些庫(kù)通常位于/lib64/libnss_*
。)
我個(gè)人不建議這樣做纷妆,因?yàn)樗苌衩乜福y以維護(hù),將來(lái)很可能會(huì)被改變掩幢。
使用 busybox:glibc
有專(zhuān)門(mén)為解決所有這些問(wèn)題而設(shè)計(jì)的鏡像:busybox:glibc逊拍。busybox是一個(gè)小鏡像(5 MB),提供了許多用于故障排除和操作的有用工具际邻,并提供了GNU C庫(kù)(或glibc)芯丧。該鏡像恰好包含我們前面提到的所有這些討厭的文件。如果要在小的鏡像中運(yùn)行動(dòng)態(tài)二進(jìn)制文件世曾,可以使用此方法缨恒。
但是請(qǐng)記住,如果我們的程序使用其他庫(kù)轮听,則也需要復(fù)制這些庫(kù)骗露。
總結(jié)和(部分)結(jié)論
讓我們看看在C. Spoiler alert中如何為“ hello world”程序做些事情:此列表包括了通過(guò)使用Alpine獲得的結(jié)果,本系后續(xù)文章會(huì)介紹這種方式血巍。
- 原始鏡像內(nèi)置gcc:1.14 GB
- gcc和ubuntu多級(jí)構(gòu)建:64.2 MB
- 使用alpine萧锉,靜態(tài)glibc二進(jìn)制:6.5 MB
- 使用alpine,動(dòng)態(tài)二進(jìn)制:5.6 MB
- 使用scratch述寡,靜態(tài)二進(jìn)制:940 kB
- 使用scratch柿隙,靜態(tài)musl二進(jìn)制:94 kB
大小減少了12000倍,磁盤(pán)空間減少了99.99%鲫凶。
不錯(cuò)禀崖。
就個(gè)人而言,我不會(huì)使用scratch鏡像(因?yàn)閷?duì)它們進(jìn)行故障定位可能會(huì)很麻煩)掀序,但是如果你要這樣做帆焕,那么它們就在這里!
在下一部分中,我們將介紹Go語(yǔ)言特定的一些方面叶雹,包括cgo和標(biāo)簽财饥。我們還將介紹其他流行語(yǔ)言,并且我們將討論更多有關(guān)Alpine的信息,它非常棒。