凌晨三點,手機在枕頭邊震動
那天是禮拜三,準確說是禮拜四的凌晨三點十一分。我記得這麼清楚,是因為後來寫 postmortem 的時候,那個時間戳被我貼進了 timeline 的第一行。
手機亮起來的瞬間,我整個人從淺眠裡彈起來。不是被吵醒的那種,是身體在睡著的狀態下,耳朵還掛著一根弦,PagerDuty 的鈴聲一響,腎上腺素先到,意識後到。我手在黑暗裡摸索,先碰到水杯差點打翻,才抓到那支發燙的手機。螢幕上是一行字:撮合引擎的下單延遲 P99 超過 5 秒,持續觸發三分鐘。
我心臟跳得很快。不是因為這個告警有多罕見,而是因為我知道現在是亞洲時段半夜、歐洲下午、美洲傍晚,這個交易所是 24 小時不打烊的,沒有「半夜沒人交易」這種事。下單延遲五秒,對一個做合約的用戶來說,可能就是爆倉跟沒爆倉的差別。每多一分鐘,就是一筆一筆真實的錢,跟一封一封真實的客訴。
我坐在床邊,腳還沒踩到地,左手已經打開筆電。室友的房間沒聲音,窗外是那種台北凌晨特有的、安靜到有點不真實的暗。我深吸一口氣,告訴自己一句後來變成我口頭禪的話:先止血,再查因。
這篇想老實講講,在一個永遠不睡的系統值班,到底是什麼感覺,它怎麼把我從一個寫完功能就下班的工程師,變成一個半夜會夢到 dashboard 的人;也想講講那些真的有用、跟那些只是讓你更焦慮的事故處理習慣。
帶著呼叫器的那種焦慮,沒人會跟你說
剛輪到我第一次 on-call 的時候,主管很輕鬆地跟我說:「放心,大部分時候不會響。」
這句話有一半是真的。問題是另一半。
值班週最折磨人的,從來不是那些真的響起來的夜晚,而是那些「可能會響」的每一個時刻。你出門吃飯會看一下手機訊號滿不滿格。你洗澡會把音量開到最大、手機放在浴室門口。你跟朋友聚餐喝酒前會猶豫——萬一三點響了,我這個狀態能上線嗎?你躺在床上,明明很累,卻睡得很淺,因為你的身體知道它今晚可能要被叫起來。
我那時候才理解,on-call 真正的成本不是工時,是一種背景焦慮。它像一個一直開著的後台程序,慢慢吃你的記憶體。一個禮拜的值班結束,就算一通都沒響,我也覺得整個人被掏空。
我也犯過一個新人常犯的錯:把所有告警都當成自己的責任,自己的失敗。第一次半夜被叫起來,我處理完之後沒辦法再睡,一直想是不是我哪裡寫錯了、是不是我害的。後來資深的同事跟我說了一句話我一直記得:「告警響不是你的錯,告警響是系統在跟你說話。你的工作是聽懂它,不是替它道歉。」
這個心態的轉變很重要。當你不再把每次告警當成個人審判,你才有辦法冷靜下來,真的去解決問題。
那個很糟的晚上,我做對的跟做錯的
回到凌晨三點那一晚。
我先做的第一件事,不是去翻程式碼。是打開監控,確認「現在到底壞到什麼程度」。延遲 P99 五秒、錯誤率還在低檔、訂單沒有掉、資料庫連線數正常但偏高。這些資訊讓我大致定位:不是整個系統掛掉,是某個環節在塞車。
接著我做了第二件,後來證明最重要的事:我在事故頻道貼了一句話。「我看到撮合延遲告警,正在查,目前判斷是延遲升高不是服務中斷,先同步給大家。」
這句話看起來沒做什麼,但它做了三件事。第一,它讓還醒著的人知道有人在處理,不會有第二個人重複爬起來。第二,它開了一條 timeline,之後每一步我都接著貼。第三,它讓我自己冷靜——把狀況講成一句別人聽得懂的話,這個動作本身會逼你把混亂整理成秩序。
然後我開始止血。我看到那台撮合服務的某個下游——一個負責寫入成交紀錄的 worker——堆積了大量的 queue,而它寫的那張資料表,剛好在幾小時前被另一個團隊跑了一個大型的歷史資料遷移,鎖住了。連鎖反應:遷移鎖表,worker 寫不進去開始堆積,撮合主流程等 worker 的回壓,下單延遲就上去了。
我做對的地方是:我沒有第一時間去研究那個遷移為什麼會鎖表。那是 root cause,但那不是現在該做的事。現在該做的是止血。我聯絡了值班的 DBA,請他先把那個遷移任務暫停,鎖一放開,queue 在大概九十秒內消化完,延遲掉回正常。從告警到緩解,前後大概二十五分鐘。
我做錯的地方是:我太想自己解決了。中間有大概十分鐘,我其實已經懷疑是鎖的問題,但我不確定,又不想半夜吵醒 DBA、怕自己判斷錯了很丟臉。這十分鐘是我自己的焦慮浪費掉的。後來我學到,事故當下,「叫醒對的人」永遠比「自己硬撐」便宜。把人叫起來最多就是不好意思,把事情拖下去是真的賠錢。
怎麼在事故當下保持冷靜:止血、溝通、再查因
事故處理最反直覺的一點是:你最想做的事,通常是最不該先做的事。
工程師的本能是「找出原因」。但在事故當下,找原因跟止血常常是衝突的。你花二十分鐘 root-cause,用戶就多痛二十分鐘。所以我自己內化了一個順序,這個順序救過我很多次:
第一,止血(stop the bleeding)。先讓用戶不痛,手段可以很髒。重啟、回滾、切流量、關掉某個功能、暫停某個任務——這些都不優雅,但它們能讓系統先活下來。優雅留給明天。
第二,溝通(communicate)。在事故頻道持續更新。不是等你解決了才說,是每一步都說。「我在看 X」、「我懷疑是 Y」、「我要做 Z 了」。溝通的對象不只是你的隊友,還有客服、主管、有時候是用戶。一個正在處理中、有人定時更新的事故,跟一個沉默的黑洞,給人的感受天差地遠。
第三,再查因(root-cause)。等血止住了、用戶不痛了,你才慢慢回去把真正的原因挖出來,補上根本的修復。這時候你不慌了,可以做得仔細。
這個順序背後其實是一個價值排序:用戶的體驗 > 你的好奇心。聽起來簡單,但事故當下腎上腺素飆高的時候,要壓住「我一定要當場搞懂」的衝動,其實非常難。
真正有用的事故處理習慣
值了一兩年班,踩過夠多坑之後,我們團隊慢慢長出一套習慣。不是什麼大公司的華麗框架,就是一些被痛教出來的土法煉鋼,但很有用。
明確的嚴重度分級
我們把事故分等級。最高級是「有用戶的錢在真實流失或交易完全停擺」,這種要立刻把所有人叫起來;中間級是「功能降級但核心可用」;最低級是「內部工具壞了、不影響交易」。
分級的重點不是貼標籤,是它決定了你的反應強度。一個 P3 的告警你不需要半夜叫醒五個人;一個 P1 你不該自己一個人硬扛。早期我們最大的問題就是分級模糊,結果不是反應過度、就是反應不足。把標準寫清楚之後,半夜的決策成本低很多——你不用在三點鐘的腦霧裡判斷「這到底嚴不嚴重」,分級表會幫你。
一個事故指揮官
這是我覺得最被低估的一條。當一個大事故發生,三五個人同時撲上來,看起來人多力量大,實際上是災難。每個人在改不同的東西,沒人知道別人在幹嘛,互相覆蓋,越救越亂。
我們的做法是:每個事故,指定一個 incident commander(事故指揮官)。這個人不一定是技術最強的,他的工作不是親自下去修,而是統籌——誰去查資料庫、誰去看網路、誰負責對外溝通、現在要不要回滾。他握著全局。其他人有發現就回報給他,由他決定下一步。
聽起來很官僚,但在混亂裡,一個清楚的決策中心,比五個各自為政的天才有用太多。
一條持續更新的 timeline
事故當下就開始記時間軸。幾點幾分發生什麼、誰做了什麼、結果如何。這件事在當下看起來是負擔,但它有兩個巨大的價值。一是它逼你把行動講清楚,避免你在慌亂中亂改一通。二是它是 postmortem 最寶貴的素材——事後你絕對記不清當下的順序,但 timeline 記得。
我現在的習慣是,事故一開始就在頻道貼,後面所有訊息都接著那串。等於免費生出一份時間軸。
無咎責的檢討
這個下一段詳細講,但它是整套習慣的地基。沒有它,前面所有東西都會慢慢腐爛。
一份好的 postmortem 長什麼樣,跟一場抓戰犯有什麼不同
我參加過兩種完全不同的事後檢討會。
第一種,我叫它「抓戰犯大會」。會議一開始,主管的第一句話是「這次是誰改的?」整個房間的空氣瞬間凝固。那個改了 code 的同事臉色發白,開始解釋、開始防衛。接下來一個小時,大家不是在討論系統哪裡脆弱,而是在討論「這件事該怪誰」。會議結束,得到一個結論:以後改這塊要更小心。然後呢?沒有然後。三個月後同類型的事故又發生了,只是換了一個人當代罪羔羊。
第二種,是真正的 blameless postmortem。
它的核心信念是:人會犯錯,這是常態,不是意外。如果一個人的一次操作就能搞垮整個系統,那問題不在這個人,在系統居然允許這種操作這麼容易就造成災難。我們檢討的對象是系統跟流程,不是個人。
一份好的 postmortem,我自己看重幾個東西。
它有一條客觀的 timeline,純事實,沒有形容詞,沒有「應該」、「居然」這種帶情緒的字眼。它清楚寫出影響範圍:多少用戶受影響、持續多久、損失多少。它誠實地分析根本原因,而且會追問到夠深——不是停在「因為 worker 寫不進去」,而是繼續問「為什麼遷移可以在交易時段鎖一張熱表」、「為什麼我們的監控沒有更早發現 queue 堆積」、「為什麼這個 worker 沒有回壓保護」。每一層 why,都指向一個可以改的系統缺陷。
最後,它產出的是具體、有人認領、有期限的行動項,而不是「以後要更小心」這種沒有人能執行的空話。
我記得有一次 postmortem,那個犯了操作失誤的同事,在會議上很坦然地把自己怎麼想、怎麼一步步走到那個錯誤的,全部講出來。沒有人指責他,大家反而很感謝他,因為他的誠實讓我們發現了三個之前沒人注意到的系統盲點。那一刻我真的相信了 blameless 的力量——只有當人不怕被懲罰,他才敢說真話;只有人說真話,你才看得到系統真正的破洞。
抓戰犯的代價是,下次出事的時候,大家第一反應是遮掩、是甩鍋、是把問題藏起來。而藏起來的問題,會在最糟的時間用最糟的方式爆出來。
On-call 怎麼改變了我寫程式的方式
值班這件事,最深遠的影響其實不在事故當下,而在我平常寫 code 的時候。
以前我寫完一個功能,測試過、能動,就覺得結束了。現在我寫 code 的時候,腦子裡會多一個人——那個半夜被我這段 code 叫起來的人。很多時候,那個人就是未來的我自己。
這個視角改變了非常多細節。
我開始認真寫 log。以前 log 是給自己 debug 用的,隨便印印。現在我會想:如果這段邏輯半夜出錯,值班的人看到這行 log,能不能立刻知道發生什麼、該往哪查?我會把關鍵的 ID、狀態、為什麼走到這個分支,都記清楚。一行好的錯誤 log,可能幫值班的人省下半小時的盲找。
我開始在乎告警的品質。會吵醒人的告警,必須是真的需要人介入的。如果一個告警三點響了,結果是「喔這個自己會好」,那這個告警就是在謀殺值班者的睡眠跟信任。告警太多、太吵、太多假警報,最後的結果是大家對告警麻木,真正重要的那一個反而被忽略。我寧可少設幾個告警,但每一個響起來都值得爬起來。
我開始給系統留逃生口。可以開關的 feature flag、可以暫停的任務、可以降級的服務、清楚的回滾路徑。因為我知道,事故當下你最需要的不是完美的修復,是一個能讓你先止血的開關。我寫的時候多花十分鐘做這個開關,可能幫未來的某個人在三點鐘省下兩小時的痛苦。
我也更尊重「無聊但穩」的設計。年輕的時候喜歡炫技、喜歡用最新的東西。值過班之後,我越來越欣賞那些無聊、可預測、半夜不會給你驚喜的系統。在凌晨三點,無聊是一種美德。
說到底,on-call 把「寫程式」這件事,從一個交付當下的動作,變成了一個對未來負責的承諾。你寫的每一行,都可能在某個你睡得正熟的夜晚,決定另一個人——或者未來的你——是好好睡到天亮,還是從床上彈起來。
關於 burnout 跟界線,我得老實說
我不想把 on-call 講得太英雄主義,好像半夜爬起來救火很帥。它不帥,它很累,而且它真的會把人燒壞。
我有一段時間狀況很差。連續幾個值班週都不平靜,加上白天還有正常的開發工作,我的睡眠變得支離破碎,脾氣變差,對告警產生一種生理性的厭惡——聽到類似 PagerDuty 的鈴聲,就算不是我的手機,心臟也會揪一下。這是 alert fatigue,也是 burnout 的前兆。
後來我學到幾件關於界線的事。
值班結束就是結束,不要當那個明明不是你的班、卻還一直盯著 dashboard 的人。系統是團隊的,不是你一個人的。你需要真正的休息,而真正的休息是把責任暫時、完整地交給下一個人。
要敢於把問題講出來。如果你的值班一直不平靜,那不是你抗壓性不夠,那是系統有問題、或者人力配置有問題。一個健康的團隊,會把「on-call 太累」當成一個需要解決的工程問題——減少假告警、自動化常見的修復、輪班更合理——而不是要求個人忍耐。我後來會在 retro 直接講:這個告警這個月吵醒我三次,我們得修它。把痛說出來,才有機會改變。
也要對自己誠實。如果一個團隊長期把 on-call 的代價全部轉嫁給個人,從來不投資在讓系統更穩、告警更準上面,那這是一個值得重新評估的環境。承擔責任是一回事,被慢慢燒乾是另一回事。
寫在最後
現在回頭看那個凌晨三點,我對它的感覺很複雜。
那一晚當下,它只是焦慮、是腎上腺素、是「拜託快點好」的祈禱。但拉長時間看,那些半夜爬起來的夜晚,那些對著螢幕逼自己冷靜的二十五分鐘,那些一份份寫得很痛但很誠實的 postmortem,其實是我這幾年成長最快的養分。它教會我的,遠不只是怎麼處理一個壞掉的系統,它教我怎麼在壓力下保持清醒、怎麼跟人協作、怎麼為看不見的未來負責,也教我認得自己的極限在哪裡。
我現在依然會值班,依然會在某些夜晚被叫醒,依然會心臟揪一下。但我不再把告警當成審判,也不再覺得自己得一個人扛下整個系統。我會先止血,會把狀況講清楚,會在對的時候叫醒對的人,會在事後誠實地檢討而不是找人怪罪。然後,我會在下一次寫 code 的時候,多想一秒那個三點鐘可能被叫起來的人——很可能,就是還在熟睡的、未來的我自己。