chundev
日期:2026-05-04 環境:k3s + NFS CSI Driver + Synology NAS(Btrfs)
PVC 上寫 50Gi 完全不會被 enforce — NFS 不像 block storage 會強制 quota。真正的容量限制來自 NAS 端 shared folder 的 quota。Registry GC 跑成功不代表 NAS 空間真的釋放,因為 Synology Btrfs 預設開 24h snapshot,刪掉的 blob 還會被 snapshot 保留住,造成「明明 GC 跑了還是滿」的錯覺。
K3s cluster 跑 local docker registry,storage 走 NFS CSI driver 接 NAS。多個專案的 image push 到同一台 registry,pull 進 cluster 部署。
某天 make app-deploy 失敗,Docker push 一半收到 blob upload unknown,registry log 直接寫 disk quota exceeded。處理過程中發現幾個會誤導排查方向的點,記下來。
The push refers to repository [<registry>/<image>]
35519686be6e: Retrying in 3 seconds
cad2ad9ae25d: Retrying in 3 seconds
...
blob upload unknown
make: *** [app-push-clean] Error 1
Registry pod log:
level=error msg="error closing blobwriter:
filesystem: sync .../hashstates/sha256/0: disk quota exceeded"
level=error msg="response completed with error"
err.code=unknown err.detail="filesystem: sync .../startedat: disk quota exceeded"
先把架構畫出來,後面的排查才看得懂。
NAS (Synology, Btrfs)
└── volume1
└── <shared-folder> ← admin 在 DSM 設 quota(例如 500GB)
└── cold-storage/
├── kube-system/
│ └── local-registry-pvc/ ← Registry 寫這裡
├── kymo-erp/
│ └── web-uploads-pvc/
├── dfaa/
│ └── postgres-cluster-tbs-cold-data/
└── ... 共用同一個 quota
K8s 那邊用一個 nfs-cold StorageClass 對應這個 shared folder:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-cold
provisioner: nfs.csi.k8s.io
parameters:
server: "${NFS_SERVER}"
share: "${NFS_COLD_SHARE}"
# 每個 PVC 自動建子目錄,但共用 share 的 quota
subDir: "cold-storage/${pvc.metadata.namespace}/${pvc.metadata.name}"
mountOptions:
- nfsvers=4.1
- hard
PVC 寫 50Gi:
apiVersion: v1
kind: PersistentVolumeClaim
spec:
storageClassName: nfs-cold
resources:
requests:
storage: 50Gi # ← 對 NFS 來說只是 metadata,沒人 enforce
重點:
subDir 變數把每個 PVC 切成獨立子資料夾50Gi 只是「scheduler 用來找夠大的 PV」的提示,NFS 從不檢查也不限制實際用量不用爬到 NAS 上,從任何掛該 PVC 的 pod 都查得到:
kubectl exec -n kube-system deploy/local-registry \
-- df -h /var/lib/registry
Filesystem Size Used Available Use% Mounted on
<NAS-IP>:/<volume>/<share>/cold-storage/kube-system/local-registry-pvc
500.0G 500.0G 128.0K 100% /var/lib/registry
Size 欄就是 NAS 端 shared folder 的 quota。這個值跟 PVC 的 50Gi 一點關係都沒有。
要看實際用了多少(vs quota 算的):
kubectl exec -n kube-system deploy/local-registry \
-- du -sh /var/lib/registry/docker/registry/v2/blobs
如果 du < df 差很多(例如 du 23GB / df 顯示 500GB 滿),這就是這篇筆記的主角 — snapshot 陷阱。
| 嘗試 | 做法 | 結果 | 為什麼沒用 |
|---|---|---|---|
| ❌ 1 | 手動觸發 registry-gc CronJob |
log 顯示「Deleting blob: sha256:…」、「Done.」 | df 還是 500G 滿 |
| ❌ 2 | 重啟 registry pod 釋放 NFS file handle | pod ready,沒有 .nfs* lock file |
df 還是 500G 滿 |
| ✅ 3 | 確認 du 只 23G、df 顯示 500G → 上 NAS DSM 看 snapshot | 發現 shared folder 開了自動 snapshot 24h retention,吃掉空間 | 刪 snapshot 後立刻釋放 |
Synology DSM 預設行為:
這就是「最近一直滿」的根因 — 不是寫入量真的暴衝,而是每次 push 累積的舊 layer 都被 snapshot 凍住。
順便補完 GC 的限制 — 這也是排查中容易誤判的點:
registry garbage-collect /etc/docker/registry/config.yml
:latest 時舊 manifest 變孤兒 → GC 才有得清DELETE /v2/<name>/manifests/<digest>)REGISTRY_STORAGE_DELETE_ENABLED=true,否則 GC 只 log 不做事加上 snapshot 陷阱:「GC log 寫 Deleting」≠「真的釋放空間」,必須對照 df 才算數。
按優先級:
NAS shared folder 關掉 snapshot Registry storage 本來就不需要 snapshot 保護(GC 是它自己的清理機制;image 本身內容定址不需要版本)。對其他需要 snapshot 的 shared folder 影響也分開。
registry-gc 改 daily(如果還是 weekly) Multi-repo push churn 一週累積容易爆。日跑成本很低(registry 只需要短暫 read-only)。
# 偽碼
for repo in $(curl /v2/_catalog); do
for tag in $(curl /v2/$repo/tags/list | older_than 30d); do
digest=$(curl -I /v2/$repo/manifests/$tag | grep Docker-Content-Digest)
curl -X DELETE /v2/$repo/manifests/$digest
done
done
registry garbage-collect /etc/docker/registry/config.yml
這個是踩坑時必踩的另一個雷 — 講錯標的,admin 改錯地方。完整描述模板:
NAS:
<DSM IP>(Synology)標的:
<volume>上<shared-folder>這個 shared folder 的 quota動作:把 quota 從 N GB 擴到 M GB
確認位置:DSM → Control Panel → Shared Folder → 找
<shared-folder>→ Edit → Quota 頁籤背景:這個 shared folder 是 k3s 叢集
nfs-coldstorage class 的根目錄,N 個 PVC 共用(local registry、各專案的 web-uploads、postgres cold tablespace 等)。NFS 端不需要動(沒有 LVM resize、沒有 PV resize);DSM 改 quota 之後 K8s 立刻看得到新空間,不需要重啟任何 pod。
順便讓 admin 一起做:
#recycle 資料夾 → 清空storage: 50Gi 對 NFS 來說只是 hint。NFS provisioner 不會 enforce,scheduler 也不會檢查實際用量。要 enforce 必須在 storage server 端設 quota。df 看的是 NAS 回報的 quota,du 看的是實際用量。兩者落差很大時,幾乎一定是 storage server 端的 snapshot / recycle bin / quota 計算方式有事。.nfs* sillyrename file 是 file handle 沒釋放的徵兆;如果沒這類檔案但 df 還是滿,就確定不是 client 端的事。subDir pattern 是把單一 share 切多個 PVC 的好方法,但代價是「一個 PVC 寫爆殃及所有 PVC」。重要工作負載(如 registry)若預期會大量寫入,最好獨立 share folder 設獨立 quota。subDir 變數展開、storage class 參數說明REGISTRY_STORAGE_DELETE_ENABLED