🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

你打的日誌,正在拖垮你的系統:從P4小白到P7專家都是怎麼打日誌的?

老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小白 —— 日誌 = “到此一遊”的塗鴉

行為特徵:

  • 萬物皆可用System.out.println()或e.printStackTrace()。
  • 日誌內容隨心所欲,比如log.info("111"), log.info("走到這裡了")。
  • 熱衷於用字串拼接("value=" + var)來構建日誌消息。

潛在的“B面”災難:

  1. 性能殺手:用字串拼接,即使日誌級別被禁用,也會強制執行字串操作(浪費資源),在高併發下嚴重拖慢系統。
  2. 資訊丟失:習慣性地丟掉異常,只打印e.getMessage()而不記錄完整的堆疊跟蹤,丟棄了最關鍵的異常資訊。
  3. 毫無價值:無法關閉,無法分級,無法被集中式日誌系統(如SLS)進行有效採集和分析。線上出事時,你只能瞪眼看著。

第二境: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那種感覺,才是真正的大海中撈針。

行為特徵:

  • 已經學會了使用日誌門面(如SLF4J)和實現(如Logback),懂得INFO, WARN, ERROR的區別。
  • 日誌內容開始關注業務流程,比如log.info("用戶下單成功,訂單號:{}", orderId)。

潛在的“B面”災難:

  1. 資訊孤島,無法定位問題:日誌只能證明“這個方法被執行了”,但無法串聯起一個完整的用戶請求鏈路。一旦出問題,你看到的只是散落在幾十台機器上的、毫無關聯的日誌碎片。
  2. 缺少關鍵上下文:日誌裡只有orderId,但沒有trace_id或其他關鍵的資訊。大型系統中如果缺少trace_id這樣的關聯ID,當一個用戶反饋問題時,我們根本無法從海量日誌中,找到屬於他的那幾條。

第三境:P6/P7專家 —— 日誌 = 天網

行為特徵:
專家打日誌,追求的不是簡單“記錄”,而是“可觀測性”和“可診斷性”。他們會讓日誌成為系統的“黑匣子”。

“B面”心法:

  1. 心法一:結構化一切

    • What:不再打印純文本,而是輸出JSON格式的日誌。
    • Why:結構化的日誌,才能被SLS、ELK等系統完美解析、索引和聚合查詢。這樣才能解答“過去一小時,service_name為payment-processor且user_id為123的所有ERROR日誌”這類問題。
    • How:例如,使用SLF4J + Logback,配合JSON Encoder:
    log.error("{\"event\":\"order_creation_failed\", \"order_id\":\"{}\", \"user_id\":\"{}\", \"error\":\"{}\"}", orderId, userId, e.getMessage());

老A說: 別小看這個JSON。有次618,我們需要緊急統計某個特定優惠券,在上海地區,因為庫存不足而失敗的下單次數。用文本日誌,SRE需要花半小時寫腳本去撈。用結構化日誌,我在SLS上只用10秒鐘,就給出了答案。 這,就是專家的效率。

  1. 心法二:上下文為王 (MDC & trace_id)

    • What:MDC(Mapped Diagnostic Context)是Java中記錄與線程相關的上下文的一種機制。底層使用ThreadLocal。通過 MDC,我們可以為當前線程附加一些特定的上下文資訊(例如用戶 ID、事務 ID),這些資訊會自動與日誌關聯,從而幫我們更有效地分析和追蹤日誌。
    • Why:每條日誌必須包含trace_id,用於追蹤請求在多個微服務間的流轉。MDC像線程的“專屬背包”,在入口處放入trace_id,後續日誌自動攜帶。
    • How:在Interceptor中設定:
    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,有興趣的同學可以去搜搜。

  1. 心法三:日誌本身就是“炸彈”
    • What:日誌打印不當,可能會引發大型故障。
    • Why:舉個栗子:Redis超時 → 海量錯誤日誌 → 撐爆Logstash → 丟棄日誌 → 關鍵線索丟失。這就是日誌炸彈。
    • How:別直接打印複雜物件,只打印關鍵ID和欄位。高頻事件用採樣,如只記錄1%的INFO日誌(參考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時間

感謝各位兄弟的閱讀。
我是老A,一個只想跟你說點B面真話的師兄。如果這篇文章讓你有了一點點啟發,那就是對我最大的肯定。
為了感謝大家的支持,我把這兩年在一線大廠面試和帶團隊中,沉澱下來的所有上不了台面的私房筆記,整理成了一份《程式員B面生存手冊》。
裡面沒有市面上千篇一律的八股文,只有一些極其管用的“潛規則”和“避坑指南”,希望能幫你少走一些彎路。
關注我的同名公眾號【大廠碼農老A】,在後台回覆“B面”,就能免費獲取。
回覆“簡歷”獲取《簡歷優化手冊》
回覆“arthas”獲取史上最全的《大廠arthas實戰手冊》
回覆“指導”獲取《外包鍍金手冊》
回覆“日誌”獲取《技術專家日誌打印秘籍》
最後,如果覺得內容還行,也希望能點個讚、點個在看,讓更多需要它的兄弟看到。
我們一起,在技術的路上結伴“陪跑”。


原文出處:https://juejin.cn/post/7561077821995597876


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝23   💬9   ❤️6
633
🥈
我愛JS
📝4   💬14   ❤️7
276
🥉
御魂
💬1  
3
#4
2
#5
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付