Kafka——性能逆天的存在
0、引言
Kafka是LinkedIn開源出來的一款消息服務器,用scala語言實現;這貨的性能是百萬級的QPS(估計是掛載了多塊磁盤),我隨便寫個測試程序就是十萬級。
1、Kafka基本概念
在 Kafka中消息是按照Topic進行分類的; 每條發布到 Kafka集群的消息都有一個類別,這個類別被稱為Topic。(物理上不同Topic的消息分開存儲,邏輯上一個Topic的消息雖然保存于一個或多個broker上但用戶只需指定消息的Topic即可生產或消費數據而不必關心數據存于何處) 。
每個 Topic包含一個或多個Parition;Parition是物理存儲上的概念,創建Topic時可指定Parition數量。每個Parition對應一個存儲文件夾,文件夾下存儲該Parition所持有的消息數據和索引文件。Topic進行分區劃分的主要目的是出于性能方面的考慮,Kafka盡量的使所有分區均勻的分布到集群所有的節點上而不是集中在某些節點上,另外主從關系也盡量均衡,這樣每個節點都會擔任一定比例的分區的Leader。 每個 Parition是一個有序的隊列,每條消息在Parition中 擁有一個 offset。
消息的發布者可 將消息發布到指定的 Topic中,同時Producer也能決定將此消息發送到哪個Parition( 也可以采取隨機、哈希、輪訓等策略 )。
消息的消費者主動從 Kafka中拉取消息進行消費(pull模式),在Kafka中一個Parition中的消息可以被無限多個消費者進行消費,每個消費者之間是完全獨立,每個Consumer消費后的消息Kafka并不進行刪除操作,Kafka中的消息刪除是定期進行的,可以指定保留多長時間消息不被刪除。通過指定offset就可以消費任意位置的消息,當然前提是指定的offset是存在的。從這點上看Kafka更像是一個只能追加、不能修改、支持隨機讀取的小文件管理系統。
上面提到每個 Consumer是完全獨立,如果多個Consume想輪流消費同一個Topic的同一個Parition就做不到;后來Kafka發明了一個Consumer-group的概念,每個Consumer客戶端被創建時,會向Zookeeper注冊自己的信息;一個group中的多個Consumer可以交錯的消費一個Topic的所有Paritions;簡而言之,保證此Topic的所有Paritions都能被此group所消費,且消費時為了性能考慮,讓Parition相對均衡的分散到每個Consumer上,Consume-group之間是完全獨立。主人的相反是挺好的,但是悲劇的是客戶端基本都不支持,貌似只有java的客戶端支持比較好。
2、消息順序性與可靠性設計
發布到 Kafka的消息在一個Parition中是順序存儲的,發布者可以通過隨機、哈希、輪訓等方式發布到多個分區中,消費者通過指定offset進行消費;所以Kafka當中消息的順序性更多的取決于使用方如何使用。
Kafka系統中消息支持容災備份存儲,每個Parition有主分區、備用分區的概念,一個Topic中的多個Parition的主分區可能落在不同的物理機器上面,Kafka也是盡量讓其分布在不同的機器上以提高系統性能。消息的讀寫都是通過主分區直接完成,客戶端要直連主分區所在的物理機進行讀寫操作。備用分區就像一個"Consumer"消費主分區的消息并保存在本地日志中進行備份;主分區負責跟蹤所有的備用分區的狀態,如果備用分區"落后"太多或者失效,主分區將會把它從同步列表中刪除;主備分區的管理是通過zookeeper進行的。
發布時的可靠性取決于兩點:發送端的確認機制、以及 Kafka系統落地的策略 。發送端支持無確認、主分區確認 (主分區收到消息后發送確認回執)、以及主備分區確認(備用分區消息同步后主分區才發送確認回執)三種機制;Kafka系統落地的策略有兩種刷盤方式:通過配置消息數、以及配置刷盤時間間隔。
消費時的可靠性取決于消費者的讀取邏輯, Kafka是不保存消息的任何狀態的。At most once、 At least on c e 、 Exactly once 三種模式需要自己按照業務實現,最容易實現就是 At least on c e ,兩外兩種在分布式系統中都不可能做到完全的絕對實現,只能無限靠近,降低錯誤率。
3、消息存儲方式
Parition是以文件的形式存儲在 文件系統 中,比如創建了一個名為 tipocTest的Topic,其有4個Parition,在Kafka的數據目錄下面會有四個文件夾,按照Topic-partnum命名。
每個文件夾的內容
Parition中的每條Message由offset來表示它在這個Parition中的偏移量,這個offset不是該Message在Parition數據文件中的實際存儲位置,而是邏輯上一個值,它唯一確定了Parition中的一條Message。因此,可以認為offset是Parition中Message的id。Parition中的每條Message包含了三個屬性: Offset 、DataSize 、Data;Parition的數據文件則包含了若干條上述格式的Message,按offset由小到大排列在一起;Kafka收到新的消息后追加到文件末尾即可,所以消息的發布效率是很高的。
下面我們來思考另一個問題,如果一個 Parition只有一個數據文件會怎么樣? 新消息是添加在文件末尾,不論文件數據文件有多大,這個操作永遠都是O(1)。但是在讀取的時候根據offset查找Message是順序查找的,因此,如果數據文件很大的話,查找的效率就低。那么 Kafka 是如何解決查找效率的的問題呢? 1) 分段、2) 索引。
4、數據文件的分段與索引
Kafka 解決查詢效率的手段之一是將數據文件分段,可以配置每個數據文件的最大值,每段放在一個單獨的數據文件里面,數據文件以該段中最小的 offset命名。這樣在查找指定offset的Message的時候,用 二分查找 就可以定位到該 Message在哪個段中。
數據文件分段使得可以在一個較小的數據文件中查找對應 offset的Message了,但是這依然需要順序掃描才能找到對應offset的Message。為了進一步提高查找的效率, Kafka 為每個分段后的數據文件建立了 索引文件 ,文件名與數據文件的名字是一樣的,只是文件擴展名為 .index。 索引文件 中包含若干個索引條目,每個條目表示數據文件中一條 Message的索引——Offset與position(Message在數據文件中的絕對位置)的對應關系;index文件中并沒有為數據文件中的每條Message建立索引,而是采用了稀疏存儲的方式,每隔一定字節的數據建立一條索引。這樣避免了 索引文件 占用過多的空間,從而可以將 索引文件 保留在內存中。但缺點是沒有建立索引的 Message也不能一次定位到其在數據文件的位置,從而需要做一次順序掃描,但是這次順序掃描的范圍就很小了。
每個分段還有一個 .timeindex索引文件,這個文件的格式與.index文件格式一樣,所記錄的東西是消息發布時間與offset的稀疏索引,用于消息定期刪除使用。
下圖是一個分段索引的例子
這套機制是建立在 offset是有序的; 索引文件 被映射到內存中,所以查找的速度還是很快的。一句話, Kafka 的 Message存儲采用了分區(Parition)、分段(segment)和稀疏索引這幾個手段來達到高效發布和隨機讀取。
5、消費端設計
出于性能、容災方面的考慮,實際需求是有多 Consumer消費一個Topic的情況;由于多個Consumer之間是相互獨立的,可以采用競爭Parition上崗的方式進行消費,同一個時刻只有一個Consumer在消費一個Parition,多個Consumer之間定期同步offset狀態;如果是需要多通道消費,可以競爭不同的Parition對應資源上崗消費。
由于 Kafka是按照offset進行讀取的,一般的client都封裝成:給定一個起始offset后續不停的get就可以順序讀取了,沒有消費確認的概念,Kafka也不會維護每個消息、每個Consumer的狀態。其實實現一套消費確認機制也不難,這需要我們實現一個proxy層,在proxy層保留一個循環緩沖區,業務端消費確認后方可從緩沖區里面移除,如果一段時間沒有確認,下次來取的時候重復下發下去,類似于tcp滑動窗口的概念。
來自:http://blog.csdn.net/gdutliuyun827/article/details/53761108