Multi-pod 部署 runbook (Phase 5 S5)
這份 runbook 說明在 S1(lease primitive)、S2(per-pod producer instance id)、S3(Ledger fencing RPC)、S4(sidecar fencing-lease 生命週期)都已上線之後,如何安全地把 sidecar、outbox-forwarder、ttl-sweeper 這三個服務做水平 / 多節點部署。
TL;DR:outbox-forwarder 與 ttl-sweeper 可以安全擴到 N 個 pod,用 leader election 協調。sidecar 是 DaemonSet — 每個節點一個 pod — 同一時間只有一個 sidecar pod 能持有設定好的 fencing scope;其餘的會在啟動時 fail-closed,等著接手。這是 active/standby,不是水平擴展。
各元件的擴展模型
Section titled “各元件的擴展模型”outbox-forwarder
Section titled “outbox-forwarder”- Type:Deployment。
- Multi-pod 模型:leader election。只有 leader 會輪詢
audit_outboxtable 並轉送到 canonical-ingest;standby replica 會持續對 lease 送 heartbeat,具備接手資格。 - Helm gate:當
leaderElection.mode = disabled時,outboxForwarder.replicas > 1會被拒絕(S1 gate;這是預期行為)。 - 要設什麼:
outboxForwarder:replicas: 2 # or 3 for region-spreadleaderElection:mode: postgres # or k8s after S5+S7region: us-west-2ttlMs: 15000renewIntervalMs: 5000
- Failover 行為:當 active 的 leader pod 掛掉,Postgres lease 的 TTL 會在
ttlMs(預設 15s)之後過期。某個 standby 呼叫acquire_leaseSP 並勝出,轉送即恢復。不會產生重複的 canonical event,因為audit_outbox.pending_forward是 durable cursor。
ttl-sweeper
Section titled “ttl-sweeper”- Type:Deployment。
- Multi-pod 模型:與 outbox-forwarder 完全一致(leader election;只有 leader 輪詢 + 釋放過期的 reservation)。
- Helm gate:
leaderElection.mode=disabled時,ttlSweeper.replicas > 1會被拒絕(S1)。 - 建議值:
replicas: 2做 HA。再往上加沒有額外吞吐量(只有一個 pod 在 sweep)。
sidecar
Section titled “sidecar”- Type:DaemonSet(設計上每個節點一個 pod — 與掛載 UDS adapter socket 的 workload pod 共置)。
- Multi-pod 模型:每個 pod 透過 downward API 由
metadata.name推導出唯一的workload_instance_id(S2)。啟動時每個 pod 呼叫Ledger.AcquireFencingLease(S4);Ledger SP 用FOR UPDATE序列化,並把 lease 只授予其中一個 pod。其餘 pod 在啟動時 fail-closed,帶S4: acquire fencing lease at startup,停在 CrashLoopBackOff 或 Pending。 - 這不是水平擴展。任一時刻,每個 fencing scope 只有一個 active、負責決策服務的 sidecar。
- 那為什麼要用 DaemonSet? 為了共置:每個節點都有一個同節點上 app pod 能連到的 UDS socket。fencing scope 是 per-tenant(或 per-tenant×region);只有一個節點的 sidecar 持有它。
- Helm gate:必須設
sidecar.acknowledgeMultiPod=true,以明確表達 operator 已知曉 active/standby 語意。啟用 multi-pod 時,workloadInstanceIdOverride不可設定(override 代表單一 pod 身分)。
Failover 與 takeover
Section titled “Failover 與 takeover”Sidecar fencing takeover
Section titled “Sidecar fencing takeover”當 active 的 sidecar pod 掛掉(OOM、eviction、節點故障):
- 該 pod 的
AcquireFencingLeaselease 在SPENDGUARD_SIDECAR_FENCING_TTL_SECONDS(預設 30s)之後逾時。 - 其他節點上、啟動時就 crash 的 standby sidecar 會由 kubelet 重啟。重啟後它們會再次呼叫
Ledger.AcquireFencingLease。 - Ledger SP 看到前一個 lease 已過期,於是授予新 pod 一個
takeoveraction,帶epoch_increment = 1。新 pod 的稽核 row 現在會以fencing_epoch = N+1簽章。 - 舊 pod 任何 in-flight 的決策若試圖以
fencing_epoch = Ncommit,會被 Ledger 的 CAS 檢查擋掉(FENCING_EPOCH_STALEerror)。稽核不變式(「沒有有效 epoch 就不生效」)維持成立。
Operator dashboard 會呈現:
spendguard_sidecar_fencing_epochgauge(per pod)spendguard_sidecar_fencing_acquire_action_total{action}counter(acquire / renew / takeover)— takeover 突增代表發生了 failover。
Outbox-forwarder leader change
Section titled “Outbox-forwarder leader change”coordination_lease_historytable 是稽核日誌:每次 takeover 都會寫一筆 row,帶event_type = 'taken_over'與transition_count + 1。- Operator 監看
spendguard_outbox_forwarder_leader_age_secondshistogram 與coordination_lease_history的 row。
回退到單一 pod
Section titled “回退到單一 pod”三個服務的回退都只是:
sidecar: acknowledgeMultiPod: false # if you set itoutboxForwarder: replicas: 1ttlSweeper: replicas: 1不需要動 DB。lease/fencing 狀態存在 Postgres,由當下還活著的那個 pod 來 renew / take over。
Chaos drill 檢查清單
Section titled “Chaos drill 檢查清單”S5 的驗收標準要求一個「kind test:兩個 sidecar、兩個 forwarder、兩個 sweeper,全部 healthy」。在那個自動化測試落地之前(延後到 S5-followup),operator 應手動驗證:
- 以
outboxForwarder.replicas=2、ttlSweeper.replicas=2、sidecar DaemonSet 部署在 2-node cluster。 - 確認
coordination_leases顯示每個lease_name(outbox-forwarder、ttl-sweeper)正好一個 leader。 - 確認
fencing_scopes顯示每個 scope 正好一個current_holder_instance_id。 kubectl delete pod <leader>。等ttlMs + grace(預設約 30s)。- 確認
coordination_lease_history多了一筆taken_overrow。 - 確認 ledger / canonical-ingest 沒看到重複的稽核 row(
audit_outbox_global_keys對(tenant, workload_instance_id, producer_sequence)的 UNIQUE 會擋掉重複)。 - 對 sidecar 重複一次:
kubectl delete pod <active-sidecar>— 另一節點上的 standby sidecar 以 epoch+1 接手。
Observability 不變式
Section titled “Observability 不變式”每個具備 S1+S4 意識的部署都應對以下情況告警:
coordination_lease_history中event_type='taken_over'的 row,每小時每個 lease 超過 1 次 — 很可能是 lease-flap(TTL 太短或網路分區)。fencing_scope_events中action='promote'每小時超過 1 次 — sidecar takeover storm。- sidecar pod 處於
CrashLoopBackOff、log 內帶acquire fencing lease at startup超過 5 分鐘 — 通常代表 seeded 的 scope row 不見了,或 workload identity 撞號。
已知限制 (S5-followup)
Section titled “已知限制 (S5-followup)”- Per-pod fencing scope 尚未支援。所有節點上的所有 sidecar pod 共用設定好的
sidecar.fencingScopeId。真正的水平擴展需要 per-pod 的 scope 指派;以 S5-followup 追蹤。 - 上述 chaos drill 的 自動化 kind test 延後。
- takeover 期間的 sidecar pre-stop drain 已就位(S4),但 takeover SP 目前還不會撤銷前持有者的 lease — 它只是讓 TTL 自然過期。要更快 takeover 會需要一個明確的 revoke RPC。