摘要:消息存儲對于每一款消息隊列都非常重要,那么Kafka在這方面是如何來設計做到高效的呢?
Kafka這款分布式消息隊列使用文件系統和操作系統的頁緩存(page cache)分別存儲和緩存消息,摒棄了Java的堆緩存機制,同時將隨機寫操作改為順序寫,再結合Zero-Copy的特性極大地改善了IO性能。而提起磁盤的文件系統,相信很多對硬盤存儲了解的同學都知道:“一塊SATA RAID-5陣列磁盤的線性寫速度可以達到幾百M/s,而隨機寫的速度只能是100多KB/s,線性寫的速度是隨機寫的上千倍”,由此可以看出對磁盤寫消息的速度快慢關鍵還是取決于我們的使用方法。鑒于此,Kafka的數據存儲設計是建立在對文件進行追加的基礎上實現的,因為是順序追加,通過O(1)的磁盤數據結構即可提供消息的持久化,并且這種結構對于即使是數以TB級別的消息存儲也能夠保持長時間的穩定性能。在理想情況下,只要磁盤空間足夠大就一直可以追加消息。此外,Kafka也能夠通過配置讓用戶自己決定已經落盤的持久化消息保存的時間,提供消息處理更為靈活的方式。本文將主要介紹Kafka中數據的存儲消息結構、存儲方式以及如何通過offset來查找消息等內容。
一、 Kafka中幾個重要概念介紹
(1)Broker:消息中間件處理節點,一個Kafka節點就是一個broker,一個或者多個Broker可以組成一個Kafka集群;
(2)Topic:主題是對一組消息的抽象分類,比如例如page view日志、click日志等都可以以topic的形式進行抽象劃分類別。在物理上,不同Topic的消息分開存儲,邏輯上一個Topic的消息雖然保存于一個或多個broker上但用戶只需指定消息的Topic即可使得數據的生產者或消費者不必關心數據存于何處;
(3)Partition:每個主題又被分成一個或者若干個分區(Partition)。每個分區在本地磁盤上對應一個文件夾,分區命名規則為主題名稱后接“—”連接符,之后再接分區編號,分區編號從0開始至分區總數減-1;
(4)LogSegment:每個分區又被劃分為多個日志分段(LogSegment)組成,日志段是Kafka日志對象分片的最小單位;LogSegment算是一個邏輯概念,對應一個具體的日志文件(“.log”的數據文件)和兩個索引文件(“.index”和“.timeindex”,分別表示偏移量索引文件和消息時間戳索引文件)組成;
(5)Offset:每個partition中都由一系列有序的、不可變的消息組成,這些消息被順序地追加到partition中。每個消息都有一個連續的序列號稱之為offset—偏移量,用于在partition內唯一標識消息(并不表示消息在磁盤上的物理位置);
(6)Message:消息是Kafka中存儲的最小最基本的單位,即為一個commit log,由一個固定長度的消息頭和一個可變長度的消息體組成;
二、 Kafka的日志結構與數據存儲
Kafka中的消息是以主題(Topic)為基本單位進行組織的,各個主題之間相互獨立。在這里主題只是一個邏輯上的抽象概念,而在實際數據文件的存儲中,Kafka中的消息存儲在物理上是以一個或多個分區(Partition)構成,每個分區對應本地磁盤上的一個文件夾,每個文件夾內包含了日志索引文件(“.index”和“.timeindex”)和日志數據文件(“.log”)兩部分。分區數量可以在創建主題時指定,也可以在創建Topic后進行修改。(ps:Topic的Partition數量只能增加而不能減少,這點內容超出本篇幅的減少范圍,大家可以先思考下)。
在Kafka中正是因為使用了分區(Partition)的設計模型,通過將主題(Topic)的消息打散到多個分區,并分布保存在不同的Kafka Broker節點上實現了消息處理的高吞吐量。其生產者和消費者都可以多線程地并行操作,而每個線程處理的是一個分區的數據。
同時,Kafka為了實現集群的高可用性,在每個Partition中可以設置有一個或者多個副本(Replica),分區的副本分布在不同的Broker節點上。同時,從副本中會選出一個副本作為Leader,Leader副本負責與客戶端進行讀寫操作。而其他副本作為Follower會從Leader副本上進行數據同步。
2.1Kafka中分區/副本的日志文件存儲分析
在三臺虛擬機上搭建完成Kafka的集群后(Kafka Broker節點數量為3個),通過在Kafka Broker節點的/bin下執行以下的命令即可創建主題和指定數量的分區以及副本:
./kafka-topics.sh --create --zookeeper 10.154.0.73:2181 --replication-factor 3 --partitions 3 --topic kafka-topic-01
創建完主題、分區和副本后可以查到出主題的狀態(該方式主要列舉了主題所有分區對應的副本以及ISR列表信息):
./kafka-topics.sh --describe --zookeeper 10.154.0.73:2181 --topic kafka-topic-01
Topic:kafka-topic-01 PartitionCount:3 ReplicationFactor:3 Configs:
Topic: kafka-topic-01 Partition: 0 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0
Topic: kafka-topic-01 Partition: 1 Leader: 2 Replicas: 2,0,1 Isr: 2,1,0
Topic: kafka-topic-01 Partition: 2 Leader: 0 Replicas: 0,1,2 Isr: 1,2,0
通過實現一個簡單的Kafka Producer的demo,即可完成生產者發送消息給Kafka Broker的功能。在使用Producer產生大量的消息后,可以看到部署集群的三臺虛擬機在Kafka的config/server.properties配置文件中“log.dirs”指定的日志數據存儲目錄下存在三個分區目錄,同時在每個分區目錄下存在很多對應的日志數據文件和日志索引文件文件,具體如下:
#1、分區目錄文件
drwxr-x--- 2 root root 4096 Jul 26 19:35 kafka-topic-01-0
drwxr-x--- 2 root root 4096 Jul 24 20:15 kafka-topic-01-1
drwxr-x--- 2 root root 4096 Jul 24 20:15 kafka-topic-01-2
#2、分區目錄中的日志數據文件和日志索引文件
-rw-r----- 1 root root 512K Jul 24 19:51 00000000000000000000.index
-rw-r----- 1 root root 1.0G Jul 24 19:51 00000000000000000000.log
-rw-r----- 1 root root 768K Jul 24 19:51 00000000000000000000.timeindex
-rw-r----- 1 root root 512K Jul 24 20:03 00000000000022372103.index
-rw-r----- 1 root root 1.0G Jul 24 20:03 00000000000022372103.log
-rw-r----- 1 root root 768K Jul 24 20:03 00000000000022372103.timeindex
-rw-r----- 1 root root 512K Jul 24 20:15 00000000000044744987.index
-rw-r----- 1 root root 1.0G Jul 24 20:15 00000000000044744987.log
-rw-r----- 1 root root 767K Jul 24 20:15 00000000000044744987.timeindex
-rw-r----- 1 root root 10M Jul 24 20:21 00000000000067117761.index
-rw-r----- 1 root root 511M Jul 24 20:21 00000000000067117761.log
-rw-r----- 1 root root 10M Jul 24 20:21 00000000000067117761.timeindex
由上面可以看出,每個分區在物理上對應一個文件夾,分區的命名規則為主題名后接“—”連接符,之后再接分區編號,分區編號從0開始,編號的最大值為分區總數減1。每個分區又有1至多個副本,分區的副本分布在集群的不同代理上,以提高可用性。從存儲的角度上來說,分區的每個副本在邏輯上可以抽象為一個日志(Log)對象,即分區副本與日志對象是相對應的。下圖是在三個Kafka Broker節點所組成的集群中分區的主/備份副本的物理分布情況圖:
2.2Kafka中日志索引和數據文件的存儲結構
在Kafka中,每個Log對象又可以劃分為多個LogSegment文件,每個LogSegment文件包括一個日志數據文件和兩個索引文件(偏移量索引文件和消息時間戳索引文件)。其中,每個LogSegment中的日志數據文件大小均相等(該日志數據文件的大小可以通過在Kafka Broker的config/server.properties配置文件的中的“log.segment.bytes”進行設置,默認為1G大?。?073741824字節),在順序寫入消息時如果超出該設定的閾值,將會創建一組新的日志數據和索引文件)。
Kafka將日志文件封裝成一個FileMessageSet對象,將偏移量索引文件和消息時間戳索引文件分別封裝成OffsetIndex和TimerIndex對象。Log和LogSegment均為邏輯概念,Log是對副本在Broker上存儲文件的抽象,而LogSegment是對副本存儲下每個日志分段的抽象,日志與索引文件才與磁盤上的物理存儲相對應;下圖為Kafka日志存儲結構中的對象之間的對應關系圖:
為了進一步查看“.index”偏移量索引文件、“.timeindex”時間戳索引文件和“.log”日志數據文件,可以執行下面的命令將二進制分段的索引和日志數據文件內容轉換為字符型文件:
# 1、執行下面命令即可將日志數據文件內容dump出來
./kafka-run-class.sh kafka.tools.DumpLogSegments --files /apps/svr/Kafka/kafkalogs/kafka-topic-01-0/00000000000022372103.log --print-data-log > 00000000000022372103_txt.log
#2、dump出來的具體日志數據內容
Dumping /apps/svr/Kafka/kafkalogs/kafka-topic-01-0/00000000000022372103.log
Starting offset: 22372103
offset: 22372103 position: 0 CreateTime: 1532433067157 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 5d2697c5-d04a-4018-941d-881ac72ed9fd
offset: 22372104 position: 0 CreateTime: 1532433067159 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 0ecaae7d-aba5-4dd5-90df-597c8b426b47
offset: 22372105 position: 0 CreateTime: 1532433067159 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 87709dd9-596b-4cf4-80fa-d1609d1f2087
......
......
offset: 22372444 position: 16365 CreateTime: 1532433067166 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 8d52ec65-88cf-4afd-adf1-e940ed9a8ff9
offset: 22372445 position: 16365 CreateTime: 1532433067168 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 5f5f6646-d0f5-4ad1-a257-4e3c38c74a92
offset: 22372446 position: 16365 CreateTime: 1532433067168 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 51dd1da4-053e-4507-9ef8-68ef09d18cca
offset: 22372447 position: 16365 CreateTime: 1532433067168 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 80d50a8e-0098-4748-8171-fd22d6af3c9b
......
......
offset: 22372785 position: 32730 CreateTime: 1532433067174 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: db80eb79-8250-42e2-ad26-1b6cfccb5c00
offset: 22372786 position: 32730 CreateTime: 1532433067176 isvalid: true keysize: 4 valuesize: 36 magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] key: 1 payload: 51d95ab0-ab0d-4530-b1d1-05eeb9a6ff00
......
......
#3、同樣地,dump出來的具體偏移量索引內容
Dumping /apps/svr/Kafka/kafkalogs/kafka-topic-01-0/00000000000022372103.index
offset: 22372444 position: 16365
offset: 22372785 position: 32730
offset: 22373467 position: 65460
offset: 22373808 position: 81825
offset: 22374149 position: 98190
offset: 22374490 position: 114555
......
......
#4、dump出來的時間戳索引文件內容
Dumping /apps/svr/Kafka/kafkalogs/kafka-topic-01-0/00000000000022372103.timeindex
timestamp: 1532433067174 offset: 22372784
timestamp: 1532433067191 offset: 22373466
timestamp: 1532433067206 offset: 22373807
timestamp: 1532433067214 offset: 22374148
timestamp: 1532433067222 offset: 22374489
timestamp: 1532433067230 offset: 22374830
......
......
由上面dump出來的偏移量索引文件和日志數據文件的具體內容可以分析出來,偏移量索引文件中存儲著大量的索引元數據,日志數據文件中存儲著大量消息結構中的各個字段內容和消息體本身的值。索引文件中的元數據postion字段指向對應日志數據文件中message的實際位置(即為物理偏移地址)。
下面的表格先列舉了Kakfa消息體結構中幾個主要字段的說明:
Kafka消息字段 | 各個字段說明 |
---|---|
offset | 消息偏移量 |
message size | 消息總長度 |
CRC32 | CRC32編碼校驗和 |
attributes | 表示為獨立版本、或標識壓縮類型、或編碼類型 |
magic | 表示本次發布Kafka服務程序協議版本號 |
key length | 消息Key的長度 |
key | 消息Key的實際數據 |
valuesize | 消息的實際數據長度 |
playload | 消息的實際數據 |
1.日志數據文件
Kafka將生產者發送給它的消息數據內容保存至日志數據文件中,該文件以該段的基準偏移量左補齊0命名,文件后綴為“.log”。分區中的每條message由offset來表示它在這個分區中的偏移量,這個offset并不是該Message在分區中實際存儲位置,而是邏輯上的一個值(Kafka中用8字節長度來記錄這個偏移量),但它卻唯一確定了分區中一條Message的邏輯位置,同一個分區下的消息偏移量按照順序遞增(這個可以類比下數據庫的自增主鍵)。另外,從dump出來的日志數據文件的字符值中可以看到消息體的各個字段的內容值。
2.偏移量索引文件
如果消息的消費者每次fetch都需要從1G大?。J值)的日志數據文件中來查找對應偏移量的消息,那么效率一定非常低,在定位到分段后還需要順序比對才能找到。Kafka在設計數據存儲時,為了提高查找消息的效率,故而為分段后的每個日志數據文件均使用稀疏索引的方式建立索引,這樣子既節省空間又能通過索引快速定位到日志數據文件中的消息內容。偏移量索引文件和數據文件一樣也同樣也以該段的基準偏移量左補齊0命名,文件后綴為“.index”。
從上面dump出來的偏移量索引內容可以看出,索引條目用于將偏移量映射成為消息在日志數據文件中的實際物理位置,每個索引條目由offset和position組成,每個索引條目可以唯一確定在各個分區數據文件的一條消息。其中,Kafka采用稀疏索引存儲的方式,每隔一定的字節數建立了一條索引,可以通過“index.interval.bytes”設置索引的跨度;
有了偏移量索引文件,通過它,Kafka就能夠根據指定的偏移量快速定位到消息的實際物理位置。具體的做法是,根據指定的偏移量,使用二分法查詢定位出該偏移量對應的消息所在的分段索引文件和日志數據文件。然后通過二分查找法,繼續查找出小于等于指定偏移量的最大偏移量,同時也得出了對應的position(實際物理位置),根據該物理位置在分段的日志數據文件中順序掃描查找偏移量與指定偏移量相等的消息。下面是Kafka中分段的日志數據文件和偏移量索引文件的對應映射關系圖(其中也說明了如何按照起始偏移量來定位到日志數據文件中的具體消息)。
3.時間戳索引文件
從上面一節的分區目錄中,我們還可以看到存在一些以“.timeindex”的時間戳索引文件。這種類型的索引文件是Kafka從0.10.1.1版本開始引入的的一個基于時間戳的索引文件,它們的命名方式與對應的日志數據文件和偏移量索引文件名基本一樣,唯一不同的就是后綴名。從上面dump出來的該種類型的時間戳索引文件的內容來看,每一條索引條目都對應了一個8字節長度的時間戳字段和一個4字節長度的偏移量字段,其中時間戳字段記錄的是該LogSegment到目前為止的最大時間戳,后面對應的偏移量即為此時插入新消息的偏移量。
另外,時間戳索引文件的時間戳類型與日志數據文件中的時間類型是一致的,索引條目中的時間戳值及偏移量與日志數據文件中對應的字段值相同(ps:Kafka也提供了通過時間戳索引來訪問消息的方法)。
三、 總結
從全文來看,Kafka高效數據存儲設計的特點在于以下幾點:
(1)、Kafka把主題中一個分區劃分成多個分段的小文件段,通過多個小文件段,就容易根據偏移量查找消息、定期清除和刪除已經消費完成的數據文件,減少磁盤容量的占用;
(2)、采用稀疏索引存儲的方式構建日志的偏移量索引文件,并將其映射至內存中,提高查找消息的效率,同時減少磁盤IO操作;
(3)、Kafka將消息追加的操作邏輯變成為日志數據文件的順序寫入,極大的提高了磁盤IO的性能;
任何一位使用Kafka的同學來說,如果能夠掌握其數據存儲機制,對于大規模Kafka集群的性能調優和問題定位都大有裨益。鑒于篇幅所限,對Kafka的日志管理器、日志加載/恢復和日志清理將在篇幅(二)中進行介紹。
限于筆者的才疏學淺,對本文內容可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。后續還會根據自己的實踐和研發,陸續發布關于Kafka分布式消息隊列的其他相關技術文章,敬請關注。