關於「日志采樣」的一些思考及實踐

2024年2月6日 21点热度 0人点赞

一、背景:

系統日志可用於追蹤用戶操作軌跡,異常情況下,合理的日志有助於快速排查、定位問題,毫無疑問,打印日志對於系統是很重要的。

當業務規模較小時,大傢都傾向於享受日志帶來的便利,從而忽略日志帶來的潛在的負面影響,缺乏對日志的管控。在JD當前用戶量、業務規模下,絕大多數C端系統、甚至B端系統都是高吞吐的,毫無疑問,過大的日志量對系統的性能、磁盤IO有著顯著負面影響,趕上大促時,問題尤為突出。日志在為我們提供便利的同時,也無時無刻成為一根刺,時不時刺我們一下。

作為一個共性問題,由於集團暫沒推出統一的日志框架,不少團隊都會嘗試基於log4j、logback 進行輕度的封裝,通過跟配置中心聯動,增加一些諸如 '動態降級' 的功能、來緩解日志帶來的負面影響。降級帶來的效果是顯著的,但同時也讓系統喪失了記錄 '操作軌跡' 的能力,從而又帶來了新的問題。

此時,很容易想到,可以通過對 '請求' 采樣,實現請求日志的采樣輸出,並通過控制采樣比例平衡不同場景下日志對性能的影響,系統吞吐量較大時,降級采樣比例,系統吞吐量較低時,提高采樣比例。

二、正文:

在請求入口處,通過一定的采樣算法,計算當前請求是否采樣,並借助 Java 的 ThreadLocal 機制,可以實現當前請求線程的日志采樣。但是,大部分關註日志性能的系統,往往處理邏輯也是復雜的,會有各樣的異步運算邏輯,因此,大量的日志不一定來自請求線程,更多的可能來自子線程、線程池線程,甚至嵌套在線程池線程下的線程。

如何將請求線程、子線程、線程池線程統一協調起來,實現 '采樣標識' 的跨線程透傳,從而確保請求維度采樣的一致性,是本文關註的重點,希望可以給大傢提供一些思路、並附上我們業務場景下的具體案例。

(1) 可以像 Transmittable Thread Local 一樣,通過對線程池類、或者任務類(Runnable、Callable)進行包裝,將透傳邏輯封裝起來,JD內部比較典型的用法有 pfinder、jade。

必須承認,使用這種方式實現是有一定優勢的,但是奈何現在各團隊都有自己獨特功能的線程池包裝類,並已經在業務系統中廣泛使用,如果此時再增加一個處理 '日志采樣' 的包裝,會出現嵌套包裝的情況,這樣會讓程序變得混亂,增加理解成本,由於各包裝實現之間缺乏磨合,甚至面臨不可控的風險,這樣做甚至有 '添亂' 的嫌疑,我認為至少在當前的環境下是不可取的。

(2)直接粗暴一些,在業務系統中,涉及子線程、線程池的地方,通過代碼 '顯式的' 將 '采樣標識' 不斷透傳。

這樣做是能達到目的的,但是,會有大量的邏輯代碼和業務耦合的情況(線程池越多,業務越復雜,耦合越嚴重),雖然單系統改造量不大(當然,這個也是因系統而異),但是輻射面太廣,需要各業務系統都要配合改造,同時有影響業務邏輯的風險,也不可取。

(3)將請求線程的控制邏輯跟具體業務解耦開來,封裝為一個組件,並在組件中提供合適的 api,將組件復用於各業務系統,並根據業務系統具體情況,來決策是否使用 api 來處理異步線程(子線程、線程池線程的統一簡稱)的情況。

在沒有特別合適的、集團級統一api的情況下,這是一種較為務實的做法,通過接入抽象組件來控制請求線程的日志采樣,並通過擴展 api 來協調請求線程和異步線程的采樣一致性,並根據業務系統的實際情況,來決策是否需要 '修改代碼' 來協調異步線程。

如果業務系統中使用異步線程的處理邏輯較少,隻接入組件,進行請求線程的日志采樣即可

如果業務系統中大量使用異步線程做邏輯,可接入組件再針對必要的地方通過擴展 api 來協調請求線程和異步線程的采樣一致性

除上述外,應該還有其他的辦法,比如 AOP 機制,但是 AOP 隻能到最低方法粒度,對於存量系統來說,改造量還是偏大,如果是新系統開發,可以考慮。

最後,上面的思考都是偏向全局的,在具體實現時,需要考慮、斟酌的細節還有很多、下面拋磚引玉列出一些,希望對大傢拓展思路有所幫助!

采樣的算法,是 '隨機' 還是 '對 traceId 取模',然後通過配置中心控制取樣比例

甚至跟入參關聯起來,對特定場景(對哪個接口、方法,按什麼規則進行入參篩選)進行采樣,即所謂場景化采樣

擴展 api 應該提供哪些功能,怎麼封裝能做到讓業務應用改動量最少?

怎麼保證全局(包括異步線程日志)請求 traceId 的一致性,這方面其實 pfinder 也有方案可供參考

采樣比例的最小粒度,是 百分之一、千分之一、還是萬分之一?

整系統使用一個采樣概率(即當前請求命中采樣時,各級別都打),還是各級別分別設置(分別設置時還要考慮聯動)

通常,我們傾向於,如果需要對當前請求進行采樣,可能 info、error 一塊打,也可能隻打 error,但是不太可能隻打 info 不打 error,所以需要有一個策略去控制

當大量 error 產生時,怎麼收斂日志?控制打印速率?甚至可以晉級一步,將磁盤IO 和打印速率聯動

三、實踐:

促銷交易這邊目前恰好在做這方面相關的技術改造,通過在目前既存日志組件(通過 JD JSF 前置過濾器實現)基礎上,實現請求線程的日志采樣,並提供擴展 api 給業務系統,實現整體上請求維度的日志采樣,從而盡可能的減少業務系統的改造,以下流程圖描述了大致落地過程,供參考,有興趣的可以評論區留言進一步探討具體落地細節。

涉及異步線程時,使用擴展 api 改造也相對簡潔:


// 改造前,線程池任務執行邏輯
threadPoolExecutor.execute(() -> "your business logic");
// 使用擴展 api 改造後,對 '線程池執行邏輯' 進行包裝,實現 '采樣標識' 的跨線程透傳
threadPoolExecutor.execute(XxxUtils.wrap(() -> "your business logic")); 

作者:京東零售 盧吉欣

來源:京東雲開發者社區 轉載請註明來源