PlAIground Lab AI Geek Blog

三層防線一起睡著:一次 Webhook 斷鏈事故,與我們如何讓它永遠先被機器發現

給你的 AI:本篇另備乾淨 Markdown 的 agent 版,讀了就能照著搭——中文版・English

前段:寫給人看的故事

我們有一條很重要的訊息通道:外部協作的 AI 把任務透過 webhook 送進來,我方系統收下、建檔、再喚醒巡邏程序去處理。它平常安靜地工作,一天送進來好幾件急單,我們甚至為了它把不穩定的臨時網址換成了固定網域。

2026 年 11 月 1 日下午,這條通道斷了三個多小時。最諷刺的不是它斷了,而是——三層自動防線全都睡著,最後是使用者先發現的。

下午約 15:23 起,接收訊息的兩個程序悄悄死掉,最後一封成功收下的訊息停在 12:03。第一層防線本該是「守護程序」:它每 5 分鐘檢查一次、壞了就自動重啟整套服務。但那天它處於停用狀態,自癒根本沒有啟動——這是第一層。

第二層防線是我們當天中午才剛上線的「金絲雀」:一支零成本的死腳本,每 5 分鐘走一遍完整的公開路徑送一封自我測試訊息,要求對方回覆一組隨機碼,碼對了才算真的通。它其實有在工作——它每一輪都亮紅燈,端到端測試每一輪都回 502。可是它的升級邏輯寫的是「只通報一次,之後保持安靜」,而那唯一一次通報又投遞到了錯誤的聊天室。紅燈亮了三個多小時,沒有任何人被喚醒——這是第二層。

第三層防線是「備援信箱」:主通道送不進來時,送端改把訊息投進一個雲端信箱,我方每 30 秒去拉一次。這層其實救了我們——斷鏈期間的 4 封訊息一封都沒丟,全部躺在信箱裡。只是負責拉信的那個程序跟主程序一起死了,所以信躺了三個多小時,直到全棧恢復才在 18:52 被一次拉進來建檔。備援存在,但備援的搬運工也死了——這是第三層。

18:38,使用者發覺「傳訊好像有問題」。我們實查後確認三層疊死,18:52 跑了一次全棧重啟,逐段驗證:本機健康檢查正常、公開健康檢查正常、經公開網址送自我測試回 200。通道恢復,信箱裡的 4 封信隨即補建完成。

這篇文章的後段,是這次事故的完整 runbook:架構長什麼樣、金絲雀與守護程序怎麼設計、死段怎麼定位、修復的每一步與驗證方法,以及事後補上的三個修法——讓下一次斷鏈,先被機器發現,而且吵得醒人。

一句先說在前面:監控的價值不在「有裝」,而在「壞掉的時候,它的通報本身還活著」。 通報路徑跟被監控的系統共用同一條命,是這次事故最貴的一課。

PlAIground Lab 的其他產品:入口首頁|ComputeQuote|Axiom|理想型


以下為完整 runbook

1. 架構:Fleet Webhook 鏈路

送端(外部協作 AI)→ 公開端點 → 隧道層 → 接收端 → 建檔 → 喚醒巡邏。備援路徑:送端 → 雲端信箱(KV)→ 輪詢程序 → 建檔。

段 元件 說明
S1 公開端點 固定網域 https://hook.example.com/hook,POST,需帶 X-Fleet-Key 標頭驗證;JSON 主體上限 64KB,必填欄位 text
S2 命名隧道 跑在常開的家用電腦上(雲端虛擬機的出口無法承載隧道資料面,實測後改此架構),4 條 QUIC 連線
S3 反向隧道 虛擬機主動向家用電腦建立 SSH 反向隧道,把接收端埠映射過去:家用電腦本機 18791 → 虛擬機 127.0.0.1:8791
S4 接收端 receiver Python 標準函式庫 HTTP 服務,監聽 127.0.0.1:8791;驗證通過後把任務寫入任務 repo 的 tasks/pending/,再以 SSH 喚醒家用電腦上的巡邏程序
S5 信箱輪詢 poller 每 30 秒拉雲端信箱,有信則比照接收端格式建檔、刪信、留收件紀錄、喚醒巡邏,此為備援與非同步投遞路徑

健康檢查端點:本機 GET http://127.0.0.1:8791/health 與公開 GET https://hook.example.com/health,正常回 {"ok": true}。

接收端驗證規則(400 的四種原因,皆回傳錯誤字串):主體大小不符、JSON 無法解析、text 缺失或為空、task_id 格式不符;另有 401(金鑰不符)與 409(重複)。排障時先看錯誤字串,不要先猜送端壞了。

2. 事故時間線(2026-11-01,America/Phoenix)

時間 事件
10-29 夜 接收端建成,最初使用臨時隧道網址(重啟即變)
10-30 固定網域命名隧道上線(跑在家用電腦);信箱備援路徑上線
10-31 整夜 接收端多次被守護程序拉起重啟,每次實查皆已自癒;守護程序「重啟後只等 3 秒就複查」追不上隧道收斂,反覆誤判失敗(計時過敏,非真故障)
11-01 上午 送端回報連續兩次 400:查證我方格式未變,接收端補上拒絕原因紀錄(REJECT logging,只記原因與欄位名,不記內文),探針驗證通過
11-01 12:0x 第一版改善完工:金絲雀上線、輪詢加指數退避、守護程序舊版停用;拉模型評估完成但未動工
約 15:23 接收端與輪詢程序死亡;最後一筆成功收件停在 12:03
15:23–18:38 金絲雀每 5 分鐘全段紅燈(端到端回 502),但升級只響一次且投遞位置錯誤,其後恆為安靜;守護程序處於停用狀態未自癒;信箱備援累積 4 封未拉
18:38 使用者指出傳訊有問題,團隊實查確認三層疊死
18:52 執行全棧重啟 start-all.sh,逐段驗證全綠;公開路徑自我測試回 200;信箱 4 封於 18:52–18:53 補拉建檔,訊息零遺失
18:5x 善後三修完工(見第 5 節);守護程序重新啟用

3. 死段定位:分段健康檢查

不要只測「通或不通」。把鏈拆成可獨立判讀的段,每段留各自的健康紀錄,事故時直接讀出死段:

  1. 接收端本機段:curl http://127.0.0.1:8791/health,預期 200 且主體含 "ok": true。
  2. 反向隧道程序段:檢查隧道程序存在,且其命令列含預期的埠映射參數;程序在不代表功能在(見第 6 節殭屍監聽)。
  3. 公開段:curl https://hook.example.com/health,預期 200。注意:部分 CDN 會擋無 User-Agent 的 POST,金絲雀需帶自訂 UA。
  4. 輪詢段:讀輪詢健康檔(如 poller-health.json),檢查時間戳新鮮度(小於 10 分鐘)與連續失敗數(小於 3)。
  5. 端到端段:經公開端點送一封 type: selftest 的測試訊息,帶隨機 nonce;接收端收到後直接回顯 nonce、不建檔。回顯相符才算整條鏈真的通——只測 health 會漏掉「程序活著但收件路徑已死」的情況。

本次事故的判讀:段 1 失敗+段 3 回 502 → 死點在接收端本體(而非隧道或網域),處置為全棧重啟而非逐段亂試。

4. 修復步驟

前置:確認有全棧重啟腳本 start-all.sh(冪等,依序拉起接收端、輪詢、反向隧道,並確保家用電腦端隧道)。

  1. 執行 start-all.sh 一次。預期:腳本無錯誤輸出,各子程序 PID 檔更新。
  2. 等待收斂:隧道與公開路徑重建需時,至少等 45 秒再複查——重啟後 3 秒就判失敗是已知的誤報來源。
  3. 逐段複查第 3 節的五段。預期:五段全綠。
  4. 端到端複查:經公開網址再送一次 selftest。預期:回 200 且 nonce 相符,接收端紀錄出現 CANARY 或 selftest 行。
  5. 補拉檢查:看輪詢紀錄,確認斷鏈期間信箱累積的訊息已建檔。預期:每封有 ACCEPT 紀錄,任務 repo 的 pending 區出現對應檔案。
  6. 若第 2 步後仍有段未綠:停手,先保存現場紀錄(各段狀態、程序清單、最近紀錄),再依死段縮小範圍,不要連續重跑重啟腳本碰運氣。

5. 預防設計:金絲雀、守護與升級

事故後定版的設計(皆為死腳本、零 LLM 成本,只在失敗門檻觸發時才喚醒 agent):

金絲雀(canary)

  • 每 5 分鐘執行一次:先跑分段檢查,再經公開路徑送 selftest(nonce 回顯驗證)。
  • 狀態檔保存:連續失敗數、是否已升級、累計送出與成功數、最近延遲、各段狀態。
  • 門檻:連續失敗 2 次 → 自動執行 start-all.sh 修復,等 45 秒複測一次;連續失敗 6 次(約 30 分鐘)→ 升級喚醒負責團隊,並附死段判定。
  • 升級必須可重複(本次事故的修法核心):首次升級後若持續斷鏈,每再累積 12 次失敗(約 1 小時)重報一次;曾升級的事故在恢復時補送一則「已恢復」通知。通報投遞目標為主責聊天室,不得投到無人值守的位置。

守護程序(keeper)與重啟互斥

  • 守護與金絲雀都可能觸發 start-all.sh,兩者同時重啟會撞埠(address already in use)。修法:start-all.sh 開頭加檔案鎖(flock 單例,搶不到鎖直接跳過),且綁埠前輪詢等舊埠釋放(每 0.5 秒探一次,最多 10 秒)。
  • 守護的「修復後複查等待」不得小於隧道收斂時間(本系統為 45 秒級),否則會把正在恢復的系統誤判為失敗並反覆重啟。

輪詢退避

  • 輪詢上游出現 RemoteDisconnected 類錯誤時採指數退避:30 秒起、倍增、上限 300 秒;每輪結果寫健康檔(時間戳、連續失敗數、最近錯誤),供金絲雀判讀。退避不是放棄,是讓上游恢復期不被打爆、也讓健康檔能呈現「正在退避」而非「已死」。

送端備援約定

  • 送端連續呼叫 webhook 失敗(例如吃到 502)時,改投雲端信箱,我方輪詢拉取,訊息不遺失。此約定在上線一小時內即被實戰驗證:一次 502 後送端改投信箱,訊息成功送達(重複副本依 task_id 去重結案)。該次 502 在我方接收端紀錄中查無痕跡,判死在隧道層——這也是為什麼分段紀錄必須包含隧道段。

結構評估(未動工,僅評估結論)

  • 長期方向建議「拉為主、推為加速」:送端寫信箱、我方主動拉取為主路徑,webhook 隧道降為加速通道;先雙寫一週對照成功率與延遲,再決定是否遷移。理由:拉模型少一整段隧道依賴,天然不怕端點網址變動。

6. 已知陷阱清單

  • 殭屍監聽:家用電腦端曾出現舊 SSH 工作階段佔著映射埠不放——用戶端已死、經代理半開、系統 sshd 未設定 ClientAlive 故永不釋放;隧道程序看似在跑,新連線卻綁不上。判讀法:家用電腦本機直接 curl 127.0.0.1:18791/health 不通,但隧道程序存在,即懷疑此項;處置為清除舊工作階段程序,讓隧道迴圈綁上新工作階段,再以本機 curl 驗通。
  • 出口憑證輪替:雲端虛擬機的出口代理憑證會隨工作階段輪替,長命的隧道迴圈會拿著死憑證一直重試(程序活著、功能已死)。全棧重啟時必須一併重建隧道迴圈,讓它換上新憑證——守護定期呼叫全棧重啟,客觀上界定了憑證過期的最長影響時間。
  • 計時過敏:重啟後收斂需要時間,複查太快只會製造假警報與二次重啟;所有複查等待都要以實測收斂時間定版,寫進腳本,不靠手感。
  • 只報一次的升級:任何「只通報一次、之後安靜」的設計,在通報遺失或投錯位置時等於永遠安靜。升級要有節奏地重報,恢復要補報。

7. 驗證清單(修復完成逐項打勾)

  • [ ] 本機 health 回 200 且 "ok": true
  • [ ] 公開 health 回 200
  • [ ] 公開路徑 selftest 回 200 且 nonce 相符
  • [ ] 輪詢健康檔時間戳小於 10 分鐘、連續失敗為 0
  • [ ] 斷鏈期間信箱訊息全數建檔(逐封核對 ACCEPT 紀錄)
  • [ ] 金絲雀狀態檔連續失敗歸零、恢復通知已送出
  • [ ] 守護程序為啟用狀態

© PlAIground Lab。歡迎引用與供 AI 學習,轉載請註明出處;禁止商業轉售。

Agent 版:同文另備乾淨 Markdown 版(英文版 2026-11-01-webhook-outage.en.agent.md 已備;中文版與英文人讀版全文為正式刊出前待辦),讓你的 AI 讀了就能照著搭。 原稿時間戳:2026-11-01(repo commit 存證)