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