一行忘了關的 debug log,一夜之間把磁碟寫爆
Leo Wu,後端工程師,主力 Go,做過金流與高併發系統,被自己忘了關的一行 debug log 教訓過一次。
凌晨三點的連環告警
那天凌晨三點十七分,我的手機開始震個不停。不是一則告警,是一整排:API 5xx 飆到 40%、下單服務健康檢查失敗、資料庫連線逾時、對帳批次 job 掛掉。Slack 的 on-call 頻道在幾分鐘內被紅色圖示塞滿。
我第一個念頭跟大部分人一樣:完了,資料庫掛了。因為錯誤訊息看起來就是那個方向——could not write to file、transaction aborted、connection reset。金流系統最怕的就是半夜 DB 出事,我整個人瞬間清醒,一邊爬起來開電腦一邊在腦中盤點:昨天有上什麼版嗎?有人動過 DB 設定嗎?是不是主從切換出問題?
結果全都不是。真正的原因蠢到我後來不太好意思講,但既然要覆盤,就老實說。
我一開始想錯的地方
我花了快二十分鐘在錯的方向上。
看到一堆服務同時失敗、又都跟「寫入」有關,我直覺認定是共用的那台資料庫出問題。於是我先去看 DB 的 CPU、記憶體、連線數——全部正常。CPU 甚至比平常還低,因為請求根本進不來。這時我才覺得不對勁:如果是 DB 自己的效能問題,負載應該是高,不是低。
接著我 SSH 進其中一台應用機,想看看服務的 log 到底噴了什麼。結果連 vim 都打不開,跳出一行 No space left on device。
那一刻我全身涼掉。順手敲了 df -h,答案就攤在眼前:
/dev/sda1 200G 200G 0 100% /磁碟,滿了。100%,一個 byte 都不剩。
真正的根因:那行是我加的
定位到是哪個檔案吃掉磁碟不難,du -sh /var/log/app/* | sort -h 一跑,一個單一的 log 檔顯示 168G。前一天同時間它還只有幾百 MB。
我點開一看,整個檔案在瘋狂重複同一段內容:某個下游服務回了非預期的格式,我們的解析在一個重試迴圈裡,每重試一次就把完整的 request/response body 用 logger.Debug 印出來。而那個下游從半夜某個時間點開始持續回錯,於是這段迴圈就以每秒上千次的頻率、每次都吐一大坨 JSON,一路寫到天亮。
更打臉的是,那行 logger.Debug 是我三天前加的。當時為了查一個偶發的解析問題,我特地把整包 body 印出來,還在 PR 底下留言「查完就拿掉」。結果查到了、修了、然後我忘了拿掉,連 log level 都忘了它在 production 是開著的。
磁碟一滿,連鎖反應就開始了。PostgreSQL 的 WAL 寫不進去,交易全部 abort;應用程式要寫暫存檔失敗;甚至連要寫一行「我寫入失敗了」的新 log 都寫不了。所有依賴「寫檔」的東西同時死掉——這就是為什麼現象看起來像四五個不相干的元件一起爆炸。
為什麼磁碟滿這麼致命,又這麼常被忽略
磁碟滿的可怕之處在於它的失敗方式很奇怪:它不會讓某一個服務掛掉,而是讓「所有需要寫入的操作」一起失敗,卻各自報各自的錯。你會看到一堆看似無關的症狀,很難第一眼指向同一個根因。
而且它是「有限資源被慢慢吃光」型的故障。CPU 爆了、記憶體爆了,大家都會警覺,因為監控面板天天在看。但磁碟用量是那種平常 60% 躺在那邊、你半年不會多看一眼的指標。它從 60% 漲到 100% 可能只需要幾個小時,中間完全沒人攔。
說穿了,我們當時有兩個致命的缺口:
- 沒有 log rotation。 那個 log 檔就是一個會無限長大的檔案,沒有任何機制在它超過某個大小時去切割、壓縮、刪除。
- 沒有磁碟用量告警。 我們的告警系統盯著 CPU、記憶體、QPS、錯誤率,唯獨沒有人設磁碟。等到「發現」磁碟滿,是靠服務全掛反推回來的,這已經太晚了。
當下怎麼止血
凌晨的優先目標只有一個:讓磁碟騰出空間,把服務救回來。
- 先確認那個暴漲的 log 檔可以砍。直接
rm之後有個坑:如果還有 process 開著這個檔案的 handle,空間不會真的釋放,得先讓寫它的程序停掉或重啟。我的做法是先把那個吵死人的服務停掉,再刪檔,df -h立刻掉回 40%。 - 清掉其他明顯的舊日誌和暫存檔,多爭取一點餘裕。
- 把噪音來源關掉:緊急改設定把那個 service 的 log level 從 debug 拉回 info,重新部署,確認迴圈不再狂寫。
- 磁碟一有空間,
WAL就能寫了,DB 自己恢復,應用服務跟著綠回來。前後大概四十分鐘,但其中二十分鐘是我在查錯方向。
止血完我沒馬上回去睡,因為我很清楚,今天能靠手動 rm 解決,不代表下次還來得及。
長期怎麼修
隔天的 post-mortem 我把改善項目列成幾條,逐一落地:
- 上 `logrotate`。 依大小和時間輪替,超過設定大小就切割、壓縮、保留 N 份後自動刪除。這是最基本卻最容易被跳過的一步。
- production 不開 debug。 日誌分級要當一回事,debug 只在本機和測試環境開。臨時為了查問題開的,要有機制提醒自己關掉,而不是靠一句 PR 留言和我的記性。
- 高頻錯誤要限流採樣。 同一個錯誤在迴圈裡狂噴時,印第一萬筆跟第一筆帶來的資訊是一樣的。改成每秒最多印幾筆、或每 N 筆採樣一筆,並帶上累計計數,資訊沒少,磁碟壓力少掉好幾個數量級。
- 日誌集中送出,不要堆在本機。 結構化日誌透過 agent 送到集中式的 log 系統,本機只留短期緩衝。就算某台機器狂寫,也不會把單機磁碟塞爆。
- 補上磁碟使用率告警。 這是最重要的一條。磁碟到 80% 就 warning、90% 就 critical,直接呼叫 on-call。一個門檻告警,就能把「服務全掛才發現」變成「還有兩小時餘裕時就處理」。
小結
這次事故給我最深的一句話是:日誌是資產,也是負債。
它幫你查問題的時候是資產,但它同時在吃磁碟這種有限資源。你印得越多、印得越沒節制,這筆負債就越重,只是平常看不到帳單,直到它一次跟你結清。
另一個教訓是關於監控的盲區。我們太習慣盯著 CPU 和記憶體,好像系統只會死在這兩件事上。但磁碟一樣會殺死你,而且死狀更難看——它讓一切寫入同時失敗,卻不直接告訴你是磁碟的錯。監控要盯的是所有會被耗盡的有限資源,磁碟絕對是其中之一。
至於那行 debug log,我現在的習慣是:任何臨時加的高頻日誌,加的當下就設好它的死期,要嘛包在明確的開關後面,要嘛在同一個 commit 就決定它什麼時候消失。信誓旦旦「查完就拿掉」這種話,我是再也不敢信自己了。
留言討論
有想法、有不同經驗、或想糾正我?歡迎在下面留言,免註冊,填個暱稱就能留。