Docker鏡像:第三部分-進(jìn)一步優(yōu)化鏡像大小

原文地址:Docker Images : Part III - Going Farther To Reduce Image Size

介紹

在本系列的前兩部分中,我們介紹了優(yōu)化Docker鏡像大小的最常用方法。我們看到了多階段構(gòu)建,結(jié)合基于Alpine的鏡像以及有時(shí)是靜態(tài)構(gòu)建的方式,通??梢詾槲覀儙碜畲蟮目臻g優(yōu)化。在最后一部分中,我們將進(jìn)行更深一步探討。我們將討論標(biāo)準(zhǔn)化基礎(chǔ)鏡像,剝離二進(jìn)制文件,項(xiàng)目?jī)?yōu)化以及其他構(gòu)建系統(tǒng)或附加組件,例如DockerSlim或Bazel,以及NixOS發(fā)行版。

我們還將討論一些我們?cè)缦冗z漏的小細(xì)節(jié),但這些細(xì)節(jié)很重要,例如時(shí)區(qū)文件和證書。

公共基礎(chǔ)配置

如果我們的節(jié)點(diǎn)并行運(yùn)行許多容器(甚至只有幾個(gè)),那么有一件事也可以節(jié)省大量資源。

Docker鏡像由layer組成。每一個(gè)layer都可以添加,刪除或更改文件。就像代碼存儲(chǔ)庫中的提交代碼或從另一個(gè)類繼承的類一樣。當(dāng)我們執(zhí)行時(shí)docker build,Dockerfile的每一行都會(huì)生成一個(gè)layer。傳輸鏡像時(shí),僅傳輸目標(biāo)上尚不存在的layer。

layer不僅節(jié)省了網(wǎng)絡(luò)帶寬,還節(jié)省了存儲(chǔ)空間:如果多個(gè)鏡像共享layer,則Docker只需要存儲(chǔ)一次這些layer。并且,根據(jù)所使用的存儲(chǔ)驅(qū)動(dòng)程序,layer還可以節(jié)省磁盤I / O和內(nèi)存,因?yàn)楫?dāng)多個(gè)容器需要從layer讀取相同的文件時(shí),系統(tǒng)將僅讀取和緩存這些文件一次。(overlay2和aufs驅(qū)動(dòng)程序就是這種情況。)

這意味著,如果我們?cè)谶\(yùn)行多個(gè)容器的節(jié)點(diǎn)中嘗試優(yōu)化網(wǎng)絡(luò)和磁盤訪問以及內(nèi)存使用率,則可以通過確保這些容器運(yùn)行的鏡像具有盡可能多的公共layer來節(jié)省大量資源。

這可能直接違反我們之前給出的一些準(zhǔn)則!例如,如果我們使用靜態(tài)二進(jìn)制文件構(gòu)建超級(jí)優(yōu)化的鏡像,則這些二進(jìn)制文件可能比其動(dòng)態(tài)等效文件大10倍。讓我們假設(shè)一個(gè)場(chǎng)景,運(yùn)行10個(gè)容器,每個(gè)容器使用帶有這些二進(jìn)制文件之一的不同鏡像。

方案1:scratch鏡像中的靜態(tài)二進(jìn)制文件

每個(gè)鏡像的占用:10 MB
10個(gè)鏡像的占用:100 MB

方案2:使用ubuntu鏡像的動(dòng)態(tài)二進(jìn)制文件(64 MB)

每個(gè)鏡像的單個(gè)占用:65 MB
每個(gè)鏡像的細(xì)目:64 MB用于ubuntu+ 1 MB用于特定的二進(jìn)制文件
磁盤總使用量:74 MB(單個(gè)layer10x1 MB +共享層64 MB)

方案3:使用alpine鏡像的動(dòng)態(tài)二進(jìn)制文件(5.5 MB)

每個(gè)鏡像的單個(gè)占用:6.5 MB
每個(gè)鏡像的細(xì)目:alpine 5.5 MB + 特定的二進(jìn)制文件1 MB
磁盤總使用量:15.5 MB
最初,這些靜態(tài)二進(jìn)制文件看起來不錯(cuò),但是在這種情況下,它們會(huì)適得其反。鏡像將需要更多的磁盤空間,需要更長(zhǎng)的傳輸時(shí)間并使用更多的RAM!

但是,為了使這些方案起作用,我們需要確保所有鏡像實(shí)際上都使用完全相同的標(biāo)準(zhǔn)。如果我們某些使用centos鏡像,而其他使用debian,這是行不通的。即使我們使用比如 ubuntu:16.04和ubuntu:18.04,兩個(gè)不同版本的ubuntu!這意味著在更新基礎(chǔ)鏡像時(shí),我們應(yīng)該重建所有鏡像,以確保所有容器中的鏡像都是一致的。

這也意味著我們需要良好的管理和團(tuán)隊(duì)之間的良好溝通。你可能會(huì)想,“這不是個(gè)技術(shù)問題!”,那么你是對(duì)的!這不是技術(shù)問題。這意味著對(duì)于某些人來說,解決起來會(huì)困難得多,因?yàn)槟銦o法自己無法解決的工作量很多:你將不得不讓其他人參與進(jìn)來!也許你堅(jiān)持使用Debian,但是另一個(gè)團(tuán)隊(duì)堅(jiān)持使用Fedora。如果你想使用通用基礎(chǔ),則必須說服其他團(tuán)隊(duì)。這也意味著你必須接受他們也可以說服你的結(jié)果。結(jié)論:在某些情況下,最有效的解決方案是需要溝通能力而非技術(shù)能力!

最后,在特定情況下,靜態(tài)鏡像仍然有用:當(dāng)我們知道我們的鏡像將被部署在異構(gòu)環(huán)境中時(shí);或它們將是在給定節(jié)點(diǎn)上運(yùn)行的唯一對(duì)象。在這種情況下,無論如何都不會(huì)發(fā)生任何共享。

剝離和轉(zhuǎn)換

還有一些并非特定于容器的其他技術(shù),這些技術(shù)可以從我們的鏡像中刪除幾兆字節(jié)(有時(shí)甚至是千兆字節(jié))。

剝離二進(jìn)制文件

默認(rèn)情況下,大多數(shù)編譯器會(huì)生成帶有標(biāo)記的二進(jìn)制文件,這些標(biāo)記對(duì)于調(diào)試定位問題很有用,但執(zhí)行時(shí)并不是必選項(xiàng)。strip工具可以刪除這些標(biāo)記。這不太可能改變程序本身的運(yùn)行方式,但是如果你處在每個(gè)字節(jié)都很重要的情況下,那這肯定會(huì)有所幫助。

資源處理

如果我們的容器鏡像包含媒體文件,是否可以縮小這些文件,例如通過使用不同的文件格式或解碼器?我們可以將它們托管在其他地方,以使我們發(fā)送的鏡像更小嗎?如果代碼經(jīng)常更改,而資源卻不更改,那后者特別有用。在這種情況下,我們應(yīng)盡量避免在每次發(fā)布新版本的代碼時(shí)都重新生成這些資源。

壓縮:不是一個(gè)好主意

如果要減小鏡像的大小,為什么不壓縮文件?HTML,JavaScript,CSS之類的資源使用zip或gzip應(yīng)該可以很好地壓縮。還有更有效的方法,例如bzip2、7z,lzma。首先,它看起來像是一種減小鏡像大小的簡(jiǎn)單方法。但是,如果我們的計(jì)劃是在使用之前先解壓縮這些資源,那么我們最終將浪費(fèi)資源!

Layer在傳輸之前已經(jīng)被壓縮,因此提取鏡像不會(huì)更快。而且,如果我們需要解壓縮文件,則磁盤使用率將比以前更高,因?yàn)樵诖疟P上,我們現(xiàn)在將同時(shí)擁有文件的壓縮版本和未壓縮版本!更糟糕的是:如果這些文件位于共享Layer上,那么共享將不會(huì)帶來任何好處,因?yàn)樵谶\(yùn)行容器時(shí)我們將解壓縮的這些文件將不會(huì)被共享。

那么UPX怎么樣?如果你不熟悉UPX,那么它是一個(gè)出色的工具,可以減少二進(jìn)制文件的大小。如果我們想減少容器的占用空間,UPX缺會(huì)適得其反。首先,磁盤和網(wǎng)絡(luò)的使用不會(huì)減少,因?yàn)闊o論如何Layer都是壓縮的。因此UPX不會(huì)在這里給我們?nèi)魏螏椭?/p>

當(dāng)運(yùn)行普通的二進(jìn)制文件時(shí),它會(huì)映射到內(nèi)存中,以便僅在需要時(shí)才加載(或“分頁”)所需的字節(jié)。運(yùn)行使用UPX壓縮的二進(jìn)制文件時(shí),必須在內(nèi)存中解壓縮整個(gè)二進(jìn)制文件。這會(huì)導(dǎo)致更高的內(nèi)存使用率和更長(zhǎng)的啟動(dòng)時(shí)間,尤其是對(duì)于像Go運(yùn)行時(shí),它往往會(huì)生成更大的二進(jìn)制文件。

(我曾經(jīng)嘗試在hyperkube二進(jìn)制文件上使用UPX,嘗試在KVM中構(gòu)建優(yōu)化的節(jié)點(diǎn)鏡像并運(yùn)行在本地Kubernetes集群。結(jié)果卻并不順利,因?yàn)殡m然它減少了我的VM的磁盤使用量,但它們的內(nèi)存使用量卻上升了,很多?。?/p>

一些其他小技巧

還有其他工具可以幫助我們獲得較小的圖像尺寸。這將不是一個(gè)詳盡的清單...

DockerSlim

DockerSlim使用了一種幾乎不可思議的技術(shù)來減小鏡像的大小。我不知道它到底是如何工作的(除了自述文件中的設(shè)計(jì)說明),因此我將進(jìn)行有根據(jù)的猜測(cè)。我想DockerSlim運(yùn)行我們的容器,并檢查容器中運(yùn)行的程序訪問了哪些文件。然后刪除其他文件?;谶@一猜測(cè),在使用DockerSlim之前,我會(huì)非常小心,因?yàn)樵S多框架會(huì)動(dòng)態(tài)或延遲地(即首次需要它們時(shí))加載文件。

為了驗(yàn)證該假設(shè),我嘗試使用一個(gè)簡(jiǎn)單的Django應(yīng)用程序來測(cè)試DockerSlim。DockerSlim將其從200 MB減少到30 MB,表現(xiàn)的非常好!但是,盡管該應(yīng)用程序的首頁運(yùn)行正常,但許多鏈接卻被破壞了。我想這是因?yàn)镈ockerSlim尚未檢測(cè)到它們的模板,并且它們也沒有包含在最終鏡像中。錯(cuò)誤報(bào)告本身也被破壞,可能是因?yàn)橛糜陲@示和發(fā)送異常的模塊也被忽略了。任何可以動(dòng)態(tài)地import的模塊,python代碼都會(huì)在運(yùn)行時(shí)才進(jìn)行加載。

不過請(qǐng)不要誤會(huì)我的意思:在許多情況下,DockerSlim仍然可以為我們創(chuàng)造奇跡!與往常一樣,當(dāng)有這樣一個(gè)非常強(qiáng)大的工具時(shí),了解它的內(nèi)部結(jié)構(gòu)將非常有幫助,因?yàn)樗梢詭椭覀儗?duì)它的工作方式有一個(gè)很好的理解。

Distroless

Distroless鏡像是使用外部工具構(gòu)建的最小鏡像的集合,無需使用經(jīng)典的Linux分發(fā)程序包管理器。它產(chǎn)生的鏡像非常小,但是沒有基本的調(diào)試工具,也沒有簡(jiǎn)單的安裝方法。

就個(gè)人喜好而言,我更喜歡擁有一個(gè)軟件包管理器和一個(gè)熟悉的發(fā)行版,因?yàn)檎l知道我可能需要什么額外的工具來解決容器問題?Alpine只有5.5 MB,它允許我能夠安裝所需的幾乎所有東西。我不知道是否要放棄這點(diǎn)!但是,如果你有全面的方法來對(duì)容器進(jìn)行故障排查,無需依賴鏡像中的工具,那么你確實(shí)可以通過Distroless節(jié)省一些額外的空間。

此外,基于的Alpine鏡像通常會(huì)比其Distroless鏡像小。所以你可能想知道:既然如此為什么我們還要去了解Distroless?至少有兩個(gè)原因。

首先,從安全角度考慮,Distroless使你獲得的鏡像非常小。更少的內(nèi)容意味著更少的潛在漏洞。

其次,Distroless圖像是使用Bazel構(gòu)建的,因此,如果你想學(xué)習(xí)或試驗(yàn)或使用Bazel,它們是非常不錯(cuò)的入門示例的集合。Bazel到底是什么?很高興你提出這個(gè)問題,我將在下一部分中介紹!

Bazel(和其他替代)

有些構(gòu)建系統(tǒng)甚至不使用Dockerfile。Bazel是其中之一。Bazel的強(qiáng)大在于它可以表達(dá)我們的源代碼和它所構(gòu)建的目標(biāo)之間的復(fù)雜依賴關(guān)系,有點(diǎn)像Makefile。這樣就可以只重建需要重建的東西。無論是在我們的代碼中(在進(jìn)行小的本地更改時(shí))還是在我們的基本鏡像中(以便修補(bǔ)或升級(jí)庫都不會(huì)觸發(fā)所有鏡像的整個(gè)重建)。它還可以運(yùn)行等效的單元測(cè)試,并且僅對(duì)受代碼更改影響的模塊運(yùn)行測(cè)試。

這在非常大的代碼庫上特別有效。在某個(gè)時(shí)候,我們的構(gòu)建和測(cè)試系統(tǒng)可能需要幾個(gè)小時(shí)才能運(yùn)行,有時(shí)甚至幾天。我們可以花費(fèi)數(shù)小時(shí)部署并行構(gòu)建服務(wù)器場(chǎng)和測(cè)試環(huán)境,但這需要大量資源,并且無法再次在本地環(huán)境中運(yùn)行。這種場(chǎng)景下才是Bazel之類的真正發(fā)光時(shí)刻,因?yàn)樗鼘⒛軌蛟趲追昼妰?nèi)構(gòu)建并測(cè)試所需的內(nèi)容,而不是幾小時(shí)或幾天。

很棒!那我們應(yīng)該馬上跳到Bazel嗎?沒那么快。使用Bazel需要學(xué)習(xí)完全不同的構(gòu)建系統(tǒng),即使擁有上面提到的所有漂亮的多階段構(gòu)建以及靜態(tài)和動(dòng)態(tài)庫的精妙之處,使用Dockerfile都可能比普通的Dockerfile復(fù)雜得多。維護(hù)此構(gòu)建系統(tǒng)和相關(guān)配置將需要大量工作。盡管我本人沒有使用Bazel的第一手經(jīng)驗(yàn),但根據(jù)我周圍的經(jīng)驗(yàn),至少需要安排一名專職高級(jí)或總工程師來承擔(dān)配置和維護(hù)Bazel的工作。

如果我們的組織有數(shù)百名開發(fā)人員;建造或測(cè)試時(shí)間正在成為我們發(fā)展的主要障礙;那么選擇Bazel可能是一個(gè)好主意。否則,如果我們是一家處于起步階段的初創(chuàng)企業(yè)或小型組織,那么這可能不是個(gè)好選擇。除非我們有幾位工程師非常了解Bazel并想為其他所有人去配置它。

Nix

我決定增加一個(gè)有關(guān)Nix軟件包管理器的部分,因?yàn)樵诘?部分和第2部分發(fā)布之后,有些人對(duì)它充滿了熱情。

劇透警報(bào):是的,Nix可以幫助您獲得更好的構(gòu)建,但是學(xué)習(xí)曲線陡峭。也許不像Bazel那樣陡峭,但是也很接近了。你需要學(xué)習(xí)Nix,其概念,其自定義表達(dá)語言,以及如何使用它為你喜歡的語言和框架打包(有關(guān)示例,請(qǐng)參見nixpkgs手冊(cè))。

盡管如此,我還是想談?wù)凬ix,這有兩個(gè)原因:它的核心概念非常強(qiáng)大(可以幫助我們總體上對(duì)軟件打包有更好的理解),還有一個(gè)名為Nixery的特殊項(xiàng)目可以在部署容器時(shí)幫助我們。

什么是Nix?

我第一次聽說Nix大約是10年前,當(dāng)時(shí)我參加了一場(chǎng)會(huì)議演講。那時(shí),它已經(jīng)功能齊全且穩(wěn)定。這不是一個(gè)時(shí)髦的新鮮事物。

一點(diǎn)專業(yè)的解釋:

  • Nix是一個(gè)程序包管理器,可以在任何Linux機(jī)器以及macOS上安裝;
  • NixOS是基于Nix 的Linux發(fā)行版。
  • nixpkgs 是Nix的軟件包集合;
  • “派生(derivation)”是Nix構(gòu)建的秘訣。

Nix是功能性包管理器。“功能性”是指每個(gè)程序包都由其輸入(源代碼,依賴項(xiàng)...)及其派生(構(gòu)建方式)定義。如果我們使用相同的輸入和相同的構(gòu)建,我們將獲得相同的輸出。如果它使我們想起Docker構(gòu)建緩存,那是完全正常的:因?yàn)樗麄兪峭耆嗤南敕ǎ?/p>

在傳統(tǒng)系統(tǒng)上,當(dāng)程序包依賴于另一個(gè)程序包時(shí),該依賴關(guān)系通常表示得不是很精確。例如,在Debian中, python3.8依賴于,python3.8-minimal (= 3.8.2-1)而python3.8-minimal依賴于libc6 (>= 2.29)。另一方面,ruby2.5依賴于libc6 (>= 2.17)。因此,我們安裝單個(gè)版本的libc6,大多數(shù)情況下都能正常工作。

在Nix上,程序包取決于庫的確切版本,并且有一個(gè)非常巧妙的機(jī)制,每個(gè)程序都將使用自己的庫而不與其他庫沖突。(如果你對(duì)此感到疑惑:動(dòng)態(tài)鏈接程序使用鏈接器,該鏈接器被設(shè)置為使用來自特定路徑的庫。從概念上講,這與指定#!/usr/local/bin/my-custom-python-3.8使用特定版本的Python解釋器運(yùn)行Python腳本沒有什么不同。)

例如,當(dāng)程序使用C庫時(shí),在傳統(tǒng)系統(tǒng)上,它引用/usr/lib/libc.so.6,但是對(duì)于Nix,它可能引用了/nix/store/6yaj...drnn-glibc-2.27/lib/libc.so.6

看到那個(gè)/nix/store路徑了嗎?那是Nix倉庫。存儲(chǔ)在其中的東西是不可變的文件和目錄,由哈希標(biāo)識(shí)。從概念上講,Nix存儲(chǔ)類似于Docker使用的層(layer),但有一個(gè)很大的區(qū)別:Docker中各層相互疊加,而Nix存儲(chǔ)中的文件和目錄是不相交的。它們永遠(yuǎn)不會(huì)相互沖突(因?yàn)槊總€(gè)對(duì)象都存儲(chǔ)在不同的目錄中)。

在Nix上,“安裝軟件包”意味著在Nix倉庫中下載大量文件和目錄,然后設(shè)置配置文件(實(shí)際上是一堆符號(hào)鏈接,以便我們現(xiàn)在可以使用剛剛安裝的程序$PATH)。

Nix實(shí)踐

上面的聽起來很理論吧?讓我們看看Nix的實(shí)踐。

我們可以使用在容器中運(yùn)行Nix docker run -ti nixos/nix。

然后,我們可以使用nix-env --query或檢查安裝的軟件包nix-env -q。

它只會(huì)顯示給我們nix和nss-cacert。很奇怪,難道我們還沒有像Shell ls這樣的工具以及其他工具嗎?是的,但是在這個(gè)特定的容器鏡像中,它們是由靜態(tài)busybox可執(zhí)行文件提供的。

好了,我們?cè)撊绾伟惭b?我們可以nix-env --install redisniv-env -i redis。該命令的輸出向我們表明,它正在獲取新的“路徑”并將其放置在Nix倉庫中。它至少會(huì)為redis獲取一條“路徑”,很可能為glibc獲取另一條路徑。碰巧的是,Nix本身(例如nix-env二進(jìn)制文件和其他一些文件)也使用glibc,但它可能與redis使用的版本不同。如果運(yùn)行ls -ld /nix/store/*glibc*/我們將看到兩個(gè)目錄,分別對(duì)應(yīng)于glibc的兩個(gè)不同版本。在編寫這些行時(shí),我得到了以下兩個(gè)版本glibc-2.27:

ef5936ea667f:/# ls -ld /nix/store/*glibc*/
dr-xr-xr-x    ... /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/
dr-xr-xr-x    ... /nix/store/6yaj6n8l925xxfbcd65gzqx3dz7idrnn-glibc-2.27/

你可能會(huì)想:“等等,這不是同一版本嗎?” 是的,沒有!它們是相同的版本號(hào),但可能是用不同的選項(xiàng)構(gòu)建的。結(jié)果會(huì)有些許不同,因此從Nix的角度來看,這是兩個(gè)不同的對(duì)象。就像當(dāng)我們構(gòu)建相同的Dockerfile但在某處更改一行代碼時(shí)一樣,Docker構(gòu)建器會(huì)跟蹤這些微小差異并為我們提供兩個(gè)不同的鏡像。

我們可以通過nix-store --query --referencesnix-store -qR命令,展示Nix倉庫中任何文件的依賴關(guān)系。例如,要查看我們剛剛安裝的Redis二進(jìn)制文件的依賴性,我們可以這樣做 nix-store -qR $(which redis-server)。

在我的容器中,輸出如下所示:

/nix/store/6yaj6n8l925xxfbcd65gzqx3dz7idrnn-glibc-2.27
/nix/store/mzqjf58zasr7237g8x9hcs44p6nvmdv7-redis-5.0.5

這些目錄是我們?cè)谌魏蔚胤竭\(yùn)行Redis所需要的。是的,其中包括scratch。我們不需要任何額外的庫。(也許只是為了方便我們對(duì)$PATH進(jìn)行了調(diào)整,但這不是必要的。)

我們甚至可以使用Nix 配置文件來實(shí)現(xiàn)該過程。配置文件包含我們需要添加到$PATH目錄中的bin文件夾(以及其他一些內(nèi)容;為方便起見,我將其簡(jiǎn)化)。這意味著,如果我執(zhí)行 nix-env --profile myprof -i redis memcached, myprof/bin將包含Redis和Memcached的可執(zhí)行文件。

更好的是,配置文件也是Nix倉庫中的對(duì)象。因此,我可以使用nix-store -qR列出其依賴關(guān)系。

使用Nix創(chuàng)建最小鏡像

使用上一節(jié)中看到的命令,我們可以編寫以下Dockerfile:

FROM nixos/nix
RUN mkdir -p /output/store
RUN nix-env --profile /output/profile -i redis
RUN cp -va $(nix-store -qR /output/profile) /output/store
FROM scratch
COPY --from=0 /output/store /nix/store
COPY --from=0 /output/profile/ /usr/local/

第一階段使用Nix將Redis安裝到新的“配置文件”中。然后,我們要求Nix列出該配置文件的所有依賴項(xiàng)(即nix-store -qR命令),然后將所有這些依賴項(xiàng)復(fù)制到/output/store。

第二階段將這些依賴項(xiàng)復(fù)制到/nix/store(即它們?cè)贜ix中的原始位置),并復(fù)制配置文件。(主要是因?yàn)榕渲梦募夸洶粋€(gè)bin目錄,并且我們希望該目錄位于我們的$PATH目錄中?。?/p>

鏡像的最終大小是35MB,只帶有Redis,僅此而已。如果你想要一個(gè)shell,只需更新Dockerfile為-i redis bash。

如果你想重寫所有Dockerfile來使用它,請(qǐng)稍等。首先,該鏡像缺少關(guān)鍵的元數(shù)據(jù),例如VOLUME,EXPOSE以及ENTRYPOINT。其次,在下一節(jié)中,我為你提供了更好的選擇。

Nixery

所有軟件包管理器都以相同的方式工作:他們下載(或生成)文件并將其安裝在我們的系統(tǒng)上。但是Nix有一個(gè)重要的區(qū)別:安裝的文件被設(shè)計(jì)為不可變。當(dāng)我們使用Nix安裝軟件包時(shí),不會(huì)改變我們以前的版本。Docker層可以互相影響(因?yàn)橐粋€(gè)層可以更改或刪除在上一層中添加的文件),但是Nix存儲(chǔ)對(duì)象則不能。

看一下我們先前運(yùn)行的Nix容器(或者重新開始一個(gè)新容器docker run -ti nixos/nix)。特別要注意/nix/store。有很多這樣的目錄:

b7x2qjfs6k1xk4p74zzs9kyznv29zap6-bzip2-1.0.6.0.1-bin/
cinw572b38aln37glr0zb8lxwrgaffl4-bash-4.4-p23/
d9s1kq1bnwqgxwcvv4zrc36ysnxg8gv7-coreutils-8.30/

如果我們使用Nix來構(gòu)建容器鏡像(如上一節(jié)末尾在Dockerfile中所做的那樣),則我們需要的只是一堆目錄,/nix/store以及一些鏈接以方便使用。

想象一下,我們將Nix存儲(chǔ)的每個(gè)目錄上傳為Docker注冊(cè)表中的鏡像層。

現(xiàn)在,當(dāng)我們需要使用包X,Y和Z生成鏡像時(shí),我們可以:

  • 使用符號(hào)鏈接集生成一個(gè)小的層,以輕松調(diào)用X,Y和Z中的任何程序(這對(duì)應(yīng)于上面DockerfileCOPY中的最后一行),
  • 詢問Nix,對(duì)應(yīng)的存儲(chǔ)對(duì)象是什么(對(duì)于X,Y和Z,以及它們的依賴關(guān)系),以及相應(yīng)的層,
  • 生成引用所有層的Docker鏡像清單。

這就是Nixery所做的。Nixery是一個(gè)“神奇”的容器注冊(cè)表,它動(dòng)態(tài)地生成容器鏡像清單,并引用作為Nix存儲(chǔ)對(duì)象的層。

具體來說,如果執(zhí)行docker run -ti nixery.dev/redis/memcached/bash bash,我們將在具有Redis,Memcached和Bash的容器中獲得shell;并且該容器的鏡像是即時(shí)生成的。(請(qǐng)注意,我們最好執(zhí)行docker run -ti nixery.dev/shell/redis/memcached sh,因?yàn)楫?dāng)鏡像以shell開頭時(shí),Nixery在外殼頂部為我們提供了一些基本的程序包;例如coreutils。)

Nixery中還有一些額外的優(yōu)化;如果你有興趣的話,可以查看這篇博客文章NixConf的演講。

使用Nix的其他方法

Nix還可以直接生成容器鏡像。這個(gè)博客文章中有一個(gè)很好的例子。但是請(qǐng)注意,博客文章中使用的技術(shù)需要kvm并且在大多數(shù)云實(shí)例的構(gòu)建環(huán)境(除了嵌套虛擬化的實(shí)例除外,這種情況仍然非常罕見)中或在容器中都無法使用。顯然,你將不得不放棄上面的示例并使用buildLayeredImage,但是我還沒有進(jìn)行進(jìn)一步探索,所以我不知道需要多少工作量。

要不要使用Nix?

在像這樣的簡(jiǎn)短(甚至不是那么簡(jiǎn)短)的博客文章中,我無法教你如何通過書本來使用Nix,并生成完美的容器鏡像。但是我至少可以演示一些基本的Nix命令,并演示如何在多階段Dockerfile中使用Nix,以全新的方式生成自定義容器鏡像。我希望這些例子可以幫助你確定Nix是否對(duì)你的應(yīng)用程序有幫助。

就個(gè)人而言,我希望在需要臨時(shí)容器鏡像(尤其是在Kubernetes上)時(shí)使用Nixery。讓我們假設(shè),例如,我需要的鏡像包含curl,tar以及AWS CLI。我的傳統(tǒng)方法是使用alpine,執(zhí)行apk add curl tar py-pip,然后pip install awscli。但是使用Nixery,我可以簡(jiǎn)單地使用鏡像 nixery.dev/shell/curl/gnutar/awscli!

還有一些小細(xì)節(jié)

如果我們使用非常小的鏡像(例如scratch,或某種程度上alpine甚至使用distroless,Bazel或Nix生成的鏡像)??,我們可能會(huì)遇到意想不到的問題。我們通常不會(huì)考慮某些文件在容器文件系統(tǒng)中可以找到,但是有些程序可能希望在UNIX系統(tǒng)上找到。

我們到底在談?wù)撌裁次募??好吧,這是一個(gè)簡(jiǎn)短但不詳盡的清單:

  • TLS證書
  • 時(shí)區(qū)文件,
  • UID / GID映射文件。

讓我們看看這些文件到底是什么,為什么以及何時(shí)需要它們,以及如何將它們添加到鏡像中。

TLS證書

當(dāng)我們建立到遠(yuǎn)程服務(wù)器的TLS連接時(shí)(例如,通過HTTPS向Web服務(wù)或API發(fā)出請(qǐng)求),該遠(yuǎn)程服務(wù)器通常會(huì)向我們顯示其證書。通常,該證書已由知名證書頒發(fā)機(jī)構(gòu)(比如CA)簽名。通常我們要檢查此證書是否有效,并且我們確實(shí)知道對(duì)其進(jìn)行了簽名。

(我之所以說“通?!?,是因?yàn)樵谝恍┓浅:币姷膱?chǎng)景中,這無關(guān)緊要,或者我們以不同的方式驗(yàn)證;但是,如果你處于其中一種情況,則應(yīng)該知道。如果你不知道,請(qǐng)假設(shè)你必須驗(yàn)證證書!安全第一?。?/p>

在此過程中,密鑰位于這些知名的證書頒發(fā)機(jī)構(gòu)中。要驗(yàn)證所連接服務(wù)器的證書,我們需要證書頒發(fā)機(jī)構(gòu)的證書。這些通常安裝在下/etc/ssl。

如果使用的是scratch或其他小鏡像,在連接到TLS服務(wù)器,則可能會(huì)收到證書驗(yàn)證錯(cuò)誤。使用Go,返回的信息應(yīng)該類似:x509: certificate signed by unknown authority。如果發(fā)生這種情況,我們要做的就是將證書添加到你的鏡像中。我們可以從幾乎任何常見的圖像(例如ubuntu或alpine)中獲取它們。我們使用哪一個(gè)并不重要,因?yàn)樗鼈兌几綆缀跸嗤淖C書包。

下面這個(gè)命令可以解決問題:

COPY --from=alpine /etc/ssl /etc/ssl

順便說一句,這表明如果我們要從鏡像中復(fù)制文件,即使它不是構(gòu)建階段,也可以用--from來引用!

時(shí)區(qū)

如果我們的代碼操作時(shí)間,尤其是本地時(shí)間(例如,如果我們?cè)诒镜貢r(shí)區(qū)中顯示時(shí)間,而不是日期或內(nèi)部時(shí)間戳記),則需要時(shí)區(qū)文件。你可能會(huì)想:“等等,什么?如果我想管理時(shí)區(qū),我只需要知道UTC的偏移量即可!” 嗯,但這不算夏時(shí)制!夏時(shí)制(DST)很棘手,因?yàn)椴⒎撬械胤蕉加蠨ST。在具有DST的地方中,標(biāo)準(zhǔn)時(shí)間和DST之間的更改不會(huì)在同一日期發(fā)生。多年來,有些地方在實(shí)施(或取消)DST,或更改其使用期限。

因此,如果要顯示本地時(shí)間,則需要描述所有這些信息的文件。在UNIX上,則是tzinfozoneinfo文件。它們通常存儲(chǔ)在/usr/share/zoneinfo。

一些鏡像(例如centos或debian)確實(shí)包含時(shí)區(qū)文件。其他鏡像(例如alpine或ubuntu)則沒有。包含相關(guān)信息的軟件包通常命名為tzdata。

要在我們的鏡像中安裝時(shí)區(qū)文件,我們可以執(zhí)行例如:

COPY --from=debian /usr/share/zoneinfo /usr/share/zoneinfo

或者,如果我們已經(jīng)在使用alpine,我們可以簡(jiǎn)單地進(jìn)行apk add tzdata

要檢查時(shí)區(qū)文件是否已正確安裝,我們可以在容器中運(yùn)行以下命令:
TZ=Europe/Paris date
如果顯示比如Fri Mar 13 21:03:17 CET 2020這樣的信息,則表示安裝完成。如果顯示UTC,則表明未找到時(shí)區(qū)文件。

UID / GID映射文件

我們的代碼可能還需要做的另一件事:查找用戶和組ID。這是通過在/etc/passwd/etc/group中查找來完成的。就個(gè)人而言,我唯一需要提供這些文件的場(chǎng)景是在容器中運(yùn)行桌面應(yīng)用程序(使用clinkJessica Frazelle的dockerfiles之類的工具。

如果需要將這些文件安裝在容器中,則可以在本地或在多階段容器的一個(gè)階段中生成它們,或通過主機(jī)綁定安裝它們(取決于你要實(shí)現(xiàn)的目標(biāo))。

這篇博客文章顯示了如何將用戶添加到構(gòu)建容器中,然后復(fù)制/etc/passwd/etc/group到運(yùn)行的容器。

結(jié)論

如你所見,有很多方法可以減小鏡像的大小。如果你想知道“減小鏡像尺寸的絕對(duì)最佳方法是什么?”,壞消息:沒有絕對(duì)最佳的方法。像往常一樣,答案是“看情況”。

基于Alpine的多階段構(gòu)建將在許多情況下提供出色的結(jié)果。

但是有些庫在Alpine上不可用,構(gòu)建它們可能需要比我們想要的更多的工作。因此在這種情況下,使用經(jīng)典發(fā)行版進(jìn)行多階段構(gòu)建會(huì)非常有用。

Distroless或Bazel之類的機(jī)制可能更好,但需要大量的前期調(diào)研和準(zhǔn)備。

在像嵌入式系統(tǒng)這樣的空間非常小的環(huán)境中進(jìn)行部署時(shí),靜態(tài)二進(jìn)制文件和scratch鏡像可能會(huì)很有用。

最后,如果我們構(gòu)建并維護(hù)許多鏡像,我們最好堅(jiān)持使用一種技術(shù),即使那并非是最好的。使用相同的結(jié)構(gòu)來維護(hù)數(shù)百個(gè)鏡像可能要容易一些,而不是針對(duì)某種基場(chǎng)景使用過多的變體和一些特殊的構(gòu)建系統(tǒng)或Dockerfile。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容