化:從overlay2到多階段構(gòu)建的完整實(shí)踐指南)
Docker 運(yùn)行時機(jī)制我上一篇已經(jīng)聊過重點(diǎn)講了容器進(jìn)程怎么被啟動、命名空間和 cgroups 怎么配合。這一篇標(biāo)題是“docker-image-layer-storage-optimization”核心就一件事把 Docker 鏡像分層存儲徹底講透并且從構(gòu)建、運(yùn)行到維護(hù)給出真正能落地的存儲優(yōu)化方案。如果你被鏡像體積困擾過、磁盤空間莫名其妙告警或者對 overlay2 到底是什么一知半解這篇就是寫給你的。1. 把鏡像分層這件事徹底講明白1.1 鏡像是一疊只讀層不是一個大文件夾我剛接觸 Docker 時也以為鏡像就像虛擬機(jī)鏡像一樣是一整個大文件。后來才發(fā)現(xiàn)完全不是這么回事。Docker 鏡像采用內(nèi)容尋址存儲可以拆成多個只讀層每一層只記錄和上一層之間的差異并用 SHA-256 摘要作為唯一標(biāo)識。鏡像不是一次性生成的完整 rootfs而是 Dockerfile 里的指令逐條執(zhí)行后一層層疊加出來的結(jié)果。舉個具體例子Dockerfile 里寫 RUN apt-get install 某個軟件執(zhí)行后就會產(chǎn)生新的一層。這一層記錄的是從“當(dāng)前狀態(tài)”到“安裝完成狀態(tài)”的全部文件系統(tǒng)變化新增了哪些文件、修改了哪些文件、刪除了哪些文件。只要某一層的內(nèi)容完全不變它的哈希值就不會變多個鏡像就可以安全共享同一批底層。這也是為什么所有鏡像都基于同一個基礎(chǔ)鏡像時本機(jī)磁盤占用并不會按鏡像數(shù)量線性增長而是大量復(fù)用底層數(shù)據(jù)。想直觀看到分層結(jié)構(gòu)最常用的命令是 docker history。比如 docker history nginx:alpine會按構(gòu)建順序列出每一層對應(yīng) Dockerfile 中的指令同時給出 SIZE 和 COMMENT。很多人看到 missing 字樣會困惑其實(shí)那只是表示這一層是從父鏡像繼承來的在當(dāng)前機(jī)器上沒有對應(yīng)的構(gòu)建上下文。這個命令是我排查鏡像體積問題時第一優(yōu)先使用的工具簡單直接。1.2 寫時復(fù)制容器改動為什么不會破壞鏡像鏡像層是只讀的那容器怎么寫入文件呢答案是存儲驅(qū)動在鏡像層之上疊加了一個容器可寫層。啟動容器時Docker 會把所有鏡像只讀層和這個可寫層聯(lián)合掛載成一個完整視圖對容器進(jìn)程來說它就是普通的根文件系統(tǒng)。寫入動作觸發(fā)的是寫時復(fù)制機(jī)制。容器要修改鏡像層里的某個文件時存儲驅(qū)動會先把文件從只讀層復(fù)制到容器可寫層再在副本上修改原來的只讀層保持原樣不動。刪除文件也不是真的刪除只讀層中的文件而是寫入一個 whiteout 標(biāo)記在合并視圖里把這個文件“遮住”讓上層不再看到它。如果你在容器里刪掉 /etc/nginx/nginx.conf再從宿主機(jī)看容器可寫層的 diff會發(fā)現(xiàn)那層的處理邏輯不是直接移除文件而是建立特殊標(biāo)記。這套設(shè)計價值巨大。同一臺機(jī)器跑幾十個相同鏡像的容器底層只讀層完全共享每個容器只有自己的小可寫層獨(dú)立變化。打個比方鏡像層像一份共享的教材底稿每個人把批注和筆記寫在自己的透明便利貼上互不影響也不改動底稿。生產(chǎn)環(huán)境里很多容器共用鏡像而不爆炸靠的就是這套機(jī)制。1.3 鏡像元數(shù)據(jù)和分層的關(guān)系除了文件系統(tǒng)層Docker 鏡像還有兩份關(guān)鍵元數(shù)據(jù)config 文件記錄容器配置、環(huán)境變量、默認(rèn)命令和歷史指令等信息manifest 列出所有層及其對應(yīng)摘要。docker image inspect 可以查看完整內(nèi)容其中 GraphDriver 字段會顯示當(dāng)前鏡像在本地存儲驅(qū)動下的分層數(shù)據(jù)。有個容易混淆的點(diǎn)鏡像層的本地 ID比如 overlay2 目錄下那串哈希和內(nèi)容尋址的 diffID 不是一回事。前者是存儲引擎在本地實(shí)現(xiàn)層引用的哈希后者是未壓縮文件內(nèi)容的 SHA-256兩者通過 chainID 關(guān)聯(lián)。實(shí)際排障時通常不用深挖到這一層但理解它們不同能避免你對比 docker inspect 輸出時產(chǎn)生困惑。當(dāng)你看到 /var/lib/docker/overlay2 下有一堆長哈希目錄時要知道這些只是存儲驅(qū)動的具體布局不完全是鏡像 layer 的直接映射。2. 存儲驅(qū)動選型與調(diào)優(yōu)overlay2 是默認(rèn)答案嗎2.1 overlay2目前最主流也最值得優(yōu)先選存儲驅(qū)動負(fù)責(zé)把鏡像層展開并合并成可寫視圖。目前 Linux 上絕大多數(shù)場景都該用 overlay2它基于內(nèi)核的 overlayfs把只讀層作為 lowerdir容器可寫層作為 upperdir最后掛載成一個 merged 視圖。overlay2 有一個關(guān)鍵優(yōu)勢它會嘗試把鏡像層文件通過硬鏈接方式共享到底層數(shù)據(jù)而不是復(fù)制。假設(shè)兩個鏡像共享同一個基礎(chǔ)鏡像層overlay2 可以用硬鏈接把相同文件關(guān)聯(lián)到同一 inode明顯減少磁盤占用。這也是為什么 /var/lib/docker/overlay2 目錄里有很多鏈接數(shù)大于 1 的文件。容器運(yùn)行時lowerdir 和 upperdir 的合并由內(nèi)核完成性能遠(yuǎn)好于用戶態(tài)存儲驅(qū)動。不過選 overlay2 有前提條件內(nèi)核支持 overlayfs 一般沒問題底層文件系統(tǒng)必須是 xfs 且開啟 ftype1或者 ext4。如果 xfs 沒開 ftypeDocker 可能拒絕使用 overlay2報錯信息里會提到 d_type。我記得有次在一臺老機(jī)器的舊分區(qū)上裝 Docker無論如何都無法用 overlay2最后檢查分區(qū)格式才發(fā)現(xiàn)不支持屬于環(huán)境沒打底。排查路徑很簡單先 /proc/filesystems 看有沒有 overlay再 df -T 看分區(qū)類型。2.2 rootless 模式下的 fuse-overlayfs 與 vfs 兜底rootless 模式下沒有 root 權(quán)限普通用戶無法掛載內(nèi)核 overlayfs這時通常會落到 fuse-overlayfs 或 vfs。fuse-overlayfs 通過 FUSE 用戶態(tài)文件系統(tǒng)實(shí)現(xiàn) overlay 語義功能基本兼容性能雖然比內(nèi)核態(tài)差一些但正常跑絕大多數(shù)容器沒有問題。如果環(huán)境里沒有 fuse-overlayfsDocker 可能會退回 vfs。vfs 不做寫時復(fù)制而是為每個容器完整復(fù)制一份鏡像文件空間占用巨大性能也差。我的評價是能不用就不用生產(chǎn)環(huán)境一旦落到 vfs磁盤很快會被撐爆。怎么確認(rèn)當(dāng)前用的哪個驅(qū)動docker info 里看 Storage Driver 一行就行如果顯示 vfs基本可以斷定配置有問題。在 daemon.json 里顯式配置 storage-driver: fuse-overlayfs配合 rootless 使用可以避免踩到自動探測的坑。如果你只是在自己筆記本上跑 rootless Docker 做開發(fā)驗(yàn)證fuse-overlayfs 完全夠用不必強(qiáng)求內(nèi)核 overlay2。2.3 與存儲驅(qū)動相關(guān)的幾個關(guān)鍵參數(shù)daemon 的 /etc/docker/daemon.json 集中存放存儲相關(guān)配置。常見參數(shù)有 storage-driver、data-root、storage-opts。設(shè)置>