自制單機(jī)日志解決方案 2. 日志索引

上一篇文章簡要介紹了一下 Talog 的核心原理,接下來將會對 Talog 的各個模塊進(jìn)行詳細(xì)的介紹,本文先來介紹一下 Talog 中的日志索引。
太長不看
本文的介紹可能會較為繁瑣、不好理解,因此先總結(jié)以下幾個要點(diǎn):
Talog 追求簡潔的 api 設(shè)計(jì),
newTalogger().CreateIndexer("index name")
?即可創(chuàng)建日志索引器Talog 能保證日志數(shù)據(jù)不會丟失,但是可能導(dǎo)致多次索引
Talog 默認(rèn)的 Indexer 只支持單行日志
使用 HeaderIndexer 來支持多行日志
單線程一秒鐘大致能索引 1100+ 條日志,多線程一秒鐘大致能索引 1400 條左右
V.Talog.Core 核心包的索引過程
為了讓 Talog 的 api 盡可能簡潔,我設(shè)計(jì)了一個 Talogger 類,該類包含所有使用者可能會用到的入口,只要知道 Talogger 類的存在,就可以了解到 Talog 提供的所有功能。比如想要索引日志,只需要初始化 Talogger 類,然后調(diào)用 CreateIndexer 即可,而不需要關(guān)心用于索引日志的類名是什么。以下為具體的代碼展示:
以上代碼中 Tag 方法以及 Data 方法都是在日志索引器(Indexer,后續(xù)為了方便主要使用該類名)內(nèi)部實(shí)現(xiàn)的,真正與日志索引存儲相關(guān)的是 Save 方法。調(diào)用 Save 時,Indexer 會將日志的標(biāo)簽列表以及日志文本發(fā)送給 Index 類,Index 的概念在上一篇文章中有提到,可以理解為是日志的邏輯集合,Index 對象由 Talogger 生成并傳遞給 Indexer,關(guān)于 Index 對象的初始化較為復(fù)雜后續(xù)會詳細(xì)介紹,在此我們先關(guān)注日志索引過程。Indexer 把日志數(shù)據(jù)發(fā)送給 Index 是通過調(diào)用 Push 接口,Index 的 Push 接口雖然無法保證日志數(shù)據(jù)立即索引成功,但是可以保證日志數(shù)據(jù)不會丟失,這主要是因?yàn)?Push 接口會先將日志數(shù)據(jù)保存在 Index 的未索引日志列表(unsavedLogs)中,并立即將 unsavedLogs 序列化到磁盤中。
在將日志數(shù)據(jù)保存到 unsavedLogs 之后,Index 會先根據(jù)日志的標(biāo)簽列表獲取對應(yīng)的 Bucket 對象,Bucket 的概念上一篇文章也有介紹過,就是用于存儲具有相同標(biāo)簽的日志數(shù)據(jù)的文件。
在 Index 獲取到 Bucket 對象之后,會調(diào)用 Append 接口,將日志文本內(nèi)容索引到磁盤中。到此,日志數(shù)據(jù)的索引就完成了一半,后續(xù)只需要在 Index 中對日志的元數(shù)據(jù)做維護(hù)即可。
元數(shù)據(jù)主要包括兩部分:Bucket 以及字典樹(Trie),Trie 的概念上一篇文章中也有提及,主要就是使用某一種標(biāo)簽的所有枚舉值去構(gòu)建一棵字典樹。對于 Bucket 的維護(hù)相對比較簡單,就是根據(jù) Bucket 的 key 判斷是否已存在于 Index 中(就是 Index 是否已經(jīng)知道 Bucket 的存在),若不存在則添加到 Bucket 列表中。對于 Trie 的維護(hù),就是遍歷日志的標(biāo)簽列表,根據(jù)標(biāo)簽名從 Index 的 Tries 字典中獲取對應(yīng)的字典樹(不存在的話,直接調(diào)用newTrie()
初始化),然后調(diào)用 Trie 的 Append 方法,將標(biāo)簽值以及日志存放的 Bucket 對象存到字典樹中。Trie.Append 會根據(jù)標(biāo)簽值找到對應(yīng)的節(jié)點(diǎn),并將 Bucket 存儲在 Bucket 字典中(根據(jù) Bucket.Key 去重)。
關(guān)于字典樹的用途以及結(jié)構(gòu),讀者可以自行搜索,該數(shù)據(jù)結(jié)構(gòu)我是在大三找暑假實(shí)習(xí)的時候,一位百度面試官跟我說的,不然大學(xué)課程并沒有教過該數(shù)據(jù)結(jié)構(gòu),在此感謝一下這位面試官,因?yàn)楫?dāng)時我的能力明顯不合格,但是面試官還耐心地跟我介紹了一下這個數(shù)據(jù)結(jié)構(gòu)。
至此,一個完整的 Talog 日志索引過程就介紹完畢了。
Index 的初始化
Index 的初始化主要依賴于使用者傳遞給 Talogger 的 index name,若 Index 是第一次初始化,會自動生成元數(shù)據(jù)文件,若不是第一次初始化,則會從磁盤中讀取元數(shù)據(jù)文件,還原之前所保存的狀態(tài),并且會自動對上次未索引完成的數(shù)據(jù)進(jìn)行再次索引(該機(jī)制有可能會導(dǎo)致日志數(shù)據(jù)多次索引,比如日志索引成功后,但是元數(shù)據(jù)還未保存成功時,程序意外中斷)。以下將對這兩種情況分別進(jìn)行詳細(xì)介紹。
假設(shè)首次運(yùn)行依賴 Talog 的程序并且初始化名為 test 的 Index,則首先會創(chuàng)建/data
目錄,該目錄名由 Talogger.Config.DataPath 指定,隨后會創(chuàng)建/data/test
目錄,該目錄用于存放 Index 的所有關(guān)聯(lián)文件,然后會初始化 Index.Tries、Index.Buckets、Index.unsavedLogs 三個屬性。
假設(shè)非首次初始化名為 test 的 Index,則會直接從/data/test/index.json
讀取元數(shù)據(jù),該文件反序列化后主要有兩部分?jǐn)?shù)據(jù):Index.Tries、Index.Buckets,Index.unsavedLogs 數(shù)據(jù)是單獨(dú)存放在/data/test/unsaved.json
文件中的,分成兩個文件存儲的主要原因是存儲時機(jī)不同。Index.Tries、Index.Buckets 的存儲發(fā)生在 Index.Save 方法被調(diào)用的時候,該方法主要是由 Talogger 的自動保存線程所調(diào)用。Index.unsavedLogs 數(shù)據(jù)的存儲是發(fā)生在日志索引時,前面有提到,Talog 的日志索引第一件事,就是將日志數(shù)據(jù)保存在 Index.unsavedLogs 中,同時將數(shù)據(jù)序列化到磁盤中。
HeaderIndexer
HeaderIndexer 類是在 V.Talog.Extension 包中實(shí)現(xiàn),主要是為了支持多行日志而設(shè)計(jì),但其實(shí)與 Indexer 的區(qū)別只在于 HeaderIndexer 會在每一條日志添加一個[{head}]
前綴,用于標(biāo)識新日志文本的開頭。以下為使用 Talogger 對象創(chuàng)建 HeaderIndexer 的擴(kuò)展方法簽名:
使用以下代碼來快速開始使用 HeaderIndexer:
JsonIndexer
JsonIndexer 本質(zhì)上與 Indexer 一致,因?yàn)檎?json 序列化都是采用壓縮格式,字符串只占一行,應(yīng)該不會有人不是采用壓縮格式做 json 序列化的吧,不會吧,不會吧。如果是那在 Talog 中應(yīng)該使用 HeaderIndexer 進(jìn)行索引。主要是為了與 JsonSearcher 相對應(yīng),所以特意創(chuàng)建了 JsonIndexer 類,該類在 Indexer 的基礎(chǔ)上僅增加了一個接口,用于支持傳入復(fù)雜日志對象。
索引后的日志文件結(jié)構(gòu)展示

除了存放日志的文件是使用 Bucket 的 key 命名的,其他文件結(jié)構(gòu)、文件內(nèi)容均是人類可讀的,這有利于了解 Talog 的運(yùn)行機(jī)制以及異常排查。
索引效率
以下為我在筆記本上做的不完全測試,因?yàn)橥瑫r還有其他程序在運(yùn)行,所以以下結(jié)果僅供參考。測試過程中的每一條日志平均為 80 字節(jié),CPU 為 Intel i7-1065G7 @ 1.30GHz 4 核 8 線程,硬盤為東芝 256G 固態(tài)硬盤。

單線程多線程索引 1W 條日志耗時 21s(476 條/s)耗時 17s(588 條/s)索引 5W 條日志耗時 1m53s(442 條/s)耗時 1m34s(531 條/s)索引 10W 條日志耗時 4m3s(411 條/s)耗時 3m11s(523 條/s)索引 20W 條日志耗時 7m44s(431 條/s)耗時 6m49s(488 條/s)
從以上數(shù)據(jù)可以看出單線程一秒鐘大致能索引 400+ 條日志,多線程一秒鐘大致能索引 500 條左右,從單線程變更到多線程,似乎索引速度并沒有得到太大的提升,這個可能是因?yàn)?Talog 的索引瓶頸在于文件的讀寫。這速度估計(jì)是根本無法和市面上的日志平臺相比的,但是對于 Talog 的定位(面向小微項(xiàng)目的單機(jī)日志解決方案)而言,這個索引效率應(yīng)該是已經(jīng)達(dá)到可以使用的水平了。
優(yōu)化索引速度
在測試了索引效率之后,并不是很滿意這個索引速度,并且在知道優(yōu)化方向的情況下,我果斷又花了點(diǎn)時間去優(yōu)化了下代碼。

并且換了一臺筆記本進(jìn)行測試,CPU 為 Intel i5-12450H 8 核 12 線程,硬盤為西部數(shù)據(jù) 512G 固態(tài)硬盤。

單線程多線程索引 1W 條日志耗時 8s(1250 條/s)耗時 5s(2000 條/s)索引 5W 條日志耗時 45s(1111 條/s)耗時 34s(1470 條/s)索引 10W 條日志耗時 1m30s(1111 條/s)耗時 1m8s(1470 條/s)索引 20W 條日志耗時 2m56s(1136 條/s)耗時 2m27s(1360 條/s)索引 30W 條日志耗時 4m27s(1123 條/s)耗時 3m29s(1435 條/s)
可以看到優(yōu)化后的效果比之前的版本好了許多,主要是通過使用 FileStream 代替 System.IO.File,并且使用讀寫鎖代替 lock 的方式來提高效率。目前單線程一秒鐘大致能索引 1100+ 條日志,多線程一秒鐘大致能索引 1400 條左右,這個速度對于小微項(xiàng)目絕對是達(dá)到可以用的水平了吧。
最后
本文介紹了 Talog 的日志索引過程的所有細(xì)節(jié),也許隨便一個開源項(xiàng)目的復(fù)雜度就能夠吊打 Talog,但是 Talog 設(shè)計(jì)之初就明確了目標(biāo)——小微項(xiàng)目、夠用就行。下一篇文章將會介紹一下 Talog 的日志查詢部分。