Kafka Partition儲存機制與高吞吐率原理
一、Topic定義與Partition儲存機制
Topic在邏輯上可以被認為是一個管道,每條生產/消費都必須指明Topic,也就是指定這條訊息應該在哪條管道進行傳輸。
為了使得Kafka的吞吐率可以線性提高,物理上把Topic分成一個或多個Partition,每個Partition在物理上對應一個資料夾,該資料夾下儲存這個Partition的所有訊息和索引檔案。
每個日誌檔案都是一個log entrie序列,每個log entrie包含一個4位元組整型數值(值為N+5),1個位元組的"magic value",4個位元組的CRC校驗碼,其後跟N個位元組的訊息體。每條訊息都有一個當前Partition下唯一的64位元組的offset,它指明瞭這條訊息的起始位置。
這個log entries並非由一個檔案構成,而是分成多個Segment,每個Segment以該Segment第一條訊息的offset命名並以“.kafka”為字尾。另外會有一個索引檔案,它標明瞭每個Segment下包含的log entry的offset範圍,如下圖所示:
每條訊息都被append到該Partition中,屬於順序寫磁碟,因此效率非常高。順序寫磁碟效率比隨機寫記憶體還要高,這是Kafka高吞吐率的一個很重要的保證。
對於傳統的訊息佇列而言,一般會刪除已經被消費的訊息,而Kafka叢集會保留所有的訊息,無論其被消費與否。當然,因為磁碟限制,不可能永久保留所有資料(實際上也沒必要),因此Kafka提供兩種策略刪除舊資料。一是基於時間,二是基於Partition檔案大小。
例如可以通過配置$KAFKA_HOME/config/server.properties,讓Kafka刪除一週前的資料,也可在Partition檔案超過1GB時刪除舊資料,配置如下所示:
log.retention.hours=168
log.segment.bytes=1073741824
log.retention.check.interval.ms=300000
log.cleaner.enable=false
這裡要注意,因為Kafka讀取特定訊息的時間複雜度為O(1),即與檔案大小無關,所以這裡刪除過期檔案與提高Kafka效能無關。選擇怎樣的刪除策略只與磁碟以及具體的需求有關。另外,Kafka會為每一個Consumer Group保留一些元資料資訊——當前消費的訊息的position,也即offset。這個offset由Consumer控制。正常情況下Consumer會在消費完一條訊息後遞增該offset。當然,Consumer也可將offset設成一個較小的值,重新消費一些訊息。因為offset由Consumer控制,所以Kafka Broker是無狀態的,它不需要標記哪些訊息被哪些消費過,也不需要通過Broker去保證同一個Consumer Group只有一個Consumer能消費某一條訊息,因此也就不需要鎖機制,這也為Kafka的高吞吐率提供了有力保障。
二、Producer端傳送機制
Producer傳送訊息到Broker時,會根據Paritition機制選擇將其儲存到哪一個Partition。如果Partition機制設定合理,所有訊息可以均勻分佈到不同的Partition裡,這樣就實現了負載均衡。如果一個Topic對應一個檔案,那這個檔案所在的機器I/O將會成為這個Topic的效能瓶頸,而有了Partition後,不同的訊息可以並行寫入不同Broker的不同Partition裡,極大的提高了吞吐率。
可以在$KAFKA_HOME/config/server.properties中通過配置項num.partitions來指定新建Topic的預設Partition數量,也可在建立Topic時通過引數指定,同時也可以在Topic建立之後通過Kafka提供的工具修改。
在傳送一條訊息時,可以指定這條訊息的key,Producer根據這個key和Partition機制來判斷應該將這條訊息傳送到哪個Parition。Paritition機制可以通過指定Producer的paritition.class這一引數來指定,該class必須實現kafka.producer.Partitioner介面。
(例如將key和partitions進行取餘,key為4,partitions為3,則這個key落在第2個partition上。可以通過控制key的值和partition的值一致,將key相同的訊息傳送到同一個partition上)
三、Consumer端消費機制
使用Consumer high level API時,同一Topic的一條訊息只能被同一個Consumer Group內的一個Consumer消費,但多個Consumer Group可同時消費這一訊息。
這是Kafka用來實現一個Topic訊息的廣播(發給所有的Consumer)和單播(發給某一個Consumer)的手段。一個Topic可以對應多個Consumer Group。如果需要實現廣播,只要每個Consumer有一個獨立的Group就可以了。要實現單播只要所有的Consumer在同一個Group裡。用Consumer Group還可以將Consumer進行自由的分組而不需要多次傳送訊息到不同的Topic。
實際上,Kafka的設計理念之一就是同時提供離線處理和實時處理。根據這一特性,可以使用Storm這種實時流處理系統對訊息進行實時線上處理,同時使用Hadoop這種批處理系統進行離線處理,還可以同時將資料實時備份到另一個數據中心,只需要保證這三個操作所使用的Consumer屬於不同的Consumer Group即可。
四、Delivery Guarantee
有這麼幾種可能的delivery guarantee:
1、At most once 訊息可能會丟,但絕不會重複傳輸。
2、At least one 訊息絕不會丟,但可能會重複傳輸。
3、Exactly once 每條訊息肯定會被傳輸一次且僅傳輸一次,很多時候這是使用者所想要的。
當Producer向Broker傳送訊息時,一旦這條訊息被commit,因Replication的存在,它就不會丟。但是如果Producer傳送資料給Broker後,遇到網路問題而造成通訊中斷,那Producer就無法判斷該條訊息是否已經commit。雖然Kafka無法確定網路故障期間發生了什麼,但是Producer可以生成一種類似於主鍵的東西,發生故障時冪等性的重試多次,這樣就做到了Exactly once。(僅針對Kafka consumer high level API)。
Consumer在從Broker讀取訊息後,可以選擇commit,該操作會在Zookeeper中儲存該Consumer在該Partition中讀取的訊息的offset。該Consumer下一次再讀該Partition時會從下一條開始讀取。如未commit,下一次讀取的開始位置會跟上一次commit之後的開始位置相同。當然可以將Consumer設定為autocommit,即Consumer一旦讀到資料立即自動commit。如果只討論這一讀取訊息的過程,那Kafka是確保了Exactly once。但實際使用中應用程式並非在Consumer讀取完資料就結束了,而是要進行進一步處理,而資料處理與commit的順序在很大程度上決定了訊息從Broker和Consumer的delivery guarantee semantic。
1、讀完訊息先commit再處理訊息。這種模式下,如果Consumer在commit後還沒來得及處理訊息就crash了,下次重新開始工作後就無法讀到剛剛已提交而未處理的訊息,這就對應於At most once
2、讀完訊息先處理再commit。這種模式下,如果在處理完訊息之後commit之前Consumer crash了,下次重新開始工作時還會處理剛剛未commit的訊息,實際上該訊息已經被處理過了。這就對應於At least once。在很多使用場景下,訊息都有一個主鍵,所以訊息的處理往往具有冪等性,即多次處理這一條訊息跟只處理一次是等效的,那就可以認為是Exactly once。
3、如果一定要做到Exactly once,就需要協調offset和實際操作的輸出。精典的做法是引入兩階段提交。如果能讓offset和操作輸入存在同一個地方,會更簡潔和通用。這種方式可能更好,因為許多輸出系統可能不支援兩階段提交。比如,Consumer拿到資料後可能把資料放到HDFS,如果把最新的offset和資料本身一起寫到HDFS,那就可以保證資料的輸出和offset的更新要麼都完成,要麼都不完成,間接實現Exactly once。(目前就high level API而言,offset是存於Zookeeper中的,無法存於HDFS,而low level API的offset是由自己去維護的,可以將之存於HDFS中)
總之,Kafka預設保證At least once,並且允許通過設定Producer非同步提交來實現At most once。而Exactly once要求與外部儲存系統協作,幸運的是Kafka提供的offset可以非常直接非常容易得使用這種方式。
歡迎工作一到五年的Java工程師朋友們加入Java程式設計師開發: 854393687
群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!