Share Notes

chundev

View the Project on GitHub latteouka/share-notes

ESXi thin vmdk 越用越肥 — zero-fill + vmkfstools -K 瘦身實戰

日期:2026-05-24 環境:ESXi(VMFS6)+ Ubuntu 24.04 guest + thin provisioning


TL;DR

ESXi thin provisioning 的 vmdk 大小不等於當下 disk 用量,是「曾經寫過的 block 高水位」。VM 內 rmfstrim 只是釋放檔案系統層級的 metadata,vmdk 檔本身不會跟著縮。要真正瘦身,必須:

  1. VM 內把已釋放空間寫零dd if=/dev/zero of=/zerofill
  2. ESXi datastore 端用 vmkfstools -K vmdisk.vmdk 把 zero block 摺掉
  3. (或 Storage vMotion 到別 datastore + 選 Thin format 自動 reclaim)

我這次的個案是 14 GB 真實用量的 VM、export 出來變 22 GB;走完上面流程後 export 變 4.1 GB


背景

一台 Ubuntu 24.04 VM 跑 Docker(多 image)做了一整天的部署演練:

部署完成、要 export 成 vmdk 交付客戶時發現:

觀測 數字
df -h / 當下用量 14 GB / 97 GB
ESXi export 進度 已跑到 22 GB 還沒完

換句話說,export 出來的 vmdk 比實際用量還大 60%。客戶端的 vmdk 又因為含未必要的零位元組變肥,傳輸/儲存都付出無謂成本。


為什麼會這樣 — Thin provisioning 的「高水位」

Thin vmdk 在 datastore 上是只記錄已分配過的 block 的 sparse 檔案。但這裡的「已分配」是單調遞增的——歷史曾寫過的 block 即使後來檔案被刪掉、空間在 guest 內變成 free,ESXi 不會自動把這些 block 從 vmdk 摺掉

時間軸:
  T1: 寫入 16 GB 測試資料  → vmdk 從 0 GB 漲到 16 GB
  T2: 客內 rm 砍掉 12 GB    → guest df 看到 used=4 GB,vmdk 還是 16 GB
  T3: 再寫 6 GB 新資料      → vmdk 漲到 18 GB(即使 guest df=10 GB)
  T4: 又砍 8 GB             → guest df=2 GB,vmdk 仍 18 GB
                                 ↑ 高水位卡在這裡

fstrim 會送 SCSI UNMAP/DISCARD 到底層、VMFS6 也有 auto unmap,但實務上常常因為以下原因 reclaim 不徹底:

所以可靠的瘦身策略不是「期待自動 reclaim」,而是主動把要 reclaim 的 block 寫成全零、再用 punchzero 強制摺掉


完整瘦身流程

Step 1:VM 內砍掉不必要的檔(最大兇手通常 1-2 個)

# 找出 top consumer
sudo du -sh /opt/* /var/* /usr/* /home/* 2>/dev/null | sort -hr | head -10

# 砍掉備援、cache、log
sudo rm -f /path/to/redundant-large-files
sudo docker image prune -f             # dangling docker images
sudo apt-get clean                     # apt cache
sudo journalctl --rotate
sudo journalctl --vacuum-time=1s       # 清舊 journal

Ubuntu 24.04 Docker 用 containerd-snapshotter 的陷阱: 真實 image layer 是存在 /var/lib/containerd/,不是 /var/lib/docker/du /var/lib/docker 看到的數字會嚴重低估。docker images 報的 size 又因 layer dedup 不直接等於 disk 用量。直接看 du /var/lib/containerd/ 才準。

Step 2:把 free space 寫滿零

sudo dd if=/dev/zero of=/zerofill bs=1M status=progress
# 跑到「No space left on device」就完成,這是預期的退出方式
sync
sudo rm -f /zerofill

這支會把 /所有 free block 寫成零。檔案系統層級這些 block 仍標示 used(被 /zerofill 占用),等 dd 結束、rm 釋放後,block 在 guest 內變回 free,但其資料內容已是零 —— ESXi 等下就有東西可以 reclaim。

⚠️ 跑 dd 期間 disk 會被填滿,會影響同時跑的服務(postgres、container 等可能寫入失敗)。先把要寫 disk 的服務停下來 再跑 dd 比較安全:

# 例如先停 docker stack
sudo docker compose -f /path/to/compose.yml down

Step 3:fstrim 雙保險

sudo fstrim -av

對 VMFS6 + HW version ≥ 13 來說,這會送 SCSI UNMAP 給 datastore,讓部分區塊立刻 reclaim。但單靠 fstrim 通常不夠(前述對齊問題),所以視為加分項而非主力。

Step 4:關 VM

sudo shutdown -h now

Step 5:ESXi datastore 端 punchzero(核心)

SSH 進 ESXi host:

cd /vmfs/volumes/<datastore>/<vm-folder>/
vmkfstools -K <vmdisk>.vmdk
# -K 又名 --punchzero

vmkfstools -K 會掃整個 vmdk、把全零的 block 從 sparse 檔案中真的拿掉。剛剛 dd 寫的那一大片零、加上歷史 free 過的零,全部會被摺掉。

限制:

Step 5’(替代):Storage vMotion + Thin format

如果 datastore 是 NFS、或不方便 SSH 進 ESXi host,可以 Storage vMotion 到另一個 datastore + 選 Thin format。遷移過程 ESXi 內建會把 zero block 摺掉,產生的新 vmdk 只佔實際資料量。

Step 6:Export OVF/OVA

vSphere → VM → Export OVF Template → disk format 選 Thin

OVA 本身又會 gzip 壓縮,docker image layers(alpine + node binary 等)對 gzip 友善,壓縮率通常很高


我這次的結果

階段 vmdk 大小
初始 export(沒做任何 reclaim) 22 GB(still going)
開機進 VM 砍 redundancy(~2 GB)+ docker prune(~3 GB)+ apt/journal 清理 guest df: 14 GB
dd zero-fill 76 GB + fstrim 釋出 83 GiB guest df: 14 GB(不變,但 free block 全是零)
Storage vMotion to Thin + Export OVF Thin 4.1 GB

從 22 GB 到 4.1 GB,少了 17.9 GB(81%)。export/傳輸時間都跟著省。


學到的事


參考資料