我們NetCore下日誌存儲設計

日誌的分類

首先往大的來說,日誌分2種

①業務日誌: 即業務系統需要查看的日誌, 常見的比如誰什麼時候修改了什麼.

②參數日誌: 一般是開發人員遇到問題的時候定位用的, 一般不需要再業務系統里展示.

對於業務日誌, 我們現在基本確定」 業務日誌是業務」 這麼個準則, 即業務日誌應該跟隨著業務表走.

比如你一個訂單的操作日誌, 那麼訂單表再哪它就應該在哪, 業務日誌應該要跟著你的業務操作同生共死(事務性), 基於上述理念所以業務日誌我們不會用table存

對於參數日誌, 我覺得這個說是後端開發人員的撕逼生命線毫不為過, 但是同時由於參數日誌其實並不屬於業務的一部分(完全沒有這玩意,業務也是能跑的轉,業務系統也不會顯示這些資訊)

所以很多時候除開發人員之外的其它利益相關方其實並不在意是否有這個參數日誌, 甚至不少入門級開發人員也無法理解其重要性.

而且參數日誌擁有單位價值低, 但是總量卻及其龐大的特點, 也因為這個特點導致資料庫那邊的人(比如DBA)一般也挺抗拒這個的.

而我們用Table最主要就是解決參數日誌的問題.

日誌存儲體系設計理論

首先一個大原則是我們希望業務日誌和參數日誌是能串通起來, 比如你進行了這個業務操作並且有了這個業務日誌, 那麼我要能回溯到執行這次業務操作的相關參數.

常規的想法是參數日誌里存一個業務主鍵

但是訂單表的話你存訂單號, 用戶表的話你存用戶Id, 然後再來個別的業務又要存一個別的主鍵, 其實這挺不好擴展的, 然後參數日誌就會變得亂七八糟, 另外你就算存了訂單號你也沒法和業務日誌能直接的join出來(一般會匹配下2個日誌的操作時間人肉來看)

而我們用Application Insights來作為主力監控, 我們發現它能夠把一個請求/依賴項/異常等資訊串聯在一個列表裡  參見: 統一的跨組件事務診斷

image

我們就很好奇它是怎麼做到的,然後特地扒了一下它的SDK

發現在不同年代AppInsights通過不同的機制生產了一個在當前請求操作內的Id,然後用於各個操作之間進行關聯,分別是通過:

1.早期的AppInsights里(Net 4.6之前)是通過CallContext

2.Net 4.6以後是通過AsyncLocal

3.現在NetCore年代則通過Activity

然後它Id分3個,一個是Id自身,一個是ParentId,一個是RootId

這個屬於分散式追蹤的內容,裡面包含相對較多知識點這裡就不展開太多了,具體可以看Github上微軟對於Activity的用戶手冊里有詳細描述 Activity User Guide

AppInsights這個算是給了我較大的啟示,於是乎我就在想,如果我的業務日誌也存下它的那個Id,然後我的參數日誌也存這個Id

那麼我就擁有了一個和業務無關的統一關聯Id(而不是存各個業務表的業務主鍵),同時我甚至能實現類似它的那個「事務診斷」那樣的體驗,我通過一個業務日誌的數據能迅速關聯到我的參數日誌的記錄 

扯了那麼多,具體怎麼做

首先對於如何記錄參數日誌這件事,比較笨的辦法可能是如下這樣 

image

厲害點的人可能會把這個步驟放到Filter里

但是,拜託,都2021年了,我們來點稍微主流靠譜點的技術吧。

我們是使用了abp的,我覺得裡面的Audit(審計日誌)特性就蠻不錯,我們就是通過這個來記錄日誌。

參考文檔 審計日誌 

我們只需要重寫一下它的 IAuditingStore(裡面只有個SaveAsync方法)

然後在需要的地方打上[Audited]即可

image 

Abp的審計日誌本質是基於Castle的動態代理(DynamicProxy)來實現了AOP,然後它能獲取到一個方法調用的入參/出參/執行時間/異常資訊/方法名等各種資訊,我們只要重寫下告訴ABP怎麼存就可以了

所以記錄日誌的時候只需要打一個特性(而且和Filter不同的是我這個特性可以打在任何基於介面獲取的Public的方法里,而不局限於Controller里)

規範業務日誌表

為了配套參數日誌,我們也規範了業務日誌表的存儲。

業務日誌一般會有2種比較常見的存儲模式

①新值舊值得存儲

②完全拷貝修改前的記錄進行完全存儲


我採用的是模式②,我個人不太喜歡模式①,首先新老值存儲會帶來存儲量(存儲行數)暴漲的問題,另外新老值存儲我感覺很容易遺失一些數據的細節。

我這邊的業務日誌一般是: 原始業務表的數據Copy + 日誌創建時間 + 操作人 + 操作Id + (可選)操作類型(一般是一個枚舉)

至於怎麼Copy原始業務表,AutoMapper映射下不要太簡單

然後結合下上面的理論篇,我們這裡需要獲取到一個操作Id用於接下來和參數日誌關聯。

在現在Core的年代下直接用Activity即可 

image

在你請求進來的時候它默認就已經構造了,並且能確保當前請求內是唯一,Activity生成的Id也是符合opentelemetry規範的分散式追蹤Id

其他一些APM工具(比如我用的AppInsights)現在它內部的追蹤Id也都是基於這套來進行運作 

如何存參數日誌 

首先結合之前說到參數日誌的特點,量大,單位價值低

之前我們沒更好的存儲介質的時候也是直接存資料庫里,然後DBA就經常跟我們說這個太大了,要定期清理下,然後我們大概是3周一清 

如果一個問題潛伏3周以上對我們就是個麻煩事了 

而且也因此導致我們對存儲參數日誌也比較謹慎(稍微量上去了就會叫)

所以我們認清了如下幾個基本事實:

①關係型資料庫是屬於昂貴存儲,它應該存儲的是價值高的昂貴數據

②數據必須分層,高價值的和低價值的分開 

在結合下前面我們提到的基於一個分散式追蹤Id的日誌設計體系,所以還要提供不低於1個索引能力的查詢支援 

後面我們就用上了Azure Table Storage 

具體Table是什麼我之前有一篇文章有簡單介紹 Azure Table Storage 簡單介紹 

我們把分散式追蹤Id作為PartitionKey,其他abp里能提供的數據統統塞Table里

最後的程式碼大概是這樣 

image

裡面摺疊的那個FillAudit方法

image

經過上述設計,我們整個日誌現在基本就玩的比較轉,一旦有什麼問題,我們先查詢業務日誌,然後可以通過任意一條業務日誌在關聯到參數日誌定位到當時是什麼參數進來的,由此提升排查問題的速度 

Note:可能有些眼尖的人會發現我的Async的方法沒await,經過測試證明Table那邊的調用可以FireAndForgot的,而且基本上也不會丟數據,So這不是Bug或者疏忽,是故意的,反而那個Catch可能是一句無效程式碼

多說幾句

上面我重寫Audit的是每次來一個請求我就往Table存一條記錄,如果是面向高並發介面(比如查詢類的介面)

上面我所說的這個做法會讓你死得很慘 

正確做法應該是:

將數據先在本地記憶體快取一段時間後,當達到某個時間閾值或者數據量累計到一定程度再發送 

這個做法背後還是蠻複雜的,不過當年我們再琢磨這個東西的時候發覺我們用的AppInsights的SDK里也有這個玩意,我們直接拿出來稍微訂製了下後發覺還真能用。

有興趣可以看看appInsights相關的程式碼 傳送門 

你只要想辦法重寫下它的Send方法,那麼它就能為你所用了。