在對消息進行存儲和緩存時,Kafka依賴於文件系統。(Page Cache)
線性讀取和寫入是所有使用模式中最具可預計性的一種方式,因而操作系統采用預讀(read-ahead)和后寫(write-behind)技術對磁盤讀寫進行探測並優化后效果也不錯。預讀就是提前將一個比較大的磁盤塊中內容讀入內存,后寫是將一些較小的邏輯寫入操作合並起來組成比較大的物理寫入操作。
使用文件系統並依賴於頁面緩存(Page Cache)要優於自己在內存中維護一個緩存或者什么別的結構。
通過對所有空閑內存自動擁有訪問權,我們至少將可用的緩存大小翻了一倍,然后通過保存壓縮后的字節結構而非單個對象,緩存可用大小接着可能又翻了一倍。
這還大大簡化了代碼,因為對緩存和文件系統之間的一致性進行維護的所有邏輯現在都是在OS中實現的,這事OS做起來要比我們在進程中做那種一次性的緩存更加高效,准確性也更高。
如果你使用磁盤的方式更傾向於線性讀取操作,那么隨着每次磁盤讀取操作,預讀就能非常高效使用隨后准能用得着的數據填充緩存。
數據被傳輸到OS內核的頁面緩存中了,OS隨后會將這些數據刷新到磁盤的。此外我們添加了一條基於配置的刷新策略,允許用戶對把數據刷新到物理磁盤的頻率進行控制(每當接收到N條消息或者每過M秒),從而可以為系統硬件崩潰時“處於危險之中”的數據在量上加個上限。
——————————————————————————————————————————————————
【與BTree方式對比】
持久化隊列可以按照通常的日志解決方案的樣子構建,只是簡單的文件讀取和簡單地向文件中添加內容。
雖然這種結果必然無法支持BTree實現中的豐富語義,但有個優勢之處在於其所有的操作的復雜度都是O(1),讀取操作並不需要阻止寫入操作,而且反之亦然。
這樣做顯然有性能優勢,因為性能完全同數據大小之間脫離了關系 —— 一個服務器現在就能利用大量的廉價、低轉速、容量超過1TB的SATA驅動器。雖然這些驅動器尋道操作的性能很低,但這些驅動器在大量數據讀寫的情況下性能還湊和,而只需1/3的價格就能獲得3倍的容量。 能夠存取到幾乎無限大的磁盤空間而無須付出性能代價意味着,我們可以提供一些消息系統中並不常見的功能。例如,在Kafka中,消息在使用完后並沒有立即刪除,而是會將這些消息保存相當長的一段時間(比方說一周)。
——————————————————————————————————————————————————
Kafka的存儲布局非常簡單。話題的每個分區對應一個邏輯日志。物理上,一個日志為相同大小的一組分段文件。每次生產者發布消息到一個分區,代理就將消息追加到最后一個段文件中。當發布的消息數量達到設定值或者經過一定的時間后,段文件真正寫入磁盤中。寫入完成后,消息公開給消費者。
與傳統的消息系統不同,Kafka系統中存儲的消息沒有明確的消息Id。
消息通過日志中的邏輯偏移量來公開。這樣就避免了維護配套密集尋址,用於映射消息ID到實際消息地址的隨機存取索引結構的開銷。消息ID是增量的,但不連續。要計算下一消息的ID,可以在其邏輯偏移的基礎上加上當前消息的長度。
消費者始終從特定分區順序地獲取消息,如果消費者知道特定消息的偏移量,也就說明消費者已經消費了之前的所有消息。消費者向代理發出異步拉請求,准備字節緩沖區用於消費。每個異步拉請求都包含要消費的消息偏移量。Kafka利用sendfile API高效地從代理的日志段文件中分發字節給消費者。
——————————————————————————————————————————————————
————————————————————————————————————————————————
【Kafka高效文件存儲設計特點】
Kafka把topic中一個parition大文件分成多個小文件段,通過多個小文件段,就容易定期清除或刪除已經消費完文件,減少磁盤占用。
通過索引信息可以快速定位message和確定response的最大大小。
通過index元數據全部映射到memory,可以避免segment file的IO磁盤操作。
通過索引文件稀疏存儲,可以大幅降低index文件元數據占用空間大小。
————————————————————————————————————————————————
Partition:topic物理上的分組,一個topic可以分為多個partition,每個partition是一個有序的隊列。
Segment:partition物理上由多個segment組成,下面2.2和2.3有詳細說明。
offset:每個partition都由一系列有序的、不可變的消息組成,這些消息被連續的追加到partition中。partition中的每個消息都有一個連續的序列號叫做offset,用於partition唯一標識一條消息.
————————————————————————————————————————————————
【kafka文件存儲機制】
分析過程分為以下4個步驟:
topic中partition存儲分布
partiton中文件存儲方式
partiton中segment文件存儲結構
在partition中如何通過offset查找message
————————————————————————————————————————————————
partiton中文件存儲方式
每個partion(目錄)相當於一個巨型文件被平均分配到多個大小相等segment(段)數據文件中。但每個段segment file消息數量不一定相等,這種特性方便old segment file快速被刪除。
每個partiton只需要支持順序讀寫就行了,segment文件生命周期由服務端配置參數決定。
這樣做的好處就是能快速刪除無用文件,有效提高磁盤利用率。
————————————————————————————————————————————————
【partiton中segment文件存儲結構】
讀者從2.2節了解到Kafka文件系統partition存儲方式,本節深入分析partion中segment file組成和物理結構。
segment file組成:由2大部分組成,分別為index file和data file,此2個文件一一對應,成對出現,后綴".index"和“.log”分別表示為segment索引文件、數據文件.
segment文件命名規則:partion全局的第一個segment從0開始,后續每個segment文件名為上一個segment文件最后一條消息的offset值。數值最大為64位long大小,19位數字字符長度,沒有數字用0填充。
下面文件列表是筆者在Kafka broker上做的一個實驗,創建一個topicXXX包含1 partition,設置每個segment大小為500MB,並啟動producer向Kafka broker寫入大量數據,如下圖2所示segment文件列表形象說明了上述2個規則:
————————————————————————————————————————————————
2.4 在partition中如何通過offset查找message
例如讀取offset=368776的message,需要通過下面2個步驟查找。
第一步查找segment file
上述圖2為例,其中00000000000000000000.index表示最開始的文件,起始偏移量(offset)為0.第二個文件00000000000000368769.index的消息量起始偏移量為368770 = 368769 + 1.同樣,第三個文件00000000000000737337.index的起始偏移量為737338=737337 + 1,其他后續文件依次類推,以起始偏移量命名並排序這些文件,只要根據offset **二分查找**文件列表,就可以快速定位到具體文件。
當offset=368776時定位到00000000000000368769.index|log
第二步通過segment file查找message
通過第一步定位到segment file,當offset=368776時,依次定位到00000000000000368769.index的元數據物理位置和00000000000000368769.log的物理偏移地址,然后再通過00000000000000368769.log順序查找直到offset=368776為止。
從上述圖3可知這樣做的優點,segment index file采取稀疏索引存儲方式,它減少索引文件大小,通過mmap可以直接內存操作,稀疏索引為數據文件的每個對應message設置一個元數據指針,它比稠密索引節省了更多的存儲空間,但查找起來需要消耗更多的時間。
————————————————————————————————————————————————
從上述圖5可以看出,Kafka運行時很少有大量讀磁盤的操作,主要是定期批量寫磁盤操作,因此操作磁盤很高效。
這跟Kafka文件存儲中讀寫message的設計是息息相關的。Kafka中讀寫message有如下特點:
寫message
消息從java堆轉入page cache(即物理內存)。
由異步線程刷盤,消息從page cache刷入磁盤。
讀message
消息直接從page cache轉入socket發送出去。
當從page cache沒有找到相應數據時,此時會產生磁盤IO,從磁
盤Load消息到page cache,然后直接從socket發出去
————————————————————————————————————————————————