Docker鏡像分層與層共享
· 3 min read
第一個問題
假設你的伺服器上跑了兩個容器:
container1: Ubuntu(200MB) + Nginx(50MB) = 250MB
container2: Ubuntu(200MB) + MySQL(100MB) = 300MB
按照常識來說,磁碟總共應該花了200+200+200+50+100=750MB,但是實際上只有200+50+100=350MB。這是為什麼呢?
因為Ubuntu那200MB的鏡像只存了一份,這就是Docker分層架構要做的事情。
Docker鏡像的本質:一疊唯讀的層
Docker鏡像不是一個整體的大檔案,而是一層一層疊起來的,每一層對應Dockerfile中的一條指令。
FROM ubuntu:22.04 # 這一層包含了ubuntu的檔案
RUN apt-get install -y nginx # 這一層包含了安裝nginx的檔案
RUN apt-get install -y mysql-server # 這一層包含了安裝mysql的檔案
每一層只記錄相對上一層的改變
┌─────────────────────┐
│ 第3層: config檔案 │ 1MB(只有這層的新內容)
├─────────────────────┤
│ 第2層: Nginx │ 30MB
├─────────────────────┤
│ 第1層: Ubuntu │ 200MB
└─────────────────────┘
所有層都是唯讀的,構建完成後不可以修改。
三:層共享
當你基於同一個 Ubuntu 構建兩個不同鏡像時:
image1 (nginx): [Ubuntu 200MB] → [Nginx 50MB]
image2 (mysql): [Ubuntu 200MB] → [MySQL 100MB]
Docker 透過每層的 Content Hash(內容哈希)來識別層是否相同。Ubuntu 層的 hash 完全一致,所以磁碟上只存一份:
磁碟實際儲存:
┌─────────────────────┐
│ MySQL層 │ 100MB ← image2 獨有
├─────────────────────┤
│ Nginx層 │ 50MB ← image1 獨有
├─────────────────────┤
│ Ubuntu層 │ 200MB ← 兩個鏡像共享
└─────────────────────┘
總計:350MB,而不是 750MB
四:容器執行時
鏡像層是唯讀的,那容器如果在執行的時候寫檔案怎麼辦?
Docker在唯讀鏡像層上面再加了一層可寫的容器層
┌─────────────────────┐
│ 容器層(可寫) │ ← 容器執行時產生的檔案寫在這裡
├─────────────────────┤
│ Nginx層(唯讀) │
├─────────────────────┤
│ Ubuntu層(唯讀) │
└─────────────────────┘
這就是 Copy-on-Write(寫時複製)機制:
讀檔案:從上往下找,找到就返回
寫檔案:先把檔案從唯讀層複製到容器層,再修改
所以即使 100 個容器基於同一個鏡像,鏡像層依然只有一份,每個容器只多出自己那個很薄的容器層。
container1 container2 container3
[寫層1] [寫層2] [寫層3] ← 各自獨立,很小
↘ ↓ ↙
[共享的唯讀鏡像層] ← 只有一份
五:這對Dockerfile寫法的啟示
利用層快取:把不常變的指令放前面,常變的指令放後面,這樣修改後只需要重建後面的層,前面的層可以複用。
# ✅ 好的寫法
COPY requirements.txt . # 依賴檔案變化少 → 快取穩定
RUN pip install -r requirements.txt
COPY . . # 程式碼經常變 → 放最後
# ❌ 差的寫法
COPY . . # 程式碼一變,下面的依賴安裝就要重跑
RUN pip install -r requirements.txt
合併RUN減少層數
# ❌ 產生3層,中間層的快取檔案仍佔空間
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ 一層搞定,最終只保留有用的檔案
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*