在工作中,相信大家最怕的一件事就是聽到有人在工作群艾特你:某某功能報錯啦。。。
然后你就得屁顛屁顛的去服務(wù)器看日志,日志量少還好點,多的話找起來太麻煩了。不太容易直接定位到關(guān)鍵地方。
東找找西找找,好不容易找到了報錯的信息,卻不知道當(dāng)時的參數(shù)信息是什么,也不太好復(fù)現(xiàn),太難了。。
改完還得寫故障報告,美好的一天又沒了。
要解決這類的痛點需要做下面幾件事情:
- 日志收集
- 異常告警
- 日志增加鏈路
- API響應(yīng)增加traceId
- 異常時打印當(dāng)前報錯方法的參數(shù)
- 支持調(diào)試模式
日志收集
要解決的第一個問題就是日志的集中管理,不然報錯了你得去多臺服務(wù)上找錯誤信息,效率太低了。當(dāng)然也可以用ansible這種工具來做,最好的還是日志統(tǒng)計收集起來,通過Web頁面就可以搜索查看。
日志收集的標(biāo)桿就是ELK啦,不做過多講解。像我們是用的云服務(wù),收集起來更方便,直接頁面上點點就搞定了。
日志增加鏈路
日志增加鏈路跟蹤功能分為兩個步驟,首先系統(tǒng)要有鏈路跟蹤,然后將鏈路信息集成到日志中就可以了。
我用的是Spring Cloud Sleuth,主要是Sleuth對很多開源的框架都支持了,也集成了logback這樣的日志框架,用起來非常方便。
Sleuth默認(rèn)增強(qiáng)的日志格式如下:
[${spring.zipkin.service.name:${spring.application.name:-}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]
分別是服務(wù)名,鏈路ID, 工作單元ID,是否導(dǎo)入到zipkin中。對于日志來說最重要的就是traceId了,有了traceId就能將所有系統(tǒng)的日志串連起來了。
我們也可以自己擴(kuò)展,增加一些其他的信息放入日志中。比如:
%X{X-REST-API:-},%X{X-RPC-SERVICE:-},%X{X-ORIGIN-INFO:-},%X{X-USER-ID:-},%X{X-BIZ-NAME:-},%X{X-BIZ-ID:-}
- X-REST-API:入口API, 全局透傳
- X-RPC-SERVICE:入口RPC, 每個服務(wù)入口處新增
- X-ORIGIN-INFO:來源信息(調(diào)用方應(yīng)用名:IP:服務(wù)名)
- X-USER-ID:用戶ID, 全局透傳
- X-BIZ-NAME:業(yè)務(wù)名稱, 全局透傳, 應(yīng)用內(nèi)可替換
- X-BIZ-ID:業(yè)務(wù)ID, 全局透傳, 應(yīng)用內(nèi)可替換
有了這些擴(kuò)展的信息就可以直接從日志中知道當(dāng)前請求的入口API是哪個,也知道整個請求經(jīng)過了哪些服務(wù)。
如果我是訂單服務(wù)的負(fù)責(zé)人,當(dāng)我去排查問題的時候根據(jù)日志就知道當(dāng)前這個錯誤是上游哪個系統(tǒng)和哪個接口調(diào)用導(dǎo)致的。
日志中還帶上了用戶信息,知道是哪個用戶的請求。
BIZ-ID和BIZ-NAME可以用于業(yè)務(wù)場景的問題排查,比如下單后就知道訂單ID了,后續(xù)就可以將訂單ID追加到日志中,BIZ-NAME=order, BIZ-ID=20102121212121。
后續(xù)有支付,發(fā)貨,退款等等訂單相關(guān)的操作,日志中都有訂單ID,需要排查的時候直接根據(jù)訂單ID就可以看到整個訂單相關(guān)的日志信息,前提是打了這些信息。
異常告警
除了用戶反饋功能異常,開發(fā)也應(yīng)該在第一時間知道出問題了。所以異常告警一定要做。
一般我們的應(yīng)用分為:服務(wù)應(yīng)用,Job應(yīng)用,異步消費應(yīng)用
服務(wù)應(yīng)用我們可以在統(tǒng)一的異常處理中進(jìn)行告警,Job應(yīng)用也可以在統(tǒng)一調(diào)度的入口進(jìn)行告警,異步消費的也是一樣。
可以通過消息隊列告警,也可以通過制定日志的格式,通過記錄日志的方式,讓日志收集到日志平臺,然后配置各種規(guī)則進(jìn)行告警。
異常告警的時候就可以帶上traceId,這樣在發(fā)現(xiàn)異常的時候,直接通過traceId去日志平臺搜索,就可以看到這個traceId相關(guān)的所有日志,對排錯很有幫助,前提是你打印了關(guān)鍵的日志信息。

API響應(yīng)增加traceId
可以通過ResponseBodyAdvice對響應(yīng)結(jié)果進(jìn)行統(tǒng)一定制,增加traceId響應(yīng)。這樣在出問題的時候可以通過響應(yīng)的traceId直接去日志平臺進(jìn)行日志的搜索。

除了快速排查異常問題之外,對于性能優(yōu)化的時候,我們也可以直接根據(jù)traceId去查看這個API對應(yīng)的耗時情況,前提是這個traceId就是跟你們的APM系統(tǒng)一致就行。
異常時打印當(dāng)前報錯方法的參數(shù)
通過前面的操作,我們已經(jīng)可以在異常的時候獲取一個traceId去排查相關(guān)錯誤信息,也不用去多臺機(jī)器隨機(jī)找日志了,極大的提高了問題解決的速度。
只能說這些操作對我們的問題排查起了一半的幫助作用,比如說我現(xiàn)在收到一個告警,然后我去日志平臺查了相關(guān)的日志,發(fā)現(xiàn)某行報錯了。
這個時候也只能是猜測這個地方是有問題的,因為我不知道當(dāng)時是什么參數(shù)導(dǎo)致這行報錯了。所以如果能在報錯的時候?qū)?dāng)前報錯方法的參數(shù)打印到日志中,也就相當(dāng)于保留了出問題時的現(xiàn)場,解決起問題來就是分分鐘的事。
具體實現(xiàn)方案沒有固定的,最簡單的方式就是寫一個Aspect切到所有業(yè)務(wù)方法上,當(dāng)方法拋出異常的時候記錄參數(shù)信息,切記只在異常的時候做這個記錄的操作,否則對性能影響很大。
效果:
com.xxx.biz.service.impl.GoodsSkuServiceImpl.createSku異常, 參數(shù)信息:{"cspuId":1, stock:10, price:100}
Caused by: java.util.NoSuchElementException: No value present
at java.util.Optional.get(Optional.java:135)
at com.xxx.biz.service.impl.GoodsSkuServiceImpl.createSku(GoodsSkuServiceImpl.java:682)
支持調(diào)試模式
支持調(diào)試模式指的是在某些場景下,我們可以復(fù)現(xiàn)出錯誤,但是除了當(dāng)時異常時記錄的參數(shù)信息,還想知道整個請求鏈路的參數(shù)和響應(yīng)。也就是從入口處經(jīng)過的所有方法都能夠打印出請求和響應(yīng)數(shù)據(jù)。
可以定義一個特定的請求頭,在復(fù)現(xiàn)問題的時候帶上這個請求頭,由統(tǒng)一的框架去接收這個請求頭,然后在整個鏈路上進(jìn)行透傳。再結(jié)合異常的那個Aspect將參數(shù)和結(jié)果進(jìn)行日志輸出即可。
效果:
xxx.xxxController.makeOrder 參數(shù):xxx
xxx.xxxRpcService.makeOrder 參數(shù):xxx
xxx.xxxStockRpcService.lockStock 參數(shù):xxx
xxx.xxxStockRpcService.lockStock 響應(yīng):xxx
xxx.xxxRpcService.makeOrder 響應(yīng):xxx
xxx.xxxController.makeOrder 響應(yīng):xxx
關(guān)于作者:尹吉歡,簡單的技術(shù)愛好者,《Spring Cloud微服務(wù)-全棧技術(shù)與案例解析》, 《Spring Cloud微服務(wù) 入門 實戰(zhàn)與進(jìn)階》作者, 公眾號 猿天地 發(fā)起人。