共計 3331 個字符,預計需要花費 9 分鐘才能閱讀完成。
Kafka 的 Log 存儲解析是怎樣的,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
引言
Kafka 中的 Message 是以 topic 為基本單位組織的,不同的 topic 之間是相互獨立的。每個 topic 又可以分成幾個不同的 partition(每個 topic 有幾個 partition 是在創建 topic 時指定 的),每個 partition 存儲一部分 Message。借用官方的一張圖,可以直觀地看到 topic 和 partition 的關系。
partition 是以文件的形式存儲在文件系統中,比如,創建了一個名 為 page_visits 的 topic,其有 5 個 partition,那么在 Kafka 的數據目錄中 (由配置文件中的 log.dirs 指定的) 中就有這樣 5 個目錄: page_visits-0,page_visits-1,page_visits-2,page_visits-3,page_visits-4,其命名規則 為 topic_name – partition_id,里面存儲的分別就是這 5 個 partition 的數據。
接下來,本文將分析 partition 目錄中的文件的存儲格式和相關的代碼所在的位置。
Partition 的數據文件
Partition 中的每條 Message 由 offset 來表示它在這個 partition 中的偏移量,這個 offset 不是該 Message 在 partition 數據文件中的實際存儲位置,而是邏輯上一個值,它唯一確定了 partition 中的一條 Message。因此,可以認為 offset 是 partition 中 Message 的 id。partition 中的每條 Message 包含了以下三個屬性:
offset
MessageSize
data
其中 offset 為 long 型,MessageSize 為 int32,表示 data 有多大,data 為 message 的具體內容。它的格式和 Kafka 通訊協議中介紹的 MessageSet 格式是一致。
Partition 的數據文件則包含了若干條上述格式的 Message,按 offset 由小到大排列在一起。它的實現類為 FileMessageSet,類圖如下:
它的主要方法如下:
append: 把給定的 ByteBufferMessageSet 中的 Message 寫入到這個數據文件中。
searchFor: 從指定的 startingPosition 開始搜索找到第一個 Message 其 offset 是大于或者等于指定的 offset,并返回其在文件中的位置 Position。它的實現方式是從 startingPosition 開始讀取 12 個字節,分別是當前 MessageSet 的 offset 和 size。如 果當前 offset 小于指定的 offset,那么將 position 向后移動 LogOverHead+MessageSize(其中 LogOverHead 為 offset+messagesize,為 12 個字節)。
read:準確名字應該是 slice,它截取其中一部分返回一個新的 FileMessageSet。它不保證截取的位置數據的完整性。
sizeInBytes: 表示這個 FileMessageSet 占有了多少字節的空間。
truncateTo: 把這個文件截斷,這個方法不保證截斷位置的 Message 的完整性。
readInto: 從指定的相對位置開始把文件的內容讀取到對應的 ByteBuffer 中。
我們來思考一下,如果一個 partition 只有一個數據文件會怎么樣?
新數據是添加在文件末尾(調用 FileMessageSet 的 append 方法),不論文件數據文件有多大,這個操作永遠都是 O(1)的。
查找某個 offset 的 Message(調用 FileMessageSet 的 searchFor 方法)是順序查找的。因此,如果數據文件很大的話,查找的效率就低。
那 Kafka 是如何解決查找效率的的問題呢?有兩大法寶:1) 分段 2) 索引。
數據文件的分段
Kafka 解決查詢效率的手段之一是將數據文件分段,比如有 100 條 Message,它們的 offset 是從 0 到 99。假設將數據文件分成 5 段,第一段為 0 -19,第二段為 20-39,以此類推,每段放在一個單獨的數據文 件里面,數據文件以該段中最小的 offset 命名。這樣在查找指定 offset 的 Message 的時候,用二分查找就可以定位到該 Message 在哪個段 中。
為數據文件建索引
數據文件分段使得可以在一個較小的數據文件中查找對應 offset 的 Message 了,但是這依然需要順序掃描才能找到對應 offset 的 Message。為了進一步提高查找的效率,Kafka 為每個分段后的數據文件建立 了索引文件,文件名與數據文件的名字是一樣的,只是文件擴展名為.index。
索引文件中包含若干個索引條目,每個條目表示數據文件中一條 Message 的索引。索引包含兩個部分(均為 4 個字節的數字),分別為相對 offset 和 position。
相對 offset:因為數據文件分段以后,每個數據文件的起始 offset 不為 0,相對 offset 表示這條 Message 相對于其所屬數據文件中最小的 offset 的大小。舉例,分段后的一個數據文件的 offset 是從 20 開始,那么 offset 為 25 的 Message 在 index 文件中的相對 offset 就是 25-20 = 5。存儲相對 offset 可以減小索引文件占用的空間。
position,表示該條 Message 在數據文件中的絕對位置。只要打開文件并移動文件指針到這個 position 就可以讀取對應的 Message 了。
index 文件中并沒有為數據文件中的每條 Message 建立索引,而是 采用了稀疏存儲的方式,每隔一定字節的數據建立一條索引。這樣避免了索引文件占用過多的空間,從而可以將索引文件保留在內存中。但缺點是沒有建立索引的 Message 也不能一次定位到其在數據文件的位置,從而需要做一次順序掃描,但是這次順序掃描的范圍就很小了。
在 Kafka 中,索引文件的實現類為 OffsetIndex,它的類圖如下:
主要的方法有:
append 方法,添加一對 offset 和 position 到 index 文件中,這里的 offset 將會被轉成相對的 offset。
lookup, 用二分查找的方式去查找小于或等于給定 offset 的最大的那個 offset
小結
我們以幾張圖來總結一下 Message 是如何在 Kafka 中存儲的,以及如何查找指定 offset 的 Message 的。
Message 是按照 topic 來組織,每個 topic 可以分成多個的 partition,比如:有 5 個 partition 的名為為 page_visits 的 topic 的目錄結構為:
partition 是分段的,每個段叫 LogSegment,包括了一個數據文件和一個索引文件,下圖是某個 partition 目錄下的文件:
可以看到,這個 partition 有 4 個 LogSegment。
借用博主 @lizhitao 博客上的一張圖來展示是如何查找 Message 的。
比如:要查找絕對 offset 為 7 的 Message:
首先是用二分查找確定它是在哪個 LogSegment 中,自然是在第一個 Segment 中。
打 開這個 Segment 的 index 文件,也是用二分查找找到 offset 小于或者等于指定 offset 的索引條目中最大的那個 offset。自然 offset 為 6 的那個索引是我們要找的,通過索引文件我們知道 offset 為 6 的 Message 在數據文件中的位置為 9807。
打開數據文件,從位置為 9807 的那個地方開始順序掃描直到找到 offset 為 7 的那條 Message。
這套機制是建立在 offset 是有序的。索引文件被映射到內存中,所以查找的速度還是很快的。
一句話,Kafka 的 Message 存儲采用了分區 (partition),分段(LogSegment) 和稀疏索引這幾個手段來達到了高效性。
關于 Kafka 的 Log 存儲解析是怎樣的問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注丸趣 TV 行業資訊頻道了解更多相關知識。