Day 14,我們替服務加入 Health Check、Circuit Breaker 與容量上限。
這些機制可以處理流量與外部依賴故障,卻無法挽回已經被刪除或破壞的資料。
假設部署新版本後,Migration 把所有使用者名稱改成新欄位。工程師發現錯誤後立刻回滾程式,但舊版程式已經讀不到新格式。
程式回滾成功,服務仍然故障。
今天會用本機 Demo 驗證三件事:
團隊常用一句話描述資料保護:
我們每天都有備份。
這句話沒有回答:
只有成功執行過 Restore,才能證明復原流程至少在測試條件下可用。
Backup 是一份資料副本。Recovery 則包含找到正確版本、取得權限、還原資料、驗證完整性、切換流量與處理事故後續。
Disaster Recovery 通常先定義兩個目標:
假設系統每天凌晨建立一次 Backup。
如果資料庫在晚上 11 點損壞,最後一份 Backup 可能來自 23 小時前。這個策略的 RPO 最差接近 24 小時。
如果團隊需要 6 小時才能找到 Backup、取得權限並完成 Restore,實際 RTO 就不可能是 30 分鐘。
RPO 與 RTO 必須根據使用者與業務影響設定,不能只寫「愈快愈好」。
| 系統 | RPO 範例 | RTO 範例 |
|---|---|---|
| 個人文章草稿 | 24 小時 | 8 小時 |
| 一般專案設定 | 1 小時 | 2 小時 |
| 付款與訂單 | 近乎零資料損失 | 15 分鐘 |
這些數字只用來說明差異,不是所有產品都應採用的標準。RPO 與 RTO 愈小,通常需要更高成本與更複雜的架構。
今天的程式放在:
demo-app/recovery-demo/scenario.js
腳本會建立一個暫存的 JSON Database,內容包含兩位使用者:
[
{
id: "user-1",
name: "Mickey",
email: "mickey@example.test"
},
{
id: "user-2",
name: "Builder",
email: "builder@example.test"
}
]
所有 Email 都是假資料。腳本完成後會刪除暫存目錄。
進入 Demo:
cd /media/mickey/777/ithome/demo-app
執行:
npm run recovery:demo
Demo 先把目前資料複製到 Backup,再計算 SHA-256 Checksum:
await copyFile(activePath, backupPath);
const backup = await readFile(backupPath, "utf8");
const backupChecksum = checksum(backup);
實際結果:
Backup records: 2
Backup checksum prefix: 44c9f10178ab
Checksum 可以協助確認 Restore 後的檔案是否與 Backup 相同。
但 Checksum 相同只證明位元內容一致。它不能證明:
因此,驗證流程還需要解析資料並檢查業務條件。
Demo 接著把目前資料清空:
await writeFile(activePath, "[]\n");
結果為:
Records after corruption: 0
接著從 Backup Restore:
await copyFile(backupPath, activePath);
實際結果:
Restore checksum matches: true
Records restored: 2
Demo restore elapsed: 1ms
這次 Restore 只處理兩筆本機 JSON,因此 1 毫秒沒有 Production 參考價值。
正式演練要使用接近真實規模的資料、網路、權限與 Index。否則團隊測到的只是複製小檔案所需時間,不是實際 RTO。
如果程式在星期一開始慢慢破壞資料,團隊到星期五才發現,最近幾份 Backup 可能都包含錯誤。
Backup 策略還需要考慮:
只有最新一份 Backup,無法處理延遲發現的資料損壞。
接著模擬欄位重新命名:
name → displayName
新版本程式讀取:
user.displayName
舊版本程式讀取:
user.name
破壞式 Migration 直接移除 name:
const destructiveMigration = users.map(({ name, ...user }) => ({
...user,
displayName: name
}));
新版本可以正常讀取:
New application reads: ["Mickey","Builder"]
但回滾到舊版後:
Old application after rollback: ["<missing-name>","<missing-name>"]
程式碼雖然回到舊 Commit,資料卻沒有恢復舊 Schema。
這就是不可相容 Migration 的風險。
正式部署通常不會在同一瞬間完成。
Rolling Deployment 期間可能同時存在:
Old Application
New Application
Migration Worker
如果 Migration 先移除舊欄位,仍在接收流量的舊版會立即失敗。
如果新程式先假設所有資料都有新欄位,尚未 Backfill 的舊資料也會失敗。
Migration 必須考慮兩個方向:
部署完成不代表舊版已完全停止。Queue Worker、排程工作與尚未終止的 Instance 都可能繼續執行舊程式。
較安全的流程會分成多個階段。
第一階段:Expand
新增 displayName,但保留 name:
{
id: "user-1",
name: "Mickey",
displayName: "Mickey"
}
新程式先支援兩種欄位:
const displayName =
user.displayName ?? user.name;
舊程式仍能使用 name。
第二階段:Migrate
新寫入同時維護兩個欄位,並在背景 Backfill 舊資料:
{
name: input.name,
displayName: input.name
}
團隊要監控:
第三階段:Contract
只有在以下條件成立後,才能移除舊欄位:
Demo 的 Expand Migration 保留兩個欄位。
實際結果:
New application reads: ["Mickey","Builder"]
Old application after rollback: ["Mickey","Builder"]
新舊程式都能讀取資料。
但系統仍有 12% 舊版流量,因此 Contract 遭到阻止:
Contract phase: BLOCKED old_version_traffic=12%
這個 Guard 可以避免團隊太早刪除 name。
一次更新所有文件,可能造成:
Migration Worker 應具備:
Migration 再次執行時,不應重複破壞已完成資料。
團隊也要先定義停止條件。例如 Error Rate 超過 1%,或 Database Latency 超過門檻時,自動暫停 Migration。
Cloud Firestore 提供 Scheduled Backups。官方文件指出:
Firestore 也提供 Managed Export 與 Import。
Managed Export 可以匯出全部文件或指定 Collection Group,再把資料匯入 Database。官方文件也提醒,Export 不是開始執行當下的精確 Snapshot;執行期間發生的變更可能被包含。
Scheduled Backup 與 Managed Export 的語意不同。團隊應根據 RPO、RTO、成本與還原流程選擇,而不是把所有資料複製方式都稱為相同的 Backup。
只有 Database Data,通常不足以還原完整服務。
團隊還要保存:
這些設定應存放在受控 Repository 或 Infrastructure Management System。不要假設 Database Backup 會自動包含所有平台設定。
Backup 包含正式資料,因此也是攻擊目標。
控制措施包含:
工程師不應為了方便,讓所有人都具備 Restore Admin 或 Backup Admin。
一次完整演練至少要記錄:
Backup time
Incident detection time
Restore start time
Restore complete time
Traffic switch time
Oldest restored record
Record count
Checksum or business invariant
Missing configuration
Manual steps
演練結束後,計算:
Actual RPO = Incident time - Restored data time
Actual RTO = Incident start - Service restored time
如果 Actual RPO 或 RTO 超過目標,團隊要改善 Backup 頻率、Restore Automation、權限取得或架構。
不要只記錄「Restore 成功」。真正的問題是服務能否在目標時間內,以正確資料重新提供功能。
Agent 應檢查:
一項可操作的 Finding 可以寫成:
問題:Migration 移除 name 後,舊版程式無法回滾
證據:Migration 只保存 displayName,舊版仍讀取 user.name
觸發條件:新版本部署後需要回滾程式
影響:所有使用者名稱顯示為缺失
驗證:npm run recovery:demo
結果:舊版輸出兩筆 <missing-name>
修正:先保留兩個欄位,完成 Backfill 並等待舊版流量歸零
Repository 沒有 Backup 設定,不代表正式環境一定沒有 Backup。設定可能存在 Google Cloud Console 或另一個 Infrastructure Repository。
Agent 應要求 Backup Schedule、Restore Record 與 IAM 證據,不能只根據單一 Repository 下結論。
Backup 只有在成功 Restore 後,才開始具有可信度。
程式回滾也不等於資料回滾。破壞式 Migration 可能讓舊版程式失去讀取能力。
今天的實驗得到三個結果:
name。明天,我們會拆解 Cloudflare Security Audit Skill 的六階段流程,看看它如何把 Recon、Hunt、Validation 與 Reporting 串成完整的 AI 稽核工作。