關(guān)于「日志采樣」的一些思考及實踐
一、背景
系統(tǒng)日志可用于追蹤用戶操作軌跡,異常情況下,合理的日志有助于快速排查、定位問題,毫無疑問,打印日志對于系統(tǒng)是很重要的。
當業(yè)務(wù)規(guī)模較小時,大家都傾向于享受日志帶來的便利,從而忽略日志帶來的潛在的負面影響,缺乏對日志的管控。在JD當前用戶量、業(yè)務(wù)規(guī)模下,絕大多數(shù)C端系統(tǒng)、甚至B端系統(tǒng)都是高吞吐的,毫無疑問,過大的日志量對系統(tǒng)的性能、磁盤IO有著顯著負面影響,趕上大促時,問題尤為突出。日志在為我們提供便利的同時,也無時無刻成為一根刺,時不時刺我們一下。
作為一個共性問題,由于集團暫沒推出統(tǒng)一的日志框架,不少團隊都會嘗試基于log4j、logback 進行輕度的封裝,通過跟配置中心聯(lián)動,增加一些諸如 '動態(tài)降級' 的功能、來緩解日志帶來的負面影響。降級帶來的效果是顯著的,但同時也讓系統(tǒng)喪失了記錄 '操作軌跡' 的能力,從而又帶來了新的問題。
此時,很容易想到,可以通過對 '請求' 采樣,實現(xiàn)請求日志的采樣輸出,并通過控制采樣比例平衡不同場景下日志對性能的影響,系統(tǒng)吞吐量較大時,降級采樣比例,系統(tǒng)吞吐量較低時,提高采樣比例。 促銷交易這邊目前恰好在做這方面相關(guān)的技術(shù)改造,通過在目前既存日志組件(通過 JD JSF 前置過濾器實現(xiàn))基礎(chǔ)上,實現(xiàn)請求線程的日志采樣,并提供擴展 api 給業(yè)務(wù)系統(tǒng),實現(xiàn)整體上請求維度的日志采樣,從而盡可能的減少業(yè)務(wù)系統(tǒng)的改造,以下流程圖描述了大致落地過程,供參考,有興趣的可以評論區(qū)留言進一步探討具體落地細節(jié)。
二、正文
在請求入口處,通過一定的采樣算法,計算當前請求是否采樣,并借助 Java 的 ThreadLocal 機制,可以實現(xiàn)當前請求線程的日志采樣。但是,大部分關(guān)注日志性能的系統(tǒng),往往處理邏輯也是復(fù)雜的,會有各樣的異步運算邏輯,因此,大量的日志不一定來自請求線程,更多的可能來自子線程、線程池線程,甚至嵌套在線程池線程下的線程。
如何將請求線程、子線程、線程池線程統(tǒng)一協(xié)調(diào)起來,實現(xiàn) '采樣標識' 的跨線程透傳,從而確保請求維度采樣的一致性,是本文關(guān)注的重點,希望可以給大家提供一些思路、并附上我們業(yè)務(wù)場景下的具體案例。
(1)可以像 Transmittable Thread Local 一樣,通過對線程池類、或者任務(wù)類(Runnable、Callable)進行包裝,將透傳邏輯封裝起來,JD內(nèi)部比較典型的用法有 pfinder、jade。
必須承認,使用這種方式實現(xiàn)是有一定優(yōu)勢的,但是奈何現(xiàn)在各團隊都有自己獨特功能的線程池包裝類,并已經(jīng)在業(yè)務(wù)系統(tǒng)中廣泛使用,如果此時再增加一個處理 '日志采樣' 的包裝,會出現(xiàn)嵌套包裝的情況,這樣會讓程序變得混亂,增加理解成本,由于各包裝實現(xiàn)之間缺乏磨合,甚至面臨不可控的風(fēng)險,這樣做甚至有 '添亂' 的嫌疑,我認為至少在當前的環(huán)境下是不可取的。
(2)直接粗暴一些,在業(yè)務(wù)系統(tǒng)中,涉及子線程、線程池的地方,通過代碼 '顯式的' 將 '采樣標識' 不斷透傳。
這樣做是能達到目的的,但是,會有大量的邏輯代碼和業(yè)務(wù)耦合的情況(線程池越多,業(yè)務(wù)越復(fù)雜,耦合越嚴重),雖然單系統(tǒng)改造量不大(當然,這個也是因系統(tǒng)而異),但是輻射面太廣,需要各業(yè)務(wù)系統(tǒng)都要配合改造,同時有影響業(yè)務(wù)邏輯的風(fēng)險,也不可取。
(3)將請求線程的控制邏輯跟具體業(yè)務(wù)解耦開來,封裝為一個組件,并在組件中提供合適的 api,將組件復(fù)用于各業(yè)務(wù)系統(tǒng),并根據(jù)業(yè)務(wù)系統(tǒng)具體情況,來決策是否使用 api 來處理異步線程(子線程、線程池線程的統(tǒng)一簡稱)的情況。
在沒有特別合適的、集團級統(tǒng)一api的情況下,這是一種較為務(wù)實的做法,通過接入抽象組件來控制請求線程的日志采樣,并通過擴展 api 來協(xié)調(diào)請求線程和異步線程的采樣一致性,并根據(jù)業(yè)務(wù)系統(tǒng)的實際情況,來決策是否需要 '修改代碼' 來協(xié)調(diào)異步線程。
① 如果業(yè)務(wù)系統(tǒng)中使用異步線程的處理邏輯較少,只接入組件,進行請求線程的日志采樣即可。
② 如果業(yè)務(wù)系統(tǒng)中大量使用異步線程做邏輯,可接入組件再針對必要的地方通過擴展 api 來協(xié)調(diào)請求線程和異步線程的采樣一致性。
除上述外,應(yīng)該還有其他的辦法,比如 AOP 機制,但是 AOP 只能到最低方法粒度,對于存量系統(tǒng)來說,改造量還是偏大,如果是新系統(tǒng)開發(fā),可以考慮。
最后,上面的思考都是偏向全局的,在具體實現(xiàn)時,需要考慮、斟酌的細節(jié)還有很多、下面拋磚引玉列出一些,希望對大家拓展思路有所幫助!
① 采樣的算法,是 '隨機' 還是 '對 traceId 取模',然后通過配置中心控制取樣比例。
甚至跟入?yún)㈥P(guān)聯(lián)起來,對特定場景(對哪個接口、方法,按什么規(guī)則進行入?yún)⒑Y選)進行采樣,即所謂場景化采樣。
② 擴展 api 應(yīng)該提供哪些功能,怎么封裝能做到讓業(yè)務(wù)應(yīng)用改動量最少?
③ 怎么保證全局(包括異步線程日志)請求 traceId 的一致性,這方面其實 pfinder 也有方案可供參考。
④ 采樣比例的最小粒度,是 百分之一、千分之一、還是萬分之一?
⑤ 整系統(tǒng)使用一個采樣概率(即當前請求命中采樣時,各級別都打),還是各級別分別設(shè)置(分別設(shè)置時還要考慮聯(lián)動)。
⑥ 通常,我們傾向于,如果需要對當前請求進行采樣,可能 info、error 一塊打,也可能只打 error,但是不太可能只打 info 不打 error,所以需要有一個策略去控制。
⑦ 當大量 error 產(chǎn)生時,怎么收斂日志?控制打印速率?甚至可以晉級一步,將磁盤IO 和打印速率聯(lián)動。
三、實踐
涉及異步線程時,使用擴展 api 改造也相對簡潔:
// 改造前,線程池任務(wù)執(zhí)行邏輯
threadPoolExecutor.execute(() -> "your business logic");
// 使用擴展 api 改造后,對 '線程池執(zhí)行邏輯' 進行包裝,實現(xiàn) '采樣標識' 的跨線程透傳
threadPoolExecutor.execute(XxxUtils.wrap(() -> "your business logic"));