老A的程式碼茶座 vol.1
大家好,我是老A。
國慶假期的某天,我正懶洋洋地躺在海灘的沙灘椅上,哈著冰啤酒,海風拂面,惬意極了。
突然,手機震動個不停。點開一看,是公司告警群裡接連蹦出幾條「磁碟空間不足」的告警消息。雖然這不是我負責的應用,但我還是好奇地戳進去瞄了一眼。原來是日誌檔案膨脹得太猛,把磁碟給塞滿了。沒多久,負責的同事在群裡發話:“這日誌檔案忘了掛載運維平台的自動清理腳本了。”他手動刪掉一些舊日誌,磁碟佔用瞬間恢復正常。
這事兒讓我不由得陷入了沉思。打日誌,看似程式員日常中最不起眼的小事 —— 後端、前端、客戶端,誰不是天天在打?但稍有不慎,輕則導致磁碟佔用飆升,重則在線上故障時因為缺失關鍵日誌而束手無策。明明是基礎操作,卻常常被忽略。從這個磁碟告警就能看出,就連大廠裡不少人,也沒把打日誌這件“小事”當回事兒。這本質上是一種認知偏差:小事不處理,往往釀成大事。
所以,今天咱們就來聊聊這個每個程式員每天都在做,但90%的人都沒做對的事——打日誌。把我從坑裡爬出來的經驗,分享給你,避免你重蹈覆轍。
說起打日誌的坑,我和身邊的同事們可謂是身經百戰,基本上把能踩的都踩了個遍。尤其是那些剛入職的P4小白,日誌打得那叫一個隨性
,結果往往是自食苦果。
先說第一個經典坑:日誌打了個寂寞
。
之前有個供應鏈團隊的合作同事,剛校招入職,化名小張吧。我們因為項目合作頻繁,關係不錯,他經常來找我討教技術問題。
有一次,他遇到一個線上偶發Bug,用戶反饋操作失敗。他急吼吼地跑來求助:“A哥,我在SLS裡翻了半天,只有一句‘order process error!’,根本不知道是哪個用戶、哪筆訂單、在哪行碼出的錯!這Bug沒法復現,告警也沒觸發,日誌沒線索,咋辦啊?”
我讓他把出問題的代碼發給我瞧瞧。瞄了幾眼瞬間明白了:他的问题不是Bug難復現,而是就算復現了,這日誌也沒卵用。代碼大致是這樣(偽代碼,展示日誌打印的問題):
@Service
public class OrderService {
public void processOrder(OrderDTO order) {
try {
// ...此處省略50行業務邏輯...
// 問題實際在這裡:在某種邊界條件下,order.getCustomer()可能返回null,導致NPE
String customerName = order.getCustomer().getName();
log.info("OrderService start process order..."); // 這行日誌沒打任何關鍵資訊
// ...此處省略另外50行業務邏輯...
} catch (Exception e) {
// 日誌打了個寂寞。。。
log.error("OrderService#order process error!");
}
}
}
大家仔細品品這段代碼裡的日誌,相信不少新人都會心有戚戚焉。這裡面藏著小白門常見的三大問題:
問題一:異常被莫名其妙地吃掉
看看catch塊裡,那個至關重要的Exception呢?直接被吃了!連完整的堆疊資訊都不打印,也沒有向上拋出,就這麼被“吃乾抹淨”,不留痕跡。這就好比偵探趕到犯罪現場,發現一切證據都被擦得乾乾淨淨,還怎麼破案?
問題二:沒有任何關鍵資訊
“OrderService#order process error!”——這是啥意思?哪個訂單?哪個用戶?哪個商品?日誌裡一個業務ID都沒帶。每秒鐘成千上萬筆訂單湧入,這樣的日誌無異於大海撈針,純純浪費時間。
問題三:異常資訊沒有體現在日誌中
error——到底是什麼error?是NPE?資料庫連接超時?還是RPC異常?一無所知。
最後,我嘆了口氣:“Bro,你的問題不是Bug無法復現,而是就算復現了,你這日誌也定位不到問題。你這日誌打了個寂寞啊。”
是不是在小張身上看到了自己曾經的影子呢?你有思考過如何打日誌這個問題嗎?其實這裡面還是有一些學問的。
在大廠這麼多年,我總結出了打日誌的“三層境界”,從P4小白到P7專家,每一級都有對應的行為特徵和潛在“B面”災難。咱們一層一層扒开,看看你處在哪一境界。
第一境:P4小白 —— 日誌 = “到此一遊”的塗鴉
行為特徵:
潛在的“B面”災難:
第二境:P5中級 —— 日誌 = “業務流水帳”
P5級別的工程師,已經懂得封裝Service,但處理異常的方式,也經常存在一些問題。來看小張的另一段代碼示例:
@Service
public class OrderService {
public void createOrder(OrderDTO order) {
try {
// ...業務邏輯...
String userName = null;
userName.toLowerCase(); // 這裡會一個NPE
} catch (Exception e) {
// 注意:這裡沒有打印日誌,直接向上拋出一個模糊的異常
throw new BizException("創建訂單失敗");
}
}
}
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/orders")
public void createOrder(@RequestBody OrderDTO order) {
try {
orderService.createOrder(order);
} catch (BizException e) {
// 日誌在這裡打印,但沒有實際異常的詳細堆疊和資訊
log.error("處理創建訂單請求失敗!", e);
}
}
}
老A點評:
兄弟們,看懂了嗎?當線上出問題時,你在Controller層看到的日誌,只會告訴你創建訂單失敗,你無法知道問題的根因其實是OrderService第XX行那個NPE。這就是異常日誌的二次轉手
,破案線索,在第一現場就被破壞了。
我至今都記得,有一次為了排查一個履約單的Bug,我和另一個同事,花了整整一個通宵,在幾十萬行日誌裡,去定位一個被這樣二次轉手
過的NPE
。那種感覺,才是真正的大海中撈針。
行為特徵:
潛在的“B面”災難:
第三境:P6/P7專家 —— 日誌 = 天網
行為特徵:
專家打日誌,追求的不是簡單“記錄”,而是“可觀測性”和“可診斷性”。他們會讓日誌成為系統的“黑匣子”。
“B面”心法:
心法一:結構化一切
log.error("{\"event\":\"order_creation_failed\", \"order_id\":\"{}\", \"user_id\":\"{}\", \"error\":\"{}\"}", orderId, userId, e.getMessage());
老A說: 別小看這個JSON。有次618,我們需要緊急統計
某個特定優惠券,在上海地區,因為庫存不足而失敗的下單次數
。用文本日誌,SRE需要花半小時寫腳本去撈。用結構化日誌,我在SLS上只用10秒鐘,就給出了答案。 這,就是專家的效率。
心法二:上下文為王 (MDC & trace_id)
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String traceId = UUID.randomUUID().toString();
MDC.put("trace_id", traceId);
return true;
}
}
然後日誌中會自動帶上trace_id:
log.error("訂單失敗, orderId: {}", orderId);
// 會隱含trace_id
老A說:
MDC
就是線上排錯的GPS。有一次,一個用戶反饋他的賬戶餘額顯示異常。在沒有trace_id
的年代,我們需要去用戶、交易、支付三個系統的幾十台機器上,靠著userId
和時間戳去人肉關聯日誌。
有了MDC
,我只需要拿到一個trace_id
,就能在SLS或ELK裡,一鍵拉出這個用戶從App點擊到資料庫落地的完整生命周期。 5分鐘搞定。其實我們廠基本都用EagleEye,有興趣的同學可以去搜搜。
心法三:日誌本身就是“炸彈”
老A說: 別以為日誌打多了沒事。我親眼見過一個P2故障,就是因為一個同事在
log.info
裡,打印了一個超大物件。高並發流量一來,光是這個toString()
方法的開銷,就把整個集群的CPU干到了95%以上,比業務邏輯本身還耗資源。 這是自殺式打日誌。”
現在,針對第二境的坑,來看看P7專家的正確解法:
@Service
public class OrderService {
public void createOrder(OrderDTO order) {
try {
// ...業務邏輯...
String userName = null;
userName.toLowerCase();
} catch (Exception e) {
// 正解:記錄下最完整的錯誤和堆疊
log.error("創建訂單核心邏輯發生異常!orderId: {}", order.getId(), e);
// 然後再向上拋出業務異常,通知上層調用失敗
throw new BizException("創建訂單失敗", e); // 把原始異常作為cause傳遞
}
}
}
老A點評: 同樣是拋出異常,但專家在拋出前,先用一行log.error,把
包含了完整堆疊資訊和關鍵業務ID(orderId)的第一手證據
,牢牢地釘在了日誌裡。這,就是專業。
在聊完打日誌的三層境界後,我們不妨再往前走一步,思考一下 一個真正成熟的日誌系統該是什麼樣子
。
一個成熟的日誌系統,不應該僅僅是記錄資訊的工具,而應該是整個系統可觀測性的一個核心支柱。應該像一台精密的儀器,靜靜地運行,卻能在關鍵時刻提供最有力的支持。
要達到這個目標,它必須具備三大核心能力。
能力一:跨系統的“全局透視”能力
在一個分佈式架構中,我們面臨的第一個挑戰,就是資訊孤島。成熟的日誌系統,首先要解決的就是看得全的問題。通過trace_id
這根線索,將一個用戶請求在幾十個微服務之間的完整調用,串聯成一條可視化的調用鏈路。就像阿里巴巴的EagleEye系統那樣,它能讓你在上帝視角,清晰地看到一個請求從前端到資料庫的每一個環節,哪裡卡殼、哪裡高效,一目了然。
能力二:恰到好處的數據呈現能力
看得全,不等於資訊越多越好。成熟的日誌系統,追求的是恰到好處。
一方面,它的每一條日誌,都採用結構化的JSON格式,只包含timestamp
, trace_id
, span_id
, error_code
等最關鍵的欄位,做到清晰、完整卻不冗餘。
另一方面,它有完善的過期機制。通過基於時間(保留7天)或大小(超過1GB自動輪轉)的過期策略,確保日誌不會成為拖垮磁碟的定時炸彈——記得我們開頭的那個告警故事吧?那就是反面教材。
能力三:“先知先覺”的自動化響應能力
看得全、看得清,最終是為了效率高。一個成熟的日誌系統,應該是一個半自動化的哨兵。
當它通過trace_id
發現某條鏈路的錯誤率超過閾值時,它能自動觸發告警,通過釘釘通知到責任人。在更高級的系統中,它甚至應該能觸發自動化的修復腳本——比如隔離故障節點、回滾配置。
這,才是日誌的終點:從被動的記錄員,進化為主動的系統守護神
。
老A感悟:
一個工程師在日誌層面的成長,就是從用日誌記錄,到用日誌說話,再到用日誌透視整個系統的過程。你打日誌的水平,就是你對系統掌控能力的真實寫照。
感謝各位兄弟的閱讀。
我是老A,一個只想跟你說點B面真話的師兄。如果這篇文章讓你有了一點點啟發,那就是對我最大的肯定。
為了感謝大家的支持,我把這兩年在一線大廠面試和帶團隊中,沉澱下來的所有上不了台面的私房筆記,整理成了一份《程式員B面生存手冊》。
裡面沒有市面上千篇一律的八股文,只有一些極其管用的“潛規則”和“避坑指南”,希望能幫你少走一些彎路。
關注我的同名公眾號【大廠碼農老A】,在後台回覆“B面”,就能免費獲取。
回覆“簡歷”獲取《簡歷優化手冊》
回覆“arthas”獲取史上最全的《大廠arthas實戰手冊》
回覆“指導”獲取《外包鍍金手冊》
回覆“日誌”獲取《技術專家日誌打印秘籍》
最後,如果覺得內容還行,也希望能點個讚、點個在看,讓更多需要它的兄弟看到。
我們一起,在技術的路上結伴“陪跑”。