iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0

! 本篇文章將會介紹 異常自癒:self-healing 的第一步,期望大家都能讓系統學會自己擦屁股,而不是每次都把人類搖醒 :D

昨天(D25)翻完黑盒子,還原了前天 d24 的 14 分鐘事故:20:09 的發文 job 和 20:15 的補發 job 依序醒來、依序失敗、依序告警,最後是人類被 Discord 叫醒、補好文章、手動重跑才收工。系統的劇本演得很忠實,但「擦屁股」這件事,兩次機會都沒擦動,球一路滾到人類手上。

這幾天的主線一直在鋪同一件事:D12 的錯誤處理、D23 的備援、D25 的稽核,止步於「防禦」和「告訴人類」。今天往前推一步——self-healing:讓系統不只會擋和叫,還會自己動手修。好消息是,盤點之後你會發現這套系統早就偷偷在自癒了,今天只是把這個本能變成有意識的設計。

本篇目標

讀完這篇你會學到:

  • self-healing 和重試、備援差在哪:偵測 → 隔離 → 修復 → 驗證 的閉環
  • 盤點這套系統現有的三層自癒基因(產物層、環節層、排程層),各能修什麼
  • 自癒的三個前提:冪等、留痕、修不了要上拋——以及 d24 事故教我們的「修復動作要對準病灶」

環境準備

不用裝任何新東西,今天全程盤點既有程式碼:

$ cd /Users/benben/ai/automations/ironman
$ ls state/quarantine-*.md
state/quarantine-d04-190404.md   # d04 被隔離的文章,共 1 篇
state/quarantine-d09-190431.md   # d09 以下五篇,時間戳從 19:04 排到 19:25
state/quarantine-d09-190934.md
state/quarantine-d09-191525.md
state/quarantine-d09-191922.md
state/quarantine-d09-192526.md

這六個檔案是今天最重要的證物,先記著。本文引用的程式碼除了特別標注「概念示意」的那段,都是從 scripts/ 裡原封不動搬出來的。

主要內容

步驟一:自癒不是重試——四步閉環才叫 self-healing

先講清楚定義。這套系統到目前為止有兩種「失敗後的動作」:

  • 重試(retry):同一個動作再打一次,賭的是「剛才只是暫時的」。generate.sh 生成失敗重試五次、publish.sh 點發佈重試三次,都是這一類
  • 備援(fallback):正路確定死了,換一條路。備稿、CodeMirror API 失敗改用 keyboard type,都是這一類

這兩個都很好,但有個共同盲點:不知道自己修好了沒,也不問「壞掉的東西去哪了」。self-healing 多出來的,是一個完整閉環:

偵測(detect)→ 隔離(isolate)→ 修復(repair)→ 驗證(verify)

偵測:先有機器能判斷的「健康定義」(品質判準、機敏掃描、發佈 flag);隔離:把壞產物移出正路,而不是讓它混進下一關;修復:重做或換料;驗證:修完要再檢查一次,過了才叫修好。四步缺一不可——少了隔離,壞文章會一路流到 iThelp 上;少了驗證,你只是把失敗往下游推遲一步。

步驟二:盤點系統裡已經在跑的三層自癒

產物層:generate.sh 的機敏掃描守衛,其實就是一個完整的自癒閉環:

# generate.sh:掃描命中 → 隔離 → 重生成(四步全在這幾行裡)
if secret_scan "$ARTICLE"; then
  QUAR="$ROOT/state/quarantine-d${DD}-$(date +%H%M%S).md"
  mv "$ARTICLE" "$QUAR"          # 隔離:壞產物移出正路,但留著當證據
  log "機敏掃描命中!已隔離至 ${QUAR},重試生成"
  continue                        # 修復:整個生成重來(不是修補,是換新的)
fi

開頭那六個隔離檔就是這段程式碼的戰績。最精采的是 d09:五篇文章接連被自動隔離、自動重寫,第五版過關,當天照常發佈。從頭到尾沒有人類參與,讀者完全不知道背後炸了五次。這就是 self-healing 該有的樣子:修復過程對外界透明,證據對自己透明。

小小小測驗:你知道 d09 那天,從第一篇被隔離到第五版過關,系統自己撐了多久嗎?(答案:21 分鐘。隔離檔的時間戳就是自癒的呼吸紀錄——19:04 到 19:25,五輪循環,全程無人值守)

環節層:publish.sh 的降級與驗證。填內文走 CodeMirror API,失敗就降級成 keyboard type——同一個目標換一招修復動作:

# publish.sh:主要手段失敗 → 降級模式(修復動作換一招)
if ! "${AB[@]}" eval "(function(){ /* cm.CodeMirror.setValue(...) */ })()" >>"$LOG" 2>&1; then
  log "CodeMirror API 失敗,fallback: keyboard type"
  ...
fi

發佈點擊也有驗證:按下去不是就算數,而是輪詢 URL 直到長出 /articles/10xxxx 才宣告成功——這就是閉環的最後一步。

排程層:D23 裝的 20:15 republish job。重看它會發現,這其實就是排程層的自癒:

# publish.sh 開頭的冪等守衛,同時就是 republish job 的健康檢查
PUB_FLAG="$ROOT/state/published-$(date +%Y%m%d).flag"
if [[ -e "$PUB_FLAG" ]]; then
  log "今日已發佈過(flag 存在),跳過"   # 健康 → 什麼都不做,睡回去
  exit 0
fi
# 走到這裡 = 偵測到異常(20:00 沒發成)→ 後面整條發文流程就是修復動作

設計漂亮的地方在於:健康檢查不用另外寫,冪等守衛本身就是。flag 在 = 健康,跳過;flag 不在 = 異常,重跑整支 publish.sh,它內建的重試、降級、告警全是修復手段。republish 幾乎每天都在睡,但睡著本身就是「系統健康」的證明。

步驟三:第一步的邊界——修復動作要對準病灶

那前天 d24 為什麼沒自癒成?回頭看很清楚:病灶在生成層(根本沒有文章檔),但兩次修復嘗試都發生在發文層。20:09 的 publish.sh 和 20:15 的 republish 只能修「發文環節」的病——瀏覽器炸了、按鈕沒反應、tag 沒掛上——對「沒料」這種病完全無效。它們忠實地失敗了兩次,然後正確地上拋告警,已經是能力範圍內最好的表現。

這帶出自癒漸進做法的核心紀律:動手修之前,先問這個修復動作治的是哪一層的病;治不了的,就設計乾淨的上拋。與其幻想一個萬能自癒(不存在),不如每層各自的修復能力,加上明確的升級路徑:本層能修的修,修不了的 die() 加 Discord error,把球乾淨地丟給上一層——目前是人類。

照這個模式,生成層的缺口就很好補。把 republish 的「偵測 + 重跑」複製一份過來(概念示意,這是我接下來要加的 job):

# 概念示意:生成層的自癒 job
# 19:30 檢查——generate.sh 最壞 27 分鐘收尾(5 次重試 + backoff),時間剛好錯開
ARTICLE="$ROOT/articles/$(date +%Y-%m-%d)-d$(printf '%02d' $DAY).md"
if [[ ! -s "$ARTICLE" ]]; then
  # 偵測到異常:19:00 的生成沒出貨 → 重跑 generate.sh
  # 它是冪等的(文章已存在就跳過),內建重試 + 備稿 + 告警,重跑安全
  "$ROOT/scripts/generate.sh"
fi

如果 d24 那天有這個 job,19:30 就會發現文章難產、自動重跑生成(或啟用備稿),20:00 的發文 job 醒來時有料可發,根本輪不到後來的 14 分鐘驚魂。一層小手術補掉病根——這就是 self-healing 第一步的具體長相:不急著造神經系統,先讓每一層把自己那層的常見病學會自己看診。(細節如「別跟還沒跑完的實例打架」的鎖,留到明天談邊界設計時一併處理。)

常見問題 / 踩坑記錄

  • Q:既然要自癒,重跑腳本不會把文章發兩次嗎?
    A:會——如果沒有冪等守衛。這就是自癒的第一前提:generate.sh 開頭檢查文章已存在就 exit 0,publish.sh 開頭檢查 flag 已存在就跳過。冪等讓「重跑」永遠安全,自癒才有資格談「自動重跑」。順序不能顛倒:先冪等,後自癒,不然 self-healing 會變成 self-harming。

  • Q:自癒會不會陷入無限重試,把系統越修越爛?
    A:靠三道煞車:重試有上限(MAX_ATTEMPTS=5)、每次之間有 backoff(sleep 30)、上限到了絕不硬拗,該告警就告警。還有一個容易忽略的設計:generate.sh 在「文章存在但品質不足」時保留現狀、不讓備稿覆蓋、只發 warn 叫人來看(D23 的教訓)。自癒動作太自信,有時比不自癒更危險;修不了的病,老實承認也是自癒的一部分。

  • Q:系統默默自癒成功,我怎麼知道它做過什麼?
    A:靠 D25 的黑盒子——自癒必須留痕。隔離區檔名的時間戳讓 d09 五連擋一目了然;published.log 一行一事實;流水帳裡每個判準未過的理由都逐項記下(「不能只說失敗」是 d02 的教訓)。默默修好其實是最大的雷:你看不到它修過什麼,就預測不了它下次會修壞什麼。

小結

  • self-healing 是四步閉環:偵測 → 隔離 → 修復 → 驗證;和重試、備援的差別在「先隔離壞產物、修完要驗證」
  • 系統已有三層自癒基因:產物層的隔離重生成(d09 五連擋實錄)、環節層的降級模式與 URL 驗證、排程層的 republish 重跑
  • 自癒三前提 + 一紀律:冪等(重跑安全)、留痕(可稽核)、修不了上拋(不硬拗);修復動作要對準病灶所在的那一層——d24 用 14 分鐘教會我們的事

明日預告

下一篇我們要介紹 安全煞車:全自動系統的邊界設計,敬請期待!自癒讓系統開始「自己動手」,但會自己動手的系統更需要煞車——哪些 domain 可以碰、哪些動作需要核可、人類的搶救通道長什麼樣,明天把界線畫清楚。

參考資料:

有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D


上一篇
25 日誌與稽核:自動化系統的黑盒子
系列文
自我耍廢組:全自動化の鐵人 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言