消息的存儲,消息的持久化
消息發送端發送消息到broker上以后,消息是如何持久化的呢?那么接下來去分析下消息的存儲首先我們需要了解的是,kafka是使用日志文件的方式來保存生產者和發送者的消息,每條消息都有一個offset值來表示它在分區中的偏移量。Kafka中存儲的一般都是海量的消息數據,為了避免日志文件過大,Log并不是直接對應在一個磁盤上的日志文件,而是對應磁盤上的一個目錄,這個目錄的命名規則是<topic_name>-<partition_id>
消息的文件存儲機制
一個topic的多個partition在物理磁盤上的保存路徑,路徑保存在 /tmp/kafka-logs/topic-partition,包含日志文件、索引文件和時間索引文件
kafka是通過分段的方式將Log分為多個LogSegment,LogSegment是一個邏輯上的概念,一個LogSegment對應磁盤上的一個日志文件和一個索引文件,其中日志文件是用來記錄消息的。索引文件是用來保存消息的索引。
segment的常用配置有:
#server.properties
#segment文件的大小,默認為 1G
log.segment.bytes=1024*1024*1024
#滾動生成新的segment文件的最大時長
log.roll.hours=24*7
#segment文件保留的最大時長,超時將被刪除
log.retention.hours=24*7
那么這個LogSegment是什么呢?
LogSegment
假設kafka以partition為最小存儲單位,那么我們可以想象當kafka producer不斷發送消息,必然會引起partition文件的無線擴張,這樣對于消息文件的維護以及被消費的消息的清理帶來非常大的挑戰,所以kafka 以segment為單位又把partition進行細分。每個partition相當于一個巨型文件被平均分配到多個大小相等的segment數據文件中(每個segment文件中的消息不一定相等),這種特性方便已經被消費的消息的清理,提高磁盤的利用率。
log.segment.bytes=107370 (設置分段大小),默認是1gb,我們把這個值調小以后,可以看到日志分段的效果
-
抽取其中3個分段來進行分析
imagesegment file由2大部分組成,分別為index file和data file,此2個文件一一對應,成對出現,后綴".index"和“.log”分別表示為segment索引文件、數據文件.
segment文件命名規則:partion全局的第一個segment從0開始,后續每個segment文件名為上一個segment文件最后一條消息的offset值進行遞增。數值最大為64位long大小,20位數字字符長度,沒有數字用0填充
查看segment文件命名規則
通過下面這條命令可以看到kafka消息日志的內容,注意grep必須加-a參數
grep -a 'logId' 00000000000000000000.log
假如第一個log文件的最后一個offset為:5376,所以下一個segment的文件命名為:
00000000000000005376.log。對應的index為00000000000000005376.index
segment中index和log的對應關系
從所有分段中,找一個分段進行分析
為了提高查找消息的性能,為每一個日志文件添加2個索引索引文件:OffsetIndex 和 TimeIndex,分別對應.index以及.timeindex, TimeIndex索引文件格式:它是映射時間戳和相對offset
查看索引內容:
sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.index --print-data-log
展示如下
offset: 4561 position: 683858
offset: 4566 position: 688769
offset: 4573 position: 693871
offset: 4578 position: 700261
offset: 4583 position: 704568
offset: 4586 position: 714114
offset: 4591 position: 720123
offset: 4594 position: 727926
offset: 4601 position: 733000
offset: 4603 position: 742220
offset: 4605 position: 753894
offset: 4607 position: 764212
offset: 4609 position: 771133
offset: 4614 position: 776029
offset: 4617 position: 780633
offset: 4622 position: 785519
offset: 4628 position: 796098
offset: 4633 position: 1198989
offset: 4637 position: 1204712
index采用稀疏存儲
的方式,它不會為每一條message都建立索引,而是每隔一定的字節數建立一條索引,避免索引文件占用過多的空間。缺點是沒有建立索引的offset不能一次定位到message的位置,需要做一次順序掃描,但是掃描的范圍很小。
如圖所示,.index文件中存儲了索引以及物理偏移量(position),.log文件存儲了消息的內容。
索引包含兩個部分(均為4個字節的數字),分別為相對offset和position。相對offset表示segment文件中的offset,其實就是消息的唯一標識,同一個partition內的消息offset是唯一的,position表示在消息在.log文件中在數據文件中的位置,其實是消息真實的物理偏移地址。
Kafka采用整數值position記錄單個分區的消費狀態
,當消費成功broker收到確認,position指向下一個offset。 由于消息一定時間內不清除,那么可以重置offset來重新消費消息。
在partition中如何通過offset查找message
查找的算法是
- 根據offset的值,查找segment段中的index索引文件。由于索引文件命名是以上一個文件的最后一個offset+1進行命名的,所以,使用二分查找算法能夠根據offset快速定位到指定的索引文件。
- 找到索引文件后,根據offset進行定位,找到索引文件中的符合范圍的索引。(kafka采用稀疏索引的方式來提高查找性能)
- 得到position以后,再到對應的log文件中,從position出開始查找offset對應的消息,將每條消息的offset與目標offset進行比較,直到找到消息
比如說,我們要查找offset=2490這條消息,那么先找到00000000000000000000.index, 然后找到[2487,49111]這個索引,再到log文件中,根據49111這個position開始查找,比較每條消息的offset是否大于等于2490。最后查找到對應的消息以后返回
Log文件的消息內容分析
sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.log --print-data-log | grep position
前面我們通過kafka提供的命令,可以查看二進制的日志文件信息,一條消息,會包含很多的字段。
offset: 5371 position: 102124 CreateTime: 1531477349286 isvalid: true keysize:
-1 valuesize: 12 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1
sequence: -1 isTransactional: false headerKeys: [] payload: message_5371
offset和position這兩個前面已經講過了、 createTime表示創建時間、keysize和valuesize表示key和value的大小、 compresscodec表示壓縮編碼、payload:表示消息的具體內容
kafka提供的命令的參數
sh kafka-run-class.sh kafka.tools.DumpLogSegments
Parse a log file and dump its contents to the console, useful for debugging a seemingly corrupt log segment.
Option Description
------ -----------
--deep-iteration if set, uses deep instead of shallow
iteration.
--files <String: file1, file2, ...> REQUIRED: The comma separated list of data
and index log files to be dumped.
--help Print usage information.
--index-sanity-check if set, just checks the index sanity
without printing its content. This is
the same check that is executed on
broker startup to determine if an index
needs rebuilding or not.
--key-decoder-class [String] if set, used to deserialize the keys. This
class should implement kafka.serializer.
Decoder trait. Custom jar should be
available in kafka/libs directory.
(default: kafka.serializer.StringDecoder)
--max-message-size <Integer: size> Size of largest message. (default: 5242880)
--offsets-decoder if set, log data will be parsed as offset
data from the __consumer_offsets topic.
--print-data-log if set, printing the messages content when
dumping data logs. Automatically set if
any decoder option is specified.
--transaction-log-decoder if set, log data will be parsed as
transaction metadata from the
__transaction_state topic.
--value-decoder-class [String] if set, used to deserialize the messages.
This class should implement kafka.
serializer.Decoder trait. Custom jar
should be available in kafka/libs
directory. (default: kafka.serializer.
StringDecoder)
--verify-index-only if set, just verify the index log without
printing its content.
日志的清除策略以及壓縮策略
日志清除策略
前面提到過,日志的分段存儲,一方面能夠減少單個文件內容的大小,另一方面,方便kafka進行日志清理。日志的清理策略有兩個:
- 根據消息的保留時間,當消息在kafka中保存的時間超過了指定的時間,就會觸發清理過程
- 根據topic存儲的數據大小,當topic所占的日志文件大小大于一定的閥值,則可以開始刪除最舊的消息。kafka會啟動一個后臺線程,定期檢查是否存在可以刪除的消息
通過log.retention.bytes和log.retention.hours這兩個參數來設置,當其中任意一個達到要求,都會執行刪除。
默認的保留時間是:7天
日志壓縮策略
Kafka還提供了“日志壓縮(Log Compaction)”功能,通過這個功能可以有效的減少日志文件的大小,緩解磁盤緊張的情況,在很多實際場景中,消息的key和value的值之間的對應關系是不斷變化的,就像數據庫中的數據會不斷被修改一樣,消費者只關心key對應的最新的value。因此,我們可以開啟kafka的日志壓縮功能,服務端會在后臺啟動啟動Cleaner線程池,定期將相同的key進行合并,只保留最新的value值。日志的壓縮原理是
磁盤存儲的性能問題
磁盤存儲的性能優化
我們現在大部分企業仍然用的是機械結構的磁盤,如果把消息以隨機的方式寫入到磁盤,那么磁盤首先要做的就是尋址,也就是定位到數據所在的物理地址,在磁盤上就要找到對應的柱面、磁頭以及對應的扇區;這個過程相對內存來說會消耗大量時間,為了規避隨機讀寫帶來的時間消耗,kafka采用順序寫的方式存儲數據。即使是這樣,但是頻繁的I/O操作仍然會造成磁盤的性能瓶頸
零拷貝
消息從發送到落地保存,broker維護的消息日志本身就是文件目錄,每個文件都是二進制保存,生產者和消費者使用相同的格式來處理。在消費者獲取消息時,服務器先從硬盤讀取數據到內存,然后把內存中的數據原封不動的通過socket發送給消費者。雖然這個操作描述起來很簡單,但實際上經歷了很多步驟。
操作系統將數據從磁盤讀入到內核空間的頁緩存:
? 應用程序將數據從內核空間讀入到用戶空間緩存中
? 應用程序將數據寫回到內核空間到socket緩存中
? 操作系統將數據從socket緩沖區復制到網卡緩沖區,以便將數據經網絡發出
通過“零拷貝”技術,可以去掉這些沒必要的數據復制操作,同時也會減少上下文切換次數。現代的unix操作系統提供一個優化的代碼路徑,用于將數據從頁緩存傳輸到socket;在Linux中,是通過sendfile系統調用來完成的。Java提供了訪問這個系統調用的方法:FileChannel.transferTo API
使用sendfile,只需要一次拷貝就行,允許操作系統將數據直接從頁緩存發送到網絡上。所以在這個優化的路徑中,只有最后一步將數據拷貝到網卡緩存中是需要的
頁緩存
頁緩存是操作系統實現的一種主要的磁盤緩存,但凡設計到緩存的,基本都是為了提升i/o性能,所以頁緩存是用來減少磁盤I/O操作的。
磁盤高速緩存有兩個重要因素:
第一,訪問磁盤的速度要遠低于訪問內存的速度,若從處理器L1和L2高速緩存訪問則速度更快。
第二,數據一旦被訪問,就很有可能短時間內再次訪問。正是由于基于訪問內存比磁盤快的多,所以磁盤的內存緩存將給系統存儲性能帶來質的飛越。
當 一 個進程準備讀取磁盤上的文件內容時, 操作系統會先查看待讀取的數據所在的頁(page)是否在頁緩存(pagecache)中,如果存在(命中)則直接返回數據, 從而避免了對物理磁盤的I/0操作;如果沒有命中, 則操作系統會向磁盤發起讀取請求并將讀取的數據頁存入頁緩存, 之后再將數據返回給進程。
同樣,如果 一 個進程需要將數據寫入磁盤, 那么操作系統也會檢測數據對應的頁是否在頁緩存中,如果不存在, 則會先在頁緩存中添加相應的頁, 最后將數據寫入對應的頁。 被修改過后的頁也就變成了臟頁, 操作系統會在合適的時間把臟頁中的數據寫入磁盤, 以保持數據的 一 致性
Kafka中大量使用了頁緩存, 這是Kafka實現高吞吐的重要因素之 一 。 雖然消息都是先被寫入頁緩存,然后由操作系統負責具體的刷盤任務的, 但在Kafka中同樣提供了同步刷盤及間斷性強制刷盤(fsync),可以通過 log.flush.interval.messages 和 log.flush.interval.ms 參數來控制。
同步刷盤能夠保證消息的可靠性,避免因為宕機導致頁緩存數據還未完成同步時造成的數據丟失。但是實際使用上,我們沒必要去考慮這樣的因素以及這種問題帶來的損失,消息可靠性可以由多副本來解決,同步刷盤會帶來性能的影響。 刷盤的操作由操作系統去完成即可