chundev
日期:2026-05-25 環境:k3s on VMware vSphere + Longhorn (single replica) + CloudNativePG
正式環境 Web 應用突然全部登入失敗,網頁打得開、登入一定錯。一路追:
prisma:error could not open file "global/pg_filenode.map": Input/output errorFATAL: I/O error,pod 卻顯示 1/1 Ready(probe 沒抓到)state=detached, robustness=faulted,replica failedAt=2026-05-22T14:47:08Zjournalctl -k 顯示 watchdog: BUG: soft lockup ... vmballoon_inflate ... [vmw_balloon] — VMware host memory pressure → balloon driver 在 guest VM 內 inflate 卡死 kernel CPU → Longhorn instance-manager 失聯 → replica 被標 faulted → 之後檔案系統 I/O error整段故障被以下三個獨立疏失放大成 4 天無感停機:
revisionCounterDisabled: true → 不會自動 salvage,必須手動清 failedAt/readyz 內部只跑 pg_isready(測 postmaster 是否 listen),不執行任何 SQL → I/O 死掉但 pod 還是 Readyif pg_dump | gzip 沒設 set -o pipefail → pg_dump 失敗時 gzip 仍 exit 0 → 留下 ~20 byte 的空 gzip header,但 BackupRecord 表記成 success這篇是完整指令導覽,可以照著重跑一遍。
spec.instances: 1、numberOfReplicas: 1 — single point of failure 滿載使用者回報:「網頁打得開但登入一定失敗。」截圖看登入後立刻錯誤,看不出 server-side 內容。
prisma:error could not open file "global/pg_filenode.map": Input/output error
[ldap-plugin] Error: Error [DriverAdapterError]: could not open file "global/pg_filenode.map": Input/output error
at N.onError (.next/server/chunks/_0jhm2f9._.js:1:11599)
clientVersion: '7.4.2',
[cause]: {
originalCode: '58030',
originalMessage: 'could not open file "global/pg_filenode.map": Input/output error',
kind: 'postgres',
code: '58030',
severity: 'FATAL',
message: 'could not open file "global/pg_filenode.map": Input/output error',
}
SQLSTATE 58030 = io_error(PostgreSQL errcodes 文件)。
第一個想知道的事:pod 是否在跑?最近有沒有重新部署?code 變更會不會是原因?
# Application pod 狀態(讀 your-namespace, your-app-label 替換)
kubectl get pods -n NAMESPACE -o wide
kubectl get deployments -n NAMESPACE
git log --oneline -10
kubectl get events -n NAMESPACE --sort-by='.lastTimestamp' | tail -30
關鍵欄位:
RESTARTS 是否最近增加 → 若沒增加表示 pod 沒崩過,bug 不在 application 容器內這次的結果: pod AGE 3d15h、RESTARTS=0,最近 commit 全是依賴升級。所以 bug 不在 app code。
# 完整 200 行
kubectl logs -n NAMESPACE POD_NAME --tail=200
# 過濾關鍵字(auth/db/error/io)
kubectl logs -n NAMESPACE POD_NAME --tail=500 \
| grep -iE "error|fail|auth|prisma|io.error|timeout" \
| tail -50
這次撈到 prisma:error could not open file "global/pg_filenode.map": Input/output error。
pg_filenode.map 是什麼?
PostgreSQL PGDATA/global/pg_filenode.map 是 system catalog 從 OID 對應到實際 disk file 的關鍵元資料檔(官方說明)。PostgreSQL 啟動時必讀。如果這個檔讀不到,連 system catalog 都查不到 → 任何 query 都 FATAL。
而且 error 是 Input/output error(POSIX errno EIO)→ 不是 “permission denied”、不是 “no such file” → 是 block device 層的 I/O 錯誤,問題不在 PostgreSQL,在它底下的 storage。
# 進 postgres container 跑 psql(CloudNativePG 的 unix socket 在 /controller/run/)
kubectl exec -n NAMESPACE POSTGRES_POD -c postgres -- \
psql -U postgres -d YOUR_DB -c "SELECT 1;"
這次的結果:
psql: error: connection to server on socket "/controller/run/.s.PGSQL.5432" failed:
FATAL: could not open file "global/pg_filenode.map": Input/output error
command terminated with exit code 2
重點推論:
pg_filenode.map,讀不到就掛但這時 pod 顯示 1/1 Ready 是怎麼回事?看 readiness probe:
kubectl get pod -n NAMESPACE POSTGRES_POD -o jsonpath='{.spec.containers[?(@.name=="postgres")].readinessProbe}' \
| python3 -m json.tool
{
"failureThreshold": 3,
"httpGet": {
"path": "/readyz",
"port": 8000,
"scheme": "HTTPS"
},
"periodSeconds": 10,
"successThreshold": 1,
"timeoutSeconds": 5
}
CloudNativePG 內建 /readyz HTTPS endpoint,內部用 pg_isready 檢查。pg_isready 只測「postmaster 是否在 listen」,不會送 SQL。所以 postmaster 還活著但底下 PGDATA filesystem 已死的情況,probe 完全感覺不到。
這是設計上的 readiness 失明點。
CNPG 把 PGDATA 存在 PVC,PVC 對應到 Longhorn volume。
# 1) 找出 postgres pod 對應的 PVC
kubectl get pvc -n NAMESPACE
# 2) PVC name 對應到 PV/volume 名稱(在 Longhorn 內 volume name = pvc UUID)
kubectl get pvc -n NAMESPACE PVC_NAME -o jsonpath='{.spec.volumeName}'
# 3) 看 Longhorn cluster 狀態
kubectl get cluster -n NAMESPACE
# (CloudNativePG 的 cluster CRD,不是 Longhorn 的)
# 4) Longhorn volume 健康度
kubectl get volume.longhorn.io -n longhorn-system VOLUME_NAME \
-o jsonpath='State={.status.state} Robustness={.status.robustness} NodeID={.spec.nodeID}{"\n"}'
# 5) Longhorn replica(block-level data 實體存放)
kubectl get replicas.longhorn.io -n longhorn-system \
-o jsonpath='{range .items[?(@.spec.volumeName=="VOLUME_NAME")]}{.metadata.name}{" node="}{.spec.nodeID}{" state="}{.status.currentState}{" failedAt="}{.spec.failedAt}{" healthyAt="}{.spec.healthyAt}{"\n"}{end}'
這次的結果:
State=detached Robustness=faulted NodeID=
ReplicaState=stopped failedAt=2026-05-22T14:47:08Z healthyAt=2026-01-11T03:30:34Z
state=detached → Longhorn 沒把 block device 給 postgres pod(但 pod 還抱著之前的舊 mount)robustness=faulted → 所有 replica 都壞了也看一下 Longhorn engine / disk 是否還健康(為了排除磁碟空間爆掉):
# 看每個 node 上的 disk 健康度
for n in NODE1 NODE2 NODE3 NODE4; do
echo "--- $n ---"
kubectl get nodes.longhorn.io -n longhorn-system $n \
-o jsonpath='{range .status.diskStatus.*}{.diskUUID}{" | scheduled="}{.storageScheduled}{" available="}{.storageAvailable}{" | conds: "}{range .conditions[*]}{.type}={.status}{"("}{.reason}{") "}{end}{"\n"}{end}'
done
這次發現的副產品:3 個 node 都掛了同一個 NFS path 當 Longhorn disk → filesystem UUID 共用 → 全部 Ready=False(DiskFilesystemChanged)。跟此次故障無關但是個 latent 設計問題(NFS 不該當 Longhorn disk,因為 Longhorn 期望 disk 是 node-local block device)。
Longhorn 的 salvage 機制:當 volume robustness=faulted 時,UI 上應該出現 “Salvage” 按鈕讓使用者選 last-known-good replica。但是當 volume 設定 revisionCounterDisabled: true 時,Longhorn 為了資料一致性不會自動 salvage,必須手動操作。
UI 也可能沒按鈕(不同版本行為不一)。可以直接從 CLI 操作。
mkdir -p /tmp/longhorn-salvage && cd /tmp/longhorn-salvage
kubectl get volumes.longhorn.io -n longhorn-system VOLUME_NAME -o yaml > volume.before.yaml
kubectl get replicas.longhorn.io -n longhorn-system REPLICA_NAME -o yaml > replica.before.yaml
failedAt,並把 healthyAt 設為 lastHealthyAtkubectl patch replicas.longhorn.io -n longhorn-system REPLICA_NAME \
--type=merge -p '{"spec":{"failedAt":"","healthyAt":"LAST_HEALTHY_TIMESTAMP"}}'
LAST_HEALTHY_TIMESTAMP 從 kubectl describe replica 找 Last Healthy At: 欄位。
# Volume 應該變 attached + healthy
kubectl get volume.longhorn.io -n longhorn-system VOLUME_NAME \
-o jsonpath='State={.status.state} Robustness={.status.robustness} NodeID={.spec.nodeID}{"\n"}'
# Pod 重啟(CNPG 偵測到 volume 健康後 reconcile)
kubectl wait --for=condition=Ready pod -n NAMESPACE POSTGRES_POD --timeout=60s
# 看 postgres recovery log
kubectl logs -n NAMESPACE POSTGRES_POD -c postgres --tail=30 \
| grep -E "interrupted|recovery|redo|ready to accept"
這次的結果(PostgreSQL 自動 WAL recovery,0 資料損失):
LOG: database system was interrupted; last known up at 2026-05-22 14:05:01 UTC
LOG: database system was not properly shut down; automatic recovery in progress
LOG: redo starts at 27/72004070
LOG: redo done at 27/72004178
LOG: checkpoint complete: ... lsn=27/73000028
LOG: database system is ready to accept connections
重點推論:
redo done 沒有 error → WAL 完整 replay,所有已 commit 的事務都還在unable to read block / page corruption 訊息 → block device 層的損壞沒擴散到 PGDATA 內部# 應用本身的 sanity check
kubectl exec -n NAMESPACE POSTGRES_POD -c postgres -- \
psql -U postgres -d YOUR_DB -c "SELECT count(*) FROM \"User\";"
# 最後一筆操作時間(如果有 audit log table)
kubectl exec -n NAMESPACE POSTGRES_POD -c postgres -- \
psql -U postgres -d YOUR_DB -c "SELECT MAX(\"createdAt\") FROM \"AuditLog\";"
# App 端是否還在噴 error
kubectl logs -n NAMESPACE APP_POD --tail=50 --since=30s \
| grep -iE "prisma:error|error|FATAL"
AuditLog 的最後一筆時間應該跟「使用者最後一次使用系統」吻合 — 若吻合 = 證實這段時間使用者真的沒成功操作(不是有人成功但你沒看到)。
如果故障是 storage 層的 node-level 事件,同一個 node 上其他 PVC 也可能 fault。看 CNPG operator log 一下子就會發現別的 namespace 也在炸:
# 看整個 CNPG operator log,找 prisma 之外的同樣錯誤
kubectl logs -n cnpg-system -l app.kubernetes.io/name=cloudnative-pg --tail=200 \
| grep -iE "fail|error|warning|disk|pg_filenode" \
| head -20
這次撈到:
namespace=ANOTHER_NAMESPACE name=postgres-cluster
error: ...FATAL: could not open file "global/pg_filenode.map": Input/output error
另一個 namespace 也有同樣症狀,pod 也是 1/1 Running 偽裝健康。同樣 salvage 流程救起來。
對於 single-node Longhorn 上跑很多 PVC 的情況,節點層事件會放大成多 PVC 同時死。
到這步已經把 application 修復了,但還沒回答「為什麼 5/22 22:47 那一刻 Longhorn replica 會被標 faulted」。
要看 node 上的 systemd journal、kernel ring buffer、k3s-agent log。kubectl debug node 是最方便的方式,但 attach 模式會卡住(如果 stdin 沒接),改用自己寫 manifest 起 privileged pod 並 chroot 進 host:
# 1) 寫一個 forensic pod manifest(hostPID + privileged + mount host root)
cat <<'EOF' > /tmp/node-forensic.yaml
apiVersion: v1
kind: Pod
metadata:
name: node-forensic
namespace: default
spec:
nodeName: TARGET_NODE
hostPID: true
hostNetwork: true
restartPolicy: Never
containers:
- name: forensic
image: ubuntu:24.04
securityContext:
privileged: true
command: ["sleep", "3600"]
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: /
EOF
kubectl apply -f /tmp/node-forensic.yaml
kubectl wait --for=condition=Ready pod node-forensic --timeout=30s
# 2) 從 forensic pod chroot 進 host 跑指令
kubectl exec node-forensic -- chroot /host bash -c 'COMMAND'
# 用完一定要刪
kubectl delete pod node-forensic --grace-period=0 --force
先確認 journal 有保留到故障時段(systemd persistent journal 必須有設):
kubectl exec node-forensic -- chroot /host bash -c '
echo "=== Journal disk usage ==="; journalctl --disk-usage
echo "=== Boot list ==="; journalctl --list-boots
echo "=== Oldest entry ==="; journalctl --no-pager -o short-iso -n 1 --reverse | head -2
echo "=== Newest entry ==="; journalctl --no-pager -o short-iso -n 1 | head -2
'
若 oldest entry 比故障時間還新,表示 journal 已 rotate 掉了,這條路斷掉。若 journal 沒設成 persistent,journalctl --list-boots 只會顯示當前 boot 的 log(重啟過就沒了)。
從 replica failedAt 時間(這次是 2026-05-22T14:47:08Z)往前看 ±2 小時的 priority=err:
kubectl exec node-forensic -- chroot /host bash -c '
journalctl --no-pager -o short-iso \
--since "2026-05-22 12:00:00 UTC" \
--until "2026-05-22 16:00:00 UTC" \
-p err 2>&1 | head -100
'
這次跳出來大量 iSCSI 連線失敗 + 一個 zombie k3s.service 反覆 restart loop(restart counter 96 萬次)。
k3s.service zombie 是干擾項:實際運作的是 k3s-agent.service,但 unit 名字相近容易誤判。確認:
kubectl exec node-forensic -- chroot /host bash -c '
systemctl list-units --all "k3s*" --no-pager
systemctl status k3s-agent.service --no-pager | head -10
'
UNIT LOAD ACTIVE SUB DESCRIPTION
k3s-agent.service loaded active running Lightweight Kubernetes
k3s.service loaded activating auto-restart Lightweight Kubernetes
k3s-agent 4 個月 active running,沒重啟過 → kubelet 沒掛,故障在 kubelet 之下。
這是關鍵的兩步。
# 5/22 12:00~16:00 UTC 的 OOM events
kubectl exec node-forensic -- chroot /host bash -c '
journalctl --no-pager -o short-iso \
--since "2026-05-22 12:00:00 UTC" \
--until "2026-05-22 16:00:00 UTC" -k 2>&1 \
| grep "invoked oom-killer"
'
# I/O errors / scsi reset / ext4 journal errors
kubectl exec node-forensic -- chroot /host bash -c '
journalctl --no-pager -o short-iso \
--since "2026-05-22 12:00:00 UTC" \
--until "2026-05-22 16:00:00 UTC" -k 2>&1 \
| grep -iE "i/o error|input/output|sd[a-z]: error|EXT4-fs error|JBD2|read-only|reset.*scsi|Buffer I/O error"
'
# Soft lockup(kernel watchdog 偵測到 CPU 卡住)
kubectl exec node-forensic -- chroot /host bash -c '
journalctl --no-pager -o short-iso \
--since "2026-05-22 12:00:00 UTC" \
--until "2026-05-22 16:00:00 UTC" -k 2>&1 \
| grep -iE "soft lockup|hung_task|RIP:|Call Trace:" | head -30
'
這次撈到:
2026-05-22T12:07:14 kernel: postgres invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=985
2026-05-22T12:07:14 kernel: oom-kill:constraint=CONSTRAINT_MEMCG ... oom_memcg=/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podc83b4c23.../
2026-05-22T12:07:14 kernel: Memory cgroup out of memory: Killed process 2402326 (postgres) ...
2026-05-22T14:37:22 kernel: postgres invoked oom-killer: ...
2026-05-22T14:47:08 kernel: watchdog: BUG: soft lockup - CPU#4 stuck for 28s! [kworker/4:3:2736300]
2026-05-22T14:53:56 kernel: watchdog: BUG: soft lockup - CPU#1 stuck for 41s! [longhorn-instan:12945]
2026-05-22T14:57:12 kernel: Buffer I/O error on dev sdd, logical block 557056, lost sync page write
2026-05-22T14:57:12 kernel: JBD2: I/O error when updating journal superblock for sdd-8.
這就是觸發鏈:
| 時間 | 事件 |
|---|---|
| 12:07:14 | postgres pod cgroup hit memory limit,整個 pod cgroup OOM killed(memory.oom.group set) |
| 14:37:22 | postgres 再度 OOM |
| 14:47:08 | kworker soft lockup CPU 28s → 同一秒 Longhorn 標 replica failedAt |
| 14:53:56 | longhorn-instan process soft lockup CPU 41s |
| 14:57:12 | Buffer I/O error on dev sdd + ext4 journal corruption |
但 kworker 在做什麼才會卡住?看 stack:
# 取 14:47:08 那一刻完整 kernel 訊息(含 stack trace)
kubectl exec node-forensic -- chroot /host bash -c '
journalctl --no-pager -o short-iso \
--since "2026-05-22 14:46:30 UTC" \
--until "2026-05-22 14:48:00 UTC" -k 2>&1 \
| grep -vE "^.*Modules linked in:" \
| head -60
'
Modules linked in: 行噪音很大、每行印兩三千字元,要先 filter 掉才看得到關鍵 Call Trace。
關鍵幾行:
2026-05-22T14:47:08 kernel: Workqueue: events_freezable vmballoon_work [vmw_balloon]
2026-05-22T14:47:08 kernel: RIP: 0010:clear_page_erms+0x7/0x10
2026-05-22T14:47:08 kernel: Call Trace:
2026-05-22T14:47:08 kernel: vmballoon_inflate+0x352/0x400 [vmw_balloon]
2026-05-22T14:47:08 kernel: vmballoon_work+0x108/0x1d1 [vmw_balloon]
2026-05-22T14:47:08 kernel: process_one_work+0x184/0x3a0
2026-05-22T14:47:08 kernel: worker_thread+0x306/0x440
2026-05-22T14:47:08 kernel: kthread+0xf2/0x120
Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform
找到 root cause:VMware vmballoon driver inflate page 時把 kernel CPU 卡死。
VMware vSphere hypervisor 在 host 記憶體不夠時,會透過 guest 內的 vmware-balloon driver 跟 guest 索回 page:
vmw_balloon driver「我要 X 頁」alloc_pages() 拿 page,標成「不能 swap、不能 release」,然後告訴 hypervisor「拿去」問題: 當 ESXi host 強烈 memory pressure 時,vmballoon inflate 會嘗試一次拿很多 page。在 guest VM 內:
alloc_pages → clear_page_erms(清零)→ 占用 CPUwatchdog 偵測 → soft lockup完整因果鏈:
ESXi host memory pressure
↓
vmballoon driver 在 guest VM 內 inflate
↓
guest VM 內 memory squeeze + CPU 被 vmballoon kthread 卡住
↓
postgres pod 觸發 cgroup OOM
↓
Longhorn instance-manager process 因 CPU schedule 不到 → soft lockup
↓
longhorn-manager 在其他 node 偵測 instance-manager 失聯 → 標 replica failedAt
↓
此時 instance-manager 與 iSCSI engine 之間的 TCP session 斷
↓
Buffer I/O error on /dev/sdd → ext4 journal corruption
↓
此後該 volume 上所有檔案讀寫都 I/O error
↓
應用沒 user 接觸到下班,到週末沒人發現,週一才被報
而且這個事件同時影響該 node 上所有 Longhorn replica — 我們有兩個不同 namespace 的 postgres cluster,replica 巧合都排在同一個 node,failedAt 時間戳完全相同:2026-05-22T14:47:08Z。
| 訊號 | 為什麼沒人看到 |
|---|---|
| postgres pod READY 1/1 | readiness probe 用 pg_isready,不執行 SQL,I/O error 看不到 |
| CNPG operator log 噴錯 | 沒有 alerting,沒人盯著 |
| pg_dump backup 連續 3 天失敗 | 也沒有 alerting;而且 backup script 寫的方式 (見下) 會留下 ~20 byte 空檔但 BackupRecord 標 success |
| Longhorn volume faulted | 沒有 alerting |
| 5/22 週五晚上發生,週末沒人用 ERP | 直到週一上班才有人試登入 |
四個 alert 缺口,加上週末,等於 60+ 小時零察覺。
# 簡化版
set -e
BACKUP_PATH="/backups/$(date +%Y%m%d-%H%M%S).sql.gz"
if pg_dump --verbose | gzip > ${BACKUP_PATH}; then
# ... 寫入 BackupRecord 標 success
else
# ... 寫入 BackupRecord 標 failed
fi
set -e 沒抓到 pg_dump 失敗,因為 pg_dump | gzip 中 pg_dump 不是最後一個指令。set -o pipefail 才會把整條 pipe 的 exit code 設成「最後一個 non-zero」。
結果:DB 壞掉時 pg_dump 在 stderr 噴 error 後 exit 1,但 gzip 收到空 stdin 後仍正常結束 exit 0。整個 if 判斷成功 → 寫 4-byte gzip header 到檔案 → BackupRecord 標 success → 沒有人察覺。
修正:
set -euo pipefail
-u (unset var error)、-o pipefail (pipe 失敗傳遞 exit code),是 production shell script 的最低門檻。
# 1) 找 PVC → volume → replica
PVC_NAME=postgres-cluster-1
NAMESPACE=YOUR_NAMESPACE
VOL=$(kubectl get pvc -n $NAMESPACE $PVC_NAME -o jsonpath='{.spec.volumeName}')
REPLICA=$(kubectl get replicas.longhorn.io -n longhorn-system \
-o jsonpath="{range .items[?(@.spec.volumeName=='$VOL')]}{.metadata.name}{end}")
LAST_HEALTHY=$(kubectl get replica.longhorn.io -n longhorn-system $REPLICA \
-o jsonpath='{.spec.lastHealthyAt}')
echo "Volume: $VOL"
echo "Replica: $REPLICA"
echo "Last healthy: $LAST_HEALTHY"
# 2) 備份 yaml
mkdir -p /tmp/longhorn-salvage && cd /tmp/longhorn-salvage
kubectl get volumes.longhorn.io -n longhorn-system $VOL -o yaml > volume.before.yaml
kubectl get replicas.longhorn.io -n longhorn-system $REPLICA -o yaml > replica.before.yaml
# 3) 清 failedAt(觸發 salvage)
kubectl patch replicas.longhorn.io -n longhorn-system $REPLICA \
--type=merge -p "{\"spec\":{\"failedAt\":\"\",\"healthyAt\":\"$LAST_HEALTHY\"}}"
# 4) 等 Longhorn 自動 attach + CNPG 自動 reconcile postgres pod
until [ "$(kubectl get volume.longhorn.io -n longhorn-system $VOL -o jsonpath='{.status.state}')" = "attached" ]; do
sleep 3
done
kubectl wait --for=condition=Ready pod -n $NAMESPACE $PVC_NAME --timeout=120s
# 5) 驗證 postgres recovery
kubectl logs -n $NAMESPACE $PVC_NAME -c postgres --tail=30 \
| grep -E "interrupted|recovery|redo|ready to accept"
# 6) 手動補 backup(之前的可能都是空檔)
TS=$(date +%Y%m%d-%H%M%S)
kubectl create job --from=cronjob/postgres-pgdump-backup pgdump-manual-$TS -n $NAMESPACE
kubectl create job --from=cronjob/postgres-basebackup basebackup-manual-$TS -n $NAMESPACE
如果 root cause 是 host memory pressure,要改 ESXi 端記憶體配置或加 reservation。VM 端就是調 RAM、重啟。但重啟前 single-replica 的 PVC 是個問題 — node 一關,volume 就再次 detach + faulted(剛經歷的一模一樣)。
正確做法:先把 replica 數臨時升到 2,等同步完再重啟。
# 1) 把 single-replica volume 升到 2
for VOL in VOLUME_A VOLUME_B; do
kubectl patch volume.longhorn.io -n longhorn-system $VOL \
--type=merge -p '{"spec":{"numberOfReplicas":2}}'
done
# 2) 等到 robustness=healthy(同步完)
until [ "$(kubectl get volume.longhorn.io -n longhorn-system VOLUME_A -o jsonpath='{.status.robustness}')" = "healthy" ]; do
sleep 5
done
# 3) Cordon + drain
kubectl cordon TARGET_NODE
kubectl drain TARGET_NODE --ignore-daemonsets --delete-emptydir-data --force --grace-period=120
# 4) 確認 node 上沒有 attached volume
kubectl get volume.longhorn.io -n longhorn-system \
-o jsonpath='{range .items[?(@.spec.nodeID=="TARGET_NODE")]}{.metadata.name}{"\n"}{end}'
# 5) Shutdown VM、改 RAM、開機
# 6) Node Ready 之後 uncordon
until kubectl wait --for=condition=Ready node/TARGET_NODE --timeout=10s; do sleep 5; done
kubectl uncordon TARGET_NODE
強烈建議:步驟 6 之後不要把 replica 數調回 1。多一份 replica 換來的 HA 餘裕,比省下的 disk 空間值得太多。
pg_filenode.map: Input/output error 不是 PostgreSQL 的錯,是底下 block storage I/O 死了。 SQLSTATE 58030 是 I/O 錯誤類別,跟 page corruption(XX001 系列)不同。前者要修 storage,後者要 PITR。
pg_isready 不執行 SQL,只測 postmaster 是否 listen。 用它做 readiness probe,等於只測「server 還在開 port」,不能測 server 「能不能工作」。CloudNativePG /readyz 用這個是設計取捨,但對 I/O 死掉這種場景失明。production 建議加額外 SQL probe(如 psql -c "SELECT 1" 包進 livenessProbe)。
Longhorn 對於 revisionCounterDisabled: true 的 volume 不會自動 salvage,這是資料安全設計(避免 split-brain 寫壞資料)。手動 salvage 等於人類確認「我願意接受這個 replica 作為 truth source」。
Single replica + revisionCounterDisabled = 沒有 HA,全靠 backup。這個組合在很多輕量化部署是預設的(CloudNativePG single instance + Longhorn numberOfReplicas: 1),但風險高。如果 host 上有不可預測的事件(hypervisor 行為、kernel bug、單磁碟壞)就是 100% 影響。至少要 numberOfReplicas: 2。
同一個 node 上的所有 Longhorn replica 共用一個 instance-manager pod。 node 上的 kernel 事件會同時影響該 node 所有 replica。要分散風險就要把不同 cluster 的 replica 排到不同 node(Longhorn data locality + taint/toleration 控制)。
rmmod vmw_balloon + blacklist(但會放棄 hypervisor 動態 memory 管理,trade-off)Shell script set -e 不夠,要 set -euo pipefail。 Pipe 中段的失敗 (pg_dump | gzip) 若沒 pipefail,整條 pipe 仍然 exit 0。這個 1 字差距會把「備份失敗」隱藏成「備份成功,內容是 20 bytes 的空檔」。
沒有 alerting,就等於 production down 沒人知道。 這次有四道防線(pod readiness、CNPG status、Longhorn robustness、backup job status)全部失守,每一道都沒人盯。最少最少要有一條:定期 curl 一個會打 DB 的 endpoint,掛了就 ping 人。
kubectl debug node 在 attach 模式會 hang,要嘛改用自己寫 hostPID/privileged pod manifest(更可控),要嘛改 non-interactive 模式。debug pod 結束後要手動刪,--rm 配 stdin 沒接時不會自動清。
/etc/systemd/journald.conf 沒設 Storage=persistent 或 /var/log/journal/ 不存在,重啟 node 後上一次 boot 的 log 就沒了。production 必開 persistent journal。pg_filenode.map 的角色io_error) 定義revisionCounterDisabled 行為與手動 salvage/readyz, /healthz, /startupz 細節vmw_balloon driver — VMware balloon 設計Storage= 設定set -e pitfalls — 為什麼還要 pipefail