Photo by William Warby on Unsplash
您是否曾經想過為什么您的單應用程序Docker容器會增長到400 MB? 或者,也許為什么一個只有幾十MB的應用程序二進制文件會生成一個MB的Docker映像?
在本文中,我們將回顧一些導致容器變胖的主要因素,以及為您的項目提供超薄Docker容器的最佳實踐和技巧。
Docker鏡像層
Docker容器鏡像本質上是堆積的文件,稍后將被實例化為正在運行的容器。 Docker利用聯合文件系統(UnionFS)設計,其中文件按層分組在一起。 每一層可能包含一個或多個文件,并且每一層都位于上一層的頂部。 作為最終用戶,我們體驗了作為統一文件系統的所有層的所有內容的虛擬運行時合并:
A simplified view of UnionFS (Image by the author)
UnionFS的底層實現向我們提供的最終文件系統視圖(Docker通過可插拔存儲驅動程序支持許多不同的視圖)具有其所構成的所有層的總大小。 Docker為圖像創建容器時,它將以只讀格式使用鏡像的所有層,并在它們之上添加一個薄的讀寫層。 這個薄的讀寫層使我們能夠實際修改正在運行的Docker容器中的文件:
A running container adds a read-write layer on top of an image's read-only layers.
如果在上述第4層中刪除了文件,會發生什么情況? 盡管已刪除的文件不會再出現在觀察到的文件系統中,但是由于該文件包含在較低的只讀層中,因此它最初占用的大小仍將是容器占用空間的一部分。
從一個小的應用程序二進制文件開始到以一個胖容器鏡像結束是相對容易的。 在以下各節中,我們將探索不同的方法來使鏡像的尺寸盡可能地薄。
提防構建路徑
我們構建Docker鏡像的最常見方式是什么?
docker build .
上面的命令告訴Docker我們將當前的工作文件夾視為構建過程的根文件系統路徑。
為了更好地理解發出上述命令時實際發生的情況,我們應該記住,Docker構建是一個客戶端-服務器進程。 我們從中執行docker build命令的Docker CLI(客戶端)使用基礎Docker引擎(服務器)來構建容器鏡像。 為了限制對客戶端基礎文件系統的訪問,構建過程需要知道虛擬文件系統的根目錄是什么。 正是在此確切路徑下,Dockerifle中的任何命令都試圖查找可能最終在正在生成的鏡像中結束的文件資源。
讓我們考慮一下我們通常放置Dockerfile的位置。 在項目的根源中,也許吧? 好吧,將項目根目錄中的Dockerfile與Docker構建相結合,我們已經有效地添加了完整的項目文件夾作為構建的潛在文件資源。 這可能會導致在構建上下文中不必要地添加多個MB和數千個文件。 如果我們不小心在Dockerfile中定義了ADD / COPY命令,則所有這些文件都可以成為最終鏡像的一部分。 在大多數情況下,這不是我們所需要的,因為最終容器鏡像中僅應包含一些選定的項目人工制品。
始終檢查是否為Docker構建提供了適當的構建路徑,并且Dockerfile沒有向鏡像添加不必要的文件。 如果出于任何原因確實需要將項目的根目錄定義為構建上下文,則可以通過.dockerignore有選擇地包括/排除文件。
擠壓圖像
命令合并的另一種方法是使用Docker的squash命令構建鏡像,尤其是在使用您不希望或無法修改的其他Dockerfile時。
除非您使用的是非常老的Docker版本(<1.13),否則Docker允許我們將所有層壓縮為一個層,從而有效地刪除所有虛幻資源。 我們仍然可以將原始的,未更改的Dockerfile與許多單獨的命令一起使用,但是這次我們通過--sqash選項執行構建:
docker build --squash .
再次對生成的鏡像進行100%優化:
A 100% optimised image with image squash (Image by the author)
這里需要注意的有趣一點是,由于我們的Dockerfile創建了一個添加文件的層,然后創建了另一個刪除該文件的層,所以squash足夠聰明,以至于無需創建任何層(我們只有9ccd9…層 我們正在使用的基本圖片)。 然后,額外的榮譽就可以南瓜了。 但是,請注意,擠壓圖層可能會阻止您或您的鏡像用戶利用先前緩存的圖層。
注意:使用您不想更改的第三方Dockerfile時,一種最小化任何可能浪費空間的快速簡便方法是使用--squash構建它。 您可以使用潛水工具檢查圖像的最終效率。
命令合并
您是否見過帶有RUN指令非常長的Dockerfile,其中多個Shell命令與&&聚合在一起? 命令合并。
通過合并命令,我們實際上是根據此單個long命令的結果創建了一個單獨的層。 由于不存在用于添加文件并隨后在另一層中刪除文件的中間層,因此最后一層將不會為此類幻影文件使用任何空間。 讓我們通過修改上述Dockerfile來了解這一點:
FROM alpineRUN wget http://xcal1.vodafone.co.uk/10MB.zip -P /tmp && rm /tmp/10MB.zip
現在我們有了一個優化的鏡像:
A 100% optimised image with commands merge (Image by the author)
當您完成構建Dockerfile時,請檢查它以查看是否可以合并命令以減少可能的浪費空間。
標準化鏡像層
如果基礎存儲驅動程序支持,則鏡像可以具有的最大層數為127。 如果確實需要,可以增加此限制,但是隨后您可以縮小構建該映像的位置的選擇(即,您需要在類似修改的基礎內核上運行的Docker引擎)。
正如上面有關Docker鏡像層的部分中所討論的,由于UnionFS,進入層的任何文件資源都保留在該層中,即使您在后一層中管理該文件也是如此。 我們來看一個示例Dockerfile:
FROM alpineRUN wget http://xcal1.vodafone.co.uk/10MB.zip -P /tmpRUN rm /tmp/10MB.zip
構建以上鏡像:
Building a sample image with wasted space (Image by the author)
并進行潛水檢查:
Image is only 34% efficient (Image by the author)
效率為34%表示圖像中浪費了很多空間。 這將導致更長的圖像獲取時間,額外的帶寬消耗和更慢的啟動時間。
我們如何擺脫這個浪費的空間?
刪除緩存
通常,當我們將應用程序容器化時,我們需要使用軟件包管理器(例如apk,yum或apt)在生成的映像上提供額外的工具,庫或實用程序。
當我們通過緩存先前獲取的軟件包來安裝軟件包時,軟件包管理器試圖為我們節省時間和帶寬。 為了使生成的Docker映像的尺寸盡可能小,我們不需要保留程序包管理器緩存。 畢竟,如果我們的容器需要其他映像,我們總是可以使用更新的Dockerfile重建映像。
要刪除上述三個流行的軟件包管理器的軟件包管理器緩存,我們可以在聚合(即命令合并)命令的末尾添加以下命令,例如:
APK: ... && rm -rf /etc/apk/cacheYUM: ... && rm -rf /var/cache/yumAPT: ... && rm -rf /var/cache/apt
注意:在最終確定Docker鏡像之前,請不要忘記刪除構建期間使用的所有緩存以及容器正常運行所不需要的任何其他臨時文件。
選擇基礎鏡像
每個Dockerfile都以FROM指令開頭。 在此定義我們將在其上創建自己的圖像的基礎圖像。
如Docker文檔中所述:
" FROM指令初始化一個新的構建階段,并為后續指令設置基礎映像。 因此,有效的Dockerfile必須以FROM指令開頭。 該圖像可以是任何有效的圖像-從公共存儲庫中提取圖像特別容易。"
顯然,有很多不同的基礎圖像可供選擇,每個基礎圖像都有自己的優勢和功能。 當涉及到您自己的Docker映像的最終大小時,選擇一個足以提供應用程序運行所需的工具和環境的映像至關重要。
正如您所期望的那樣,不同的流行基本鏡像的大小差異很大:
Popular Docker base images size (Image by the author)
實際上,使用Ubuntu 19.10基本映像對應用程序進行容器化將至少增加73 MB,而使用Alpine 3.10.3基本映像的完全相同的應用程序只會使大小增加6 MB。 隨著Docker緩存圖像層,下載/帶寬損失僅在您第一次使用該圖像啟動容器時適用(或者簡單地,在拉取圖像時)。 但是,增加的大小仍然存在。
此時,您可能已經得出以下(非常合邏輯的)結論:"那么,我將永遠使用Alpine!"。 如果在軟件中只有那么清楚的話。
您會發現,Alpine linux背后的家伙還沒有發現Ubuntu或Debian家伙仍在尋找的特殊秘密調味料。 為了能夠創建比Debian小(例如)小的數量級的Docker映像,他們必須對Alpine映像中要包含的內容和不包含的內容做出一些決定。 在選擇Alpine作為默認基本映像之前,應檢查它是否提供了所需的所有環境。 此外,即使Alpine隨附了軟件包管理器,您也可能會發現Alpine中不提供您在(例如)基于Ubuntu的開發環境中使用的特定軟件包或軟件包版本。 在為項目選擇最合適的基礎映像之前,您應該了解這些權衡并進行測試。
最后,如果您確實需要使用一個較胖的基礎鏡像,則可以使用鏡像最小化工具(例如免費和開源DockerSlim)來減小最終映像的大小。
注意:在嘗試減小尺寸時,為自己的鏡像選擇適當的基礎鏡像很重要。 評估您的選擇并選擇一張鏡像,該鏡像可提供您所需的工具,以確保您可以承受的尺寸。
完全不選擇基礎鏡像
如果您的應用程序可以在沒有基礎映像提供任何其他環境的情況下運行,則可以選擇完全不使用基礎映像。 當然,由于FROM在Dockerfile中是強制性的,因此您仍然必須擁有它并將其指向某個內容。 在這種情況下,您應該使用什么?
從頭開始,即:
"一個明顯為空的圖像,特別是對于構建圖像"從零開始"。 在構建基礎映像(例如debian和busybox)或超小型映像(僅包含一個二進制文件以及它所需要的任何內容,例如hello-world)的上下文中,此映像最有用。 從頭開始是Dockerfile中的一項禁止操作,并且不會在映像中創建額外的層。"
注意:如果您的應用程序包含可以以獨立方式運行的自包含可執行文件,則選擇暫存基礎映像可以使您盡可能減少容器的占用空間。
多階段構建
當Docker 17.05可用時,多階段構建成為關注的焦點。 期待已久的功能,多階段構建允許鏡像構建器將自定義鏡像構建腳本拋在后面,并將所有內容集成到眾所周知的Dockerfile格式中。
用高級術語來說,您可以將多階段構建視為將多個Dockerfile合并在一起,或者簡單地將一個具有多個FROM的Dockerfile合并。
在進行多階段構建之前,如果要構建項目的工件并使用Dockerfile將其分發到容器中,則可能必須遵循一個構建過程,最終以一個如下圖所示的容器結束:
Building and distributing your Application without multi-stage builds (Image by the author)
盡管上述過程在技術上沒有任何問題,但最終鏡像和生成的容器在構建/準備項目人工制品時創建的層上都layers腫了,這些層對于項目的運行時環境不是必需的。
多階段構建使您可以將創建/準備階段與運行時環境分開:
Multi-stage builds, separation creation/preparation from runtime (image by author)
您仍然可以使用單個Dockerfile定義完整的構建工作流程。 但是,您可以將文物從一個階段復制到另一個階段,同時將數據丟棄在不需要的層中。
注意:多階段構建允許您創建跨平臺的可重復構建,而無需使用特定于操作系統的自定義構建腳本。 通過有選擇地包括在構建的前幾個階段中生成的偽像,可以使鏡像的最終大小保持最小。
結論
為容器創建Docker鏡像是現代軟件工程師必須經常處理的過程。 有大量在線資源和示例向您展示如何創建Dockerfile,但是,您應注意生成的映像的大小。
在本文中,我們回顧了一些方法和技巧,以最大程度地減少Docker鏡像的最終大小。 通過精心制作僅包含必要工件的Dockerfile,選擇合適的基礎映像并使用多階段構建,可以大大減少Docker映像的最終大小。
(本文翻譯自Nassos Michas的文章《Super-Slim Docker Containers》,參考:
https://medium.com/better-programming/super-slim-docker-containers-fdaddc47e560)