Spring日志擴展

分布式的流行導致程序的調用關系越來越復雜,一次調用可能會涉及到十幾個微服務的運轉和幾十個代碼塊的執(zhí)行,特別是與訂單和支付相關的業(yè)務。此外還會涉及到各個部門的溝通和協(xié)作。

當線上項目出現(xiàn)問題,如何在海量日志中快速有效地定位到故障點,就顯得至關重要。

通過調用鏈,利用一個自上而下全局的調用id,把每一次請求調用過程完整的串聯(lián)起來,將日志以調用id為維度進行匯總和分類,這樣就可以實現(xiàn)對調用鏈的跟蹤和監(jiān)控。目前有許多比較成的分布式跟蹤系統(tǒng),如Google的Dapper,Twitter的zipkin,淘寶的鷹眼,京東的Hydra等。

今天筆者就來為大家分享一下,如何一步步通過擴展現(xiàn)有的日志組件,實現(xiàn)一個簡單的調用跟蹤功能,從而能夠使用flowId(即調用id)來對日志進行歸類,達到快速定位bug的效果。

1、生成、傳遞和輸出flowId

如何生成flowId?這里以rest服務為例。可以讓所有的Request參數(shù)就都繼承一個BaseRequest,BaseRequest中包括一個flowId,

這樣所有的請求都可以自帶flowId,也可以隨著方法地調用一層一層地傳遞下去,辦法和思路都很簡潔,但實施起來卻有諸多隱患:

會對項目之前所有的Request類進行修改。

按照當前思路,日志輸出的時候還需要在原有的log代碼上添加相應的log輸出操作。

若是無參方法,那豈不是要專門添加參數(shù)來傳遞這個flowId?

總的來說,代碼修改量太大,回歸測試點較多,容易影響現(xiàn)有的程序。

如何才能最方便地生成、傳遞和輸出flowId呢? slf4j的MDC可以滿足我們的要求。MDC(Mapped Diagnostic Context)為每個線程建立一個獨立的存儲空間,MDC 中包含的內容可以被同一線程中執(zhí)行的代碼所訪問。開發(fā)人員可以根據需要把信息以 key/value 對的形式存儲在 Map 中,當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可,

下面介紹一下使用方法:

// 清空map所有的條目。

public static void clear();

// 根據key值返回相應的對象

public static Object get(String key);

//返回所有的key值.

public static Enumeration getKeys();

//把key值和關聯(lián)的對象,插入map中

public static void put(String key, Object val),

//刪除key對應的對象

public static ?remove(String key)

通過MDC.put(“flow-id”, flowId),將flowId放入上下文,key為flow-id,然后指定log輸出格式為:

其中%X{flow-id}就是用來讀取MDC里面的key為flow-id的值,即可在log中輸出flowId。

2、flowId在服務之間傳遞

上面說到,MDC是基于上下文傳遞的,所以原生的MDC信息不能跨服務調用,但是flowId的使用都遵守統(tǒng)一的原則:在使用前賦值,在使用后銷毀。這一原則可以幫助我們找到切入點,在這里,筆者就以RabbitMQ RPC為例,來說一下我的解決方案。

Spring AMQP提供了對rabbitMQ的封裝,參考下面配置文件:

其中有兩個關鍵類RabbitTemplate和AmqpInvokerServiceExporter,RabbitTemplate是一個消息模板類,可以通過調用RabbitTemplate 的convertSendAndReceive方法發(fā)送和接收消息消息,在發(fā)送之前會通過Message convertMessageIfNecessary(final Object object)方法來對消息進行預處理

封裝成用于傳遞消息的Message對象,message中包括了遠程接口的參數(shù)和message的一些屬性MessageProperties,而MessageProperties會有一個叫做header的HashMap對象,如下圖:

類之間的關系可參考下圖:

參考源碼截圖和類關系可以得出結論:消息經過組裝,處理之后發(fā)送給遠程服務,并攜帶了headers,如果我們在消息處理的時候,覆寫原來的消息處理方法convertMessageIfNecessary,把需要傳遞的flowId封裝在header中,即可將它發(fā)送給遠程服務,實現(xiàn)方法參考下圖:

flowId已經被發(fā)送給了服務方,接下來我們研究一下服務方如何接收和輸出flowId信息。

AmqpInvokerServiceExporter可以發(fā)布注冊服務和監(jiān)聽客戶端的消息,并通過AMQP傳遞,onMessage方法可在接收到消息之后讀消息進行提取,處理和回送,所以我們可以覆寫onMessage方法,在onMessage方法中取出headers中的flowId,并put到MDC中,即可成功接收flowId:

相關類關系可參考:

定義好了兩個子類,再通過配置文件注入到上下文中,就可以生效了。

以上為大家分享了通過微調MQ相關的類,將flowID從客戶端傳遞到服務端。Rest服務也可以通過Filter和request header實現(xiàn)這一功能,這里就不贅述了。

3、flowId的多線程問題

當我們在多線程場景下是,會遇到flowId為空的情況,如ThreadLocal一樣,MDC信息也無法被子線程獲取。 MDC提供了getCopyOfContextMap()方法來從父線程賦值MDC信息,并通過setContextMap(mdcContext)方法來賦值到子線程,這樣子線程就繼承了父線程的flowId。

在spring中還有一種特殊的線程啟動方式,如quarz和Scheduled,沒有按照我們平時的Thread.start()方式啟動,但跟蹤Scheduled的運行可以發(fā)現(xiàn),這類方法是通過反射的方式調用的,所以筆者考慮避開線程,從job啟動時執(zhí)行的方法入手:自定義一個注解@FlowIdAnnotation來標記需要切入并生成flowId的方法,在Aspect中定義一個@Around方法,加入flowId生成和銷毀的邏輯,即可達到需求。

寫在最后

以上就是筆者本次分享的全部內容,Spring生態(tài)系統(tǒng)為開發(fā)者提供了一個很優(yōu)秀的平臺,但在我們的使用過程中難免遇到一些有特殊需求的場景,這時需要我們在原來的基礎上做一些擴展。

本文以一次日志擴展拋磚引玉,期待大家分享更多、更好的DIY。

參考文獻:

https://yq.aliyun.com/articles/58408

http://www.cnblogs.com/LBSer/p/3390852.html

本文作者:譚雷(點融黑幫),現(xiàn)就職于點融成都Data部門,畢業(yè)于四川大學,熱愛排球運動,勵志做一個瘦一點的程序員。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,533評論 19 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,637評論 18 399
  • 一. Java基礎部分.................................................
    wy_sure閱讀 4,010評論 0 11
  • 青春,在電影里、故事中都充滿了浪漫色彩……事實中,更多人的青春是枯燥的,煩悶的,索然無味的…… 所以...
    冰茜閱讀 129評論 0 0
  • 01 去年的平安夜,是和JJ一起度過,我記得我們下班之后,一起去吃了炒菜,有沒有喝酒忘記了,吃完飯之后,我們各種拍...
    田小辣閱讀 709評論 0 2

友情鏈接更多精彩內容