我們NetCore下日志存儲設計


 

日志的分類

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

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

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

 

對於業務日志, 我們現在基本確定” 業務日志是業務” 這么個准則, 即業務日志應該跟隨着業務表走.

比如你一個訂單的操作日志, 那么訂單表再哪它就應該在哪, 業務日志應該要跟着你的業務操作同生共死(事務性)

 

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

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

而且參數日志擁有單位價值低, 但是總量卻及其龐大的特點, 也因為這個特點導致數據庫那邊的人(比如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方法,那么它就能為你所用了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM