明天開始組 Pipeline,PipelineRun 會成批產生 Pod、拉 image、在 Nexus 裡堆 blob。這篇先弄清楚三件事:
| 常見的理解 | 這台的實測 |
|---|---|
| 單機 CRC 沒有內建的回收機制 | 三層機制都存在,也都在跑 |
排程任務回報 OK 代表有回收到東西 |
Nexus 的六個任務全綠,回收量是零 |
| image GC 會定期清理 | 門檻觸發,不到 85% 完全不動 |
imagefs 跟 nodefs 是不同的盤 |
這台連 containerfs 都是同一顆 |
| PVC 寫 179Gi 就有 179Gi | 十顆 PV 名目 1,641 Gi,實體盤 180 G |
讀 kubelet.conf 就知道門檻設多少 |
門檻不在檔案裡 |
du -shx 加了 -x 就準了 |
還會跨路徑去重,分解表會少算 |
CRC 2.61.0+6eb443
OpenShift 4.21.14 / Kubernetes v1.34.6(單節點 crc)
CRI-O 1.34.7-2.rhaos4.21
RHCOS 9.6.20260504-0(kernel 5.14.0-570)
Red Hat OpenShift Pipelines Operator 1.23.1
Nexus Repository 3.93.0-06(COMMUNITY)
用戶端:Windows 11 Pro + PowerShell 5.1
CRI-O 版本要列——第 3 節的 imagefs 回報行為跟 runtime 實作綁在一起。
三層互不知道彼此存在,觸發條件、設定位置、預設值都不一樣。
| 層 | 管什麼 | 誰在跑 | 怎麼觸發 |
|---|---|---|---|
| image GC | 節點上的容器映像 | kubelet | 用量門檻 |
| pruner | PipelineRun / TaskRun | Pipelines Operator 的 CronJob | 排程 + 保留筆數 |
| cleanup | Nexus 的 component / blob | Nexus 排程 | 排程 + cleanup policy |
kubelet 每 5 分鐘看一次 imagefs 用量(週期是上游預設,本次沒實測),超過 imageGCHighThresholdPercent(預設 85)就從最久沒用到的開始刪,刪到低於 imageGCLowThresholdPercent(預設 80)。
三件常被誤會的:
imageMinimumGCAge(預設 2 分鐘)的不刪。imageMaximumGCAge 預設關閉。 開了之後超過指定時間沒用到的 image 就會被刪,不看用量。這是唯一能讓 image GC 脫離門檻觸發的設定。Kubernetes 自己對跑完的 Pod 幾乎不管——PodGC 的預設門檻是叢集內累積 12,500 顆終止 Pod(上游預設,本次沒查這台 kube-controller-manager 的實際參數),小叢集碰不到。
所以 Pipelines Operator 附了一個 CronJob:依 keep 保留最近幾筆,其餘刪掉。刪 PipelineRun 會透過 owner reference 級聯刪除它產生的 TaskRun 與 Pod。
級聯刪除是非同步的。 刪掉 PipelineRun 的當下再查一次,TaskRun 還在;幾秒後才消失。那是 Kubernetes garbage collector 的正常行為,不是沒刪掉。驗證回收類的東西時要把這個時間差算進去,不然會誤判成「級聯沒生效」。
分兩階段,第二階段清的是第一階段留下的東西。這個單向依賴是後面看懂問題的關鍵。
| 階段 | 任務型別 | 做什麼 | 需要什麼 |
|---|---|---|---|
| 一 | repository.cleanup |
依規則把 component 標記移除 | repository 掛上 cleanup policy |
| 二 | assetBlob.cleanup |
刪掉「asset 記錄已不在、blob 還在」的孤兒 | 第一階段留下的殘骸 |
cleanup policy 是規則本身(例如「最後下載超過 30 天就刪」),是獨立物件,建好之後要掛到 repository 上才生效。
| 機制 | 設定 | 狀態 |
|---|---|---|
| image GC | 85 / 80,imageMinimumGCAge 2m |
用量 28%,未觸發 |
| pruner | keep: 100,每日 08:00,resources: [pipelinerun] |
PipelineRun 3 顆,未觸發 |
| Nexus cleanup | 六個任務全部 OK |
十一個 repository 皆未掛 cleanup policy |
前兩個是量還沒到門檻。第三個是設定不完整。
Cleanup unused npm blobs assetBlob.cleanup 0 */30 * * * ? 每 30 分 OK
Cleanup unused docker blobs assetBlob.cleanup 0 */30 * * * ? 每 30 分 OK
Cleanup unused nuget blobs assetBlob.cleanup 0 */30 * * * ? 每 30 分 OK
Cleanup unused maven2 blobs assetBlob.cleanup 0 */30 * * * ? 每 30 分 OK
Cleanup service repository.cleanup 0 0 1 * * ? 每日 01:00 OK
十一個 repository 全部 cleanup = (無)。
於是第一階段沒標記任何 component,第二階段就沒有孤兒可清。四支 assetBlob.cleanup 每 30 分鐘跑一次,找不到東西,正常結束,回報 OK。
這是設定不完整,不是故障。 從任務清單、執行結果、錯誤日誌都看不出來,因為每一項都正常。
「回收量是零」是推論,不是量到的。
/service/rest/v1/tasks/{id}的欄位跟清單端點完全相同,沒有處理量、掃描數、刪除數,properties也是空的。結論靠的是「沒有 policy → 沒東西可標記 → 沒有孤兒可清」這條鏈。
補起來的方式是建 cleanup policy 再掛到 repository 上,掛好之後 repository 設定裡的 cleanup.policyNames 會有值。
/service/rest/v1/cleanup-policies在這台 CE 回 404,這是預期行為。 Sonatype 文件把 Cleanup Policies API 標為 Pro 專屬功能,CE 沒有這組端點,所以不是「查不到」而是「沒有」。反查「有沒有掛」可以走 repository 端點;新增只能走 UI,文件給的路徑是 Settings → Repository → Cleanup Policies——這台 3.93.0 CE 的 UI 是不是長這樣,我沒實際點過。
{"disabled":false,"keep":100,"resources":["pipelinerun"],"schedule":"0 8 * * *"}
目前 PipelineRun 3 顆、TaskRun 7 顆,離 keep: 100 很遠,所以現在不是問題。但覆蓋範圍有兩個缺口:
缺口一:resources 只有 pipelinerun。 由 PipelineRun 產生的 TaskRun 有 owner reference,會跟著刪;手動建立的獨立 TaskRun 沒有擁有者,不在清單裡。
這不是假設,這台已經有一顆。Day 15 驗證 path routing 時建的 probe-svc-pathrouting 是直接送出的 TaskRun,跟同一個 namespace 裡由 PipelineRun 產生的一比就看得出來:
d14-run-8nxtn-git-clone ownerReferences = PipelineRun/d14-run-8nxtn ← 會被級聯刪
probe-svc-pathrouting ownerReferences = (無) ← 沒有任何機制會清它
一次性的除錯與驗證最容易產生這種 TaskRun,而它們正好是最沒人記得回頭刪的。
這是 Operator 1.23.1 寫進去的值,不是被人改的(managedFields 只有 openshift-pipelines-operator,沒有任何 namespace 帶 operator.tekton.dev/prune.* annotation)。而 Red Hat 官方文件到 1.20 為止,pruner 的預設範例都還是 resources: [taskrun, pipelinerun] 兩個都列——1.20 那頁我這次實際開來看過,1.11 到 1.16 只看到搜尋摘要,1.21 至 1.23 的對應頁面我沒查到,沒有核對。所以能講的是「文件範例與這台實際值不一致」,還不能講「哪一版改的」。
缺口二:只涵蓋七個 namespace。 CronJob 的 args 逐一列出目標:
ci / default / gitea / hostpath-provisioner / nexus-proxy / openshift / sealed-secrets
openshift-pipelines 自己和其他 openshift-* 都不在內。
要涵蓋的話,resources 加上 taskrun,namespace 那層則要看 Operator 的判定規則。
這台的終止 Pod 有 3 顆來自
image-prunerCronJob 自己——回收機制執行時也會留下需要回收的東西。
kubelet 生效的值(取自 configz,不是設定檔,理由見 5.3):
imageGCHighThresholdPercent = 85
imageGCLowThresholdPercent = 80
imageMinimumGCAge = 2m0s
imageMaximumGCAge = 0s(未啟用)
evictionHard: imagefs.available=15% nodefs.available=10% memory.available=100Mi
imagefs.inodesFree=5% nodefs.inodesFree=5%
evictionSoft: (不存在)
後兩項這台碰不到——inode 用了 350,153 / 94,109,120(1%)。下面只討論容量那三項。
imagefs.available < 15% 就是已用超過 85%,和 image GC 的啟動門檻同一個數字。
而且這台沒有 evictionSoft,只有 hard eviction——達到門檻立即驅逐,沒有寬限期。
Kubernetes 用三個名詞:nodefs(emptyDir、log)、imagefs(唯讀映像層)、containerfs(容器可寫層)。多節點的「split image filesystem」是把 imagefs 分出去,讓兩組門檻各自獨立。官方文件自己也註明,這三個名稱指的是 kubelet 觀察到的檔案系統,不必然對應到不同的掛載點。
這台的 kubelet stats:
nodefs capacity=192,668,479,488 avail=139,225,587,712
imageFs capacity=192,668,479,488 avail=139,225,587,712
containerFs capacity=192,668,479,488 avail=139,225,587,712
逐位元組相同。 三個名詞指向同一個檔案系統,兩組門檻疊在同一個 85%。
imageFs.usedBytes不能用。 這台回報 7,051,929(約 7 MB),而實際容器儲存是 40 G。共用檔案系統時 CRI-O 不分開統計 imagefs 用量。要看容器儲存用podman system df或du -shx。
依官方文件:只有 nodefs 的情況下,kubelet 觸發 eviction 會先回收死掉的 Pod 與容器,再刪除所有未使用的 image——比 image GC 的「刪到 80% 為止」更徹底。
所以門檻重合的後果不是「GC 沒時間」,而是兩條回收路徑同時啟動,eviction 那條刪得更狠。壓不回門檻以下才會標記 DiskPressure,然後打上 node.kubernetes.io/disk-pressure:NoSchedule 汙點,新的 Pod 不再排到這個節點。
症狀是 Pod 停在 Pending 且 nodeName 為空——跟資源不足的排隊看起來一樣,差別在 oc describe node 的 Conditions 與 Taints。
這台用量 28%,沒觸發過。上面的回收順序是官方文件的說法,不是實測——要實測必須把磁碟填到 85%,這台在用不能這樣做。症狀描述則來自另一套環境的紀錄,兩台數字不能互相對照。
imageMaximumGCAge,讓 image GC 改成按年齡定期清,脫離門檻觸發imageGCHighThresholdPercent(例如 70),讓 GC 早於驅逐門檻啟動兩者都要改 KubeletConfig,屬節點層級設定,本篇不做。
10 顆 PV:9 × 179Gi + 1 × 30Gi = 名目 1,641 Gi
實體盤 180 G,實際佔用 1.1 G
hostPath provisioner 把資料放在節點的 /var/lib/csi-hostpath-data/pvc-*——普通目錄,沒有 quota。
雲端的 PV 背後是真實裝置,capacity 對應實際配額,寫超過得到 ENOSPC 而不影響其他 volume。這裡的 capacity 只是 PV 物件上的一個數字,用來滿足 PVC 的容量請求比對,執行期不做限制。
這不是實作缺漏。CSI hostpath driver 的專案首頁就自述是單節點用的非生產範例 driver,容量是用 -capacity 旗標模擬出來的;沒設定時就回報上限值。名目容量本來就不打算對應到任何真實配額。
實務上是:十顆 PV 共用剩下的空間,先寫先得。 寫爆得到的錯誤是磁碟滿,不是「你的額度用完了」。
另外有三顆 Released 的 PV,因為 Retain 政策資料還在磁碟上(合計 4.9 M)。Retain 的語意就是保留給人工處理——前面三層是機制沒生效,這個是本來就沒有機制。
由難發現到好發現排列。
du 會跨路徑去重(最難發現)同一組路徑,只調換順序,數字就變:
du -shx /var/lib/kubelet /var/lib/csi-hostpath-data
2.1G /var/lib/kubelet
6.5M /var/lib/csi-hostpath-data ← 幾乎歸零
du -shx /var/lib/csi-hostpath-data /var/lib/kubelet
1.1G /var/lib/csi-hostpath-data
1.1G /var/lib/kubelet ← 換這邊縮水
各自單獨跑
2.1G /var/lib/kubelet
1.1G /var/lib/csi-hostpath-data
CSI hostpath driver 把 PV 目錄 bind-mount 進 kubelet 的 pod 目錄,兩條路徑指向同一組 inode:
482345753 /var/lib/csi-hostpath-data/pvc-95ec8cf3-…
482345753 /var/lib/kubelet/pods/…/volumes/kubernetes.io~csi/pvc-95ec8cf3-…/mount
GNU du 在單次呼叫內記住看過的 inode 只算一次,歸給先走到的那條路徑。參數順序決定數字怎麼分配。
這也不是實作巧合。GNU coreutils 手冊本身就寫了:多個連結指向同一個檔案時只計一次,而引數順序會決定哪一條被計入,改順序就可能改變輸出的數字與項目。手冊講的情境是 hard link,這裡是 bind-mount,但去重依據同樣是 inode。
難發現的原因是它沒有任何異常訊號:-x 加了、單一數字合理、總和也對得上 df。只有在「把同一份資料當成兩個獨立項目列進分解表」時才出錯——而列目錄佔用分解表正是這類文章的標準寫法。
對策:分解表裡有 CSI volume 相關路徑時,一律各自單獨跑,或把重疊的兩者併成一項。
du -sh 在容器儲存上重複計算df /sysroot 已用 50 G
du -sh containers/storage 116 G ← 錯
du -shx containers/storage 40 G ← 對
podman system df 38.87 GB
節點上有 229 個 overlay on /var/lib/containers/storage/... 掛載點(快照值,隨執行中容器變動)。每個執行中容器的 merged 目錄是 overlay 的合併視圖,du 走進去會把下層再算一次。
訊號很明顯:單一子目錄大於整顆盤。
kubelet.conf 裡的 0s 有兩種意思設定檔裡 grep imageGC|imageMinimum|imageMaximum|eviction,只有三行:
evictionPressureTransitionPeriod: 0s
imageMaximumGCAge: 0s
imageMinimumGCAge: 0s
configz 的生效值:
| 鍵 | 檔案 | 生效值 |
|---|---|---|
imageGCHighThresholdPercent |
不存在 | 85 |
imageGCLowThresholdPercent |
不存在 | 80 |
evictionHard |
不存在 | 五項 |
imageMinimumGCAge |
0s |
2m0s |
evictionPressureTransitionPeriod |
0s |
5m0s |
imageMaximumGCAge |
0s |
0s(真的停用) |
同一個 0s,前兩個是「未指定,套用預設」,最後一個是「明確關閉」,從檔案分不出來。而三個關鍵門檻根本不在檔案裡。
這不是 OpenShift 特有的行為。上游 KubeletConfiguration 型別的欄位註解就寫著 evictionPressureTransitionPeriod 的 0s 會被轉成預設的 5m,上游也有 issue 專門講這件事(標題直接叫「silently default 0s to 5m」)。
只看設定檔會得出「這台沒有設任何門檻」的結論。 生效值要問 kubelet:
& $OC --kubeconfig $kc get --raw "/api/v1/nodes/crc/proxy/configz"
順帶:
evictionPressureTransitionPeriod的5m0s是解除DiskPressure前必須連續維持在門檻以下的最短時間,用來防止狀態抖動,不是標記前的緩衝。
/dev/sda4 179.4G 容量 50G 已用(28%) 130G 可用
| 項目 | 大小 |
|---|---|
/var/lib/containers/storage(容器 image) |
40 G |
/var/log |
3.0 G |
/var/lib/kubelet(已含 PV 資料 1.1 G,見 5.1) |
2.1 G |
/var/lib/etcd |
485 M |
image 佔八成。其中 29 張 dangling,含兩張接近 1 GB——這些是 GC 觸發時最先被清掉的一批。
image GC 啟動 (85%) 還可再長約 102 G
imagefs 驅逐 (85%) 同上,重合
nodefs 驅逐 (90%) 還可再長約 111 G
長期平均:40 G ÷ 約 93 天 ≈ 0.43 G/天
單日觀測:46 G → 50 G ≈ 3.7 G/天
差了近十倍,兩個都不能用。長期平均混進了叢集初裝時一次拉下來的大量 image(前八大有四張屬於 OpenShift 自身的 release 與 operator index);單日 3.7 G 則是一次 Angular npm install 加三次 image pull 的突發。
要有可信數字得定期取樣,而這台沒有:
$ oc adm top node
error: Metrics API not available
用量可以 df 查。
evictionPressureTransitionPeriod 的 0s 會被靜默轉成 5m:https://github.com/kubernetes/kubernetes/issues/129548
/proxy/configz 取生效值:https://www.redhat.com/en/blog/image-garbage-collection-in-openshift
terminated-pod-gc-threshold:https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/