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