kafka技術內幕
前置概念理解
在開始介紹kafka細節之前,我們先對一些比較重要的概念做出解釋
ISR集合HW和LEO
ISR集合
ISR是In-Sync-Replica的簡寫, ISR集合中的的副本保持和leader的同步,當然leader本身也在ISR中。初始狀態所有的副本都處於ISR中,當一個消息發送給leader的時候,leader會等待ISR中所有的副本告訴它已經接收了這個消息(當Acks爲all的時候),如果一個副本落後太多,達到一個閾值,比如500個消息差異的時候,那麼它會被移除ISR。下一條消息來的時候,leader就會將消息發送給當前的ISR中節點了。當這個異常的副本恢復後,並且追上當前當前Leader的ISR閾值,它將會被重新納入到ISR集合中。
- 滿足ISR集合的兩個條件
1、副本所在的節點必須維持着與zookeeper的連接
2、副本最後一條消息的offset與Leader副本最後一條消息offset之前的差距不能超過閾值
HW&LEO
-
HW(High Watermark)與上面的ISR集合緊密相關。HW標記了一個特殊的offset,當消費者處理消息的時候,只能拉取到HW之前的消息,HW之後的消息對消費者不可見。與ISR類似HW由Leader管理,當ISR集合中全部Follower都拉取到HW指定消息進行同步後,Leader副本纔會遞增HW值。kafka官方之前稱呼HW爲commit,其含義是即使leader損壞,也不會出現數據丟失的情況 (HW可以理解爲當前ISR可見的最高消息水位) 。
-
LEO(Log End Offset)是所有的副本都會有的一個offset標記,它指向追加到當前副本的最後一個消息的offset。當生產者向Leader副本追加消息的時候,Leader副本的LEO標記會遞增,當Follower從Leader同步數據後Follower的LEO也會遞增
交付語義保證
kafka由於採用了offset的方式進行消息管理,所以生產者增加offset和消費者提交offset的時機就顯得格外重要
目前kafka支持語義有三種:
At most once:最多一次,消息可能會丟失但是絕不會重複消費
At least once:至少一次,消息不會丟失,但是可能會重複消費
Exactly once:精準一次,消息不會丟失,也不會重複消息(事務模式)
冪等性和事務
爲了滿足交付語義保證,kafka通過兩種方式去實現,一個是broker支持的消息冪等性,另一個是kafka集羣提供事務保障,冪等性是提供有限事務支持,當跨broker跨parition的時候冪等特性就會失效,但是事務不會。但是冪等性比事務能承受更高的TPS,一般來說冪等性接口是通過消息的key來實現的,每個消息的key都需要是唯一的。
服務端
kafka客戶端會與服務端的多個broker創建網絡連接,在這些網絡連接上流轉着各種請求及其響應,從而實現客戶端與服務端之間的交互。客戶端一般情況下不會碰到大數據量訪問、高併發的場景,所以客戶端使用NetworkClient組件管理這些網絡足矣。kafka服務端與客戶端運行場景不同,面對高併發,低延時的需要,kafka服務端使用Reactor模型進行網絡層管理。kafka客戶端的連接不但有來自client的連接也有來自Broker的連接。
服務的模型
- 圖二來源極客時間《kafka專欄》
kafka是最典型的高併發低延遲的模型,而在java裏面最成熟的莫過於 Netty 實現的 Reactor,所以從圖中可以看出kafka的服務端本身也是使用了Reactor的方式實現的。
圖中Acceptor單獨運行在一個線程中,用來處理客戶端發起的連接請求,Reader在Selector中註冊OP_READ事件,負責服務端讀取請求邏輯,也是一個線程處理多個Socket連接。Reader ThreadPool中的線程成功讀取請求後,將請求放入MessageQueue這個共享隊列中,Handler ThreadPool會從這個隊列中讀取出請求然後進行業務處理,這種模式下即便處理某個請求阻塞了,線程池也會有其它線程繼續從隊列中獲取請求繼續處理,從而避免了整個服務端的阻塞。當處理完成後,Handler負責發送響應給客戶端,這就需要Handler ThreadPool中的線程在Selector中註冊OP_WRITE事件,實現發送響應的功能。
除此之外MessageQueue還能提供緩存的功能,所以MessageQueue的隊列長度的設計就顯得格外的重要了
圖二中有一個叫做 Purgatory 的組件,叫做煉獄,這裏面也是一個隊列,專門用來保存那些同步發送的消息,當acks值是 all 的時候全部follower同步完成響應纔會從 Purgatory 進入到響應隊列裏面
(下圖是broker - topic - partition之間的關係)
kafka集羣很有特色,通過partition對topic進行分塊從而讓topic支持負載均衡,不同的partition分攤了同一個topic的壓力,但是也由於不同的partition導致消息只能在同一個partition中保證順序,不同的partition順序無法保證。所以當需要保證消息順序的情況下,生產者發送消息的時候選擇partition就顯得格外重要了 (當需要保證業務消息順序的時候,建議指定partition的方式發送,這樣可以保證需要保證順序的一類消息在同一個partition裏面) 。
向Zookeeper說不
在舊版的Kafka中,將元數據,consumer數據,producer數據都維護在zookeeper裏面通過Watcher實現,但是這樣會有兩個嚴重的問題:
-
1、羊羣效應
當一個被Watcher的Zookeeper節點發生變化,會導致大量的Watcher需要發送通知給客戶端,導致在通知期間發送網絡風暴,同時通知Consumer之後會直接導致Rebalance,就出現了羊羣效應。 -
2、腦裂
每個Consumer都是通過Zookeeper中保存的這些元數據判斷Consumer Group狀態、Broker的狀態以及Rebalance結果,由於Zookeeper只保證“最終一致性”,不保證"Simultaneously Consistent Cross-Client Views"(同時一致的跨客戶端視圖),不同Consumer在同一時刻可能連接到Zookeeper不同的集羣的服務器,看到的元數據可能就不一樣,這就會造成不正確的Rebalance。(簡而言之就是由於網絡異常,導致Zookeeper被分成了兩個規模差不多的集羣,並且兩個集羣同時對外提供服務,導致Zookeeper集羣上面的數據不一致,同時可能會發生集羣同時存在兩個控制器的場景) -
3、元數據壓力大
經常會有一些提交,例如consumer的offset或者HW和LEO的信息記錄在Zookeeper上面,這樣會大致Zookeeper承受着強大的壓力,當Zookeeper崩潰的時候所有的Kafka Topic都無法正常運行。
所以在新版的kafka中逐漸摒棄了對zookeeper的依賴,例如:新版的zookeeper Consumer Group 的 offset 不再記錄在 zookeeper上面,通過 _consumer_offsets 這個內部Topic去維護這類信息
kafka選舉
控制器選舉
控制器其實就算一個broker,只不過它除了具有一般broker的功能之外,還負責分區首領的選舉。集羣裏第一個啓動的broker通過在zookeeper搶佔/controller節點讓自己成爲控制器。其它broker在啓動也會嘗試創建這個節點,不過他們會收到一個節點已經創建的異常。然後意識到控制器節點已經存在,其它節點繼續watch這個節點,以便後續controller奔潰後搶佔。
副本Leader選舉
控制器的目的就是選舉partition的Leader,每個Topic會有多個partition進行負載均衡,每個partition會有多個副本保證數據高可用,既然有多個副本必然就會有Leader和Follower,Leader進行接收來自客戶端的請求,Follower負責同步Leader的數據,當Leader掛掉的時候,控制器一般情況下會在當前 ISR 集合的同步副本中選取一個follower當成leader。
日誌寫入以及零拷貝技術
kafka日誌寫入以及索引
kafka爲了提高消息的可靠,消息在寫入topic後會寫入到文件中,以保證消息的落地從而不會導致消息丟失的情況,如圖就是kafka每個partition寫入消息的方式。
在服務端,會生成指定的目錄文件進行數據落地
-
目錄命名規則
<topic名>-<partition_id>
-
文件的命名規則
日誌文件: [文件的第一個消息offset].log
索引文件: [文件的第一個消息offset].index (二進制)
索引文件: [文件的第一個消息offset].timeindex(二進制)
備份進度文件:leader-epoch-checkpoint
入下圖:
消息在寫入kafka後會在磁盤生成多個文件 【.log】文件和 【.index】 文件,其中爲了提高查詢消息效率每個日誌文件都會有一個索引文件,這個文件沒有爲每條消息建立起索引,而是使用稀疏索引的方式爲日誌文件建立起索引。
其中文件 leader-epoch-checkpoint 保存了每一任leader開始寫入消息時的offset 會定時更新,如果發生了leader變更,那麼新的 ISR 集羣的HW會基於這份文件來定義。前面三個文件,log index timeindex 是一起生成的,其中index 一個是順序索引,一個是時間索引。
- leader-epoch-checkpoint的內容
索引文件和日誌文件關聯
kafka 零拷貝技術
- 首先要聲明的一點kafka的零拷貝並不是由Java實現的,是通過Java調用底層操作系統的DMA(Direct Memory Access)方式實現的。那麼操作系統層面的零拷貝技術是如何實現的呢?
首先我們看看傳統意義的零拷貝技術是怎麼樣實現的吧,傳統的文件拷貝實現如下圖:
- 如果這個過程不是很理解可以看看如下的代碼:
String fileName = "aaa.file";
InputStream inputStream = new FileInputStream(fileName);
OutputStream outputStream = new DataOutputStream(socket.getOutputStream());
//用戶態緩衝空間
byte[] buffer = new byte[4096];
long read = 0, total = 0;
while ((read = inputStream.read(buffer)) >= 0) {
total = total + read;
outputStream.write(buffer);
}
-
從上面可以看到我們開闢了一個buffer用來從輸入流讀取數據,然後通過輸出流將緩衝區輸出,從中可以看出buffer就是那多餘的一步,那麼如果我們能直接控制,CPU將讀取的頁緩存發送給socket buffer 這樣就能大大提高效率,少了兩次拷貝。
-
於是使用了DMA技術後就如同下圖:
由於DMA是Linux提供的內核操作,那麼作爲Java只能使用內核提供的接口,在Java中涉及到DMA的就是 java.nio.channels.FileChannel中的transferTo方法 【注意不是:transferFrom】
String fileName = "aaa.file";
long fileSize = 123456789L , sendSize = 4096;
FileChannel fc = new FileInputStream(fileName).getChannel();
FileChannel fos = new FileOutputStream(dstPath).getChannel();
long nsent = 0,current =0;
current = fc.transferTo(0,fileSize,fos);
這裏代碼寫的比較粗糙但是不難看出其中的不同
- kafka中使用到DMA的位置:
- kafka雖然是使用Scala寫的,但是在源碼中,創始人還是大量使用JDK裏面 java 包實現的SDK,換句話來說,kafka其實是可以被改成java的畢竟都是JVM語言,正如RocketMQ很多地方都有借鑑kafka的思想,但是RocketMQ卻是完全使用Java實現的。
生產者
kafka在實際應用中,經常被用做高性能,高擴展的消息中間件。kafka目前定義了一套網絡協議,只要遵循這套協議格式,就可以向kafka發送消息,也可以從kafka拉取消息
生產者模型
- 執行順序:
1、ProducerInterceptors 對消息進行攔截
2、Serializer 對消息的key 和 value 進行序列化
3、Partitioner 爲消息選擇合適的 Partition
4、RecordAccumulator 收集消息,實現批量發送
5、Sender 從 RecordAccumulator 獲取消息
6、構造 ClientRequest
7、將ClientRequest 交給 NetworkClient,準備發送
8、NetworkClient 將請求放入KafkaChannel 的緩存(網絡請求緩存)
9、執行網絡I/O,發送請求
10、收到響應,調用ClientRequest 的回調函數
11、調用 RecordBatch 的回調函數,最終調用每個消息上面註冊的回調函數
這裏比較重要的應該就是RecordAccumulator,在kafka_client裏面RecordAccumulator使用ConcurrentMap維護着一個個發送緩衝區,Key是TopicPartition,value是Deque< RecordBatch > ,其中消息放在RecordBatch裏面並且壓縮好準備發送。除此之外,使用者可以通過重寫Serializer 和 Partitioner 去實現消息的指定序列化方式,以及消息發送給哪個Partitioner ,其中Serializer kafka默認提供了 StringSerializer 和 StringDeserializer 等基本數據結構序列化器 實現序列化和反序列化,Partitioner 在 kafka中也實現了 RoundRobinPartitioner 默認使用輪詢的方式發送給指定的partition
InFlightRequests 隊列主要作用是緩存了已經發出去但沒有收到響應的ClientRequest。其底層是通過一個Map<String,Deque< ClinentRequest >> 對象實現的,key是NodeId,value是發送到對應Node的ClientRequest對象集合。InFlightRequests提供了很多管理這個緩存隊列的方法,還通過配置參數,限制了每個連接最多緩存的ClientRequest個數。
- 這裏有一個細節,如果生產者的壓縮模式和broker的topic指定的壓縮模式不一致,broker會解壓後再重新壓縮
所以在配置的時候,儘量保持生產者和broker的壓縮模式一致,不然broker會耗費比較高的cpu去進行解壓重壓的操作。
同步和異步發送
同步發送,示例
由於這裏的示例我進行了封裝(使用ThreadLocal),所以大致瞭解一下流程就好了
通過調用.get(),等待服務器響應,那麼每次同步發送都會走完上面流程圖中的所有過程,並且等待respon響應
異步發送,示例
異步調用,將Message放入緩存區裏面,等待喚起Sender去執行發送
喚起方式:
1、多個或者第一個RecordBatch已滿
2、有其它線程等待緩衝區空間
3、顯示調用KafkaProducer的flush方法
4、Sender準備關閉
消費者
kafka_client不僅僅實現生產者KafkaProducer也實現了KafkaConsumer,通過KafkaConsumer使用者無需關心網絡協議等底層因素,通過API的方式讓開發者輕鬆使用Kafka API ,其中 KafkaConsumer 實現了 心跳機制,重試機制,網絡管理等操作。推薦使用java api提供的KafkaConsumer 舊版的Scala的consumer將會被逐漸廢棄。(此處圖片來源:https://www.cnblogs.com/huxi2b/p/7453543.html)
- HW圖解
消費者模型
從圖中可以看出來,同一個組的消費者的消費狀態會根據當前的Broker狀態進行動態變化,當消費者多於partition的時候可能會出現有消費者消費不到消息的情況,所以合理的配置消費者數量是非常有必要的,當然你自己也可以採取多線程消費的方式,不同的消費組之間是廣播的關係,但是經過我的測試kafka貌似會有一個主消費者組的概念(主消費者主能保證消息交付語義保證,其它消費者)
Rebalance
Rebalance 也叫做從平衡,爲了滿足上面的消費者模型必然會產生Rebalance 。在同一個Consumer Group中,同一個Topic的不同分區會分配給不同的消費者進行消費,那麼當這個Consumer Group 成員發生變化的時候,例如有consumer加入組會發起 JoinGropRequest,每個Consumer 對應消費的partition也會隨之發生改變,那麼就需要 Rebalance 的輔助
- Rebalance時機:
條件1:有新的 consumer 加入
條件2:舊的 consumer 掛了
條件3:coordinator 掛了,集羣選舉出新的 coordinator
條件4:topic 的 partition 增加
條件5:consumer 調用 unsubscrible(),取消topic的訂閱
方案一:
通過把同一consumer group的信息維護在zookeeper上面,然後註冊Watche去監聽各個consumer節點的狀態,當節點發生變化的時候Watche通知節點觸發Rebalance,重新分配消費模式。
方案二:
將全部Consumer Group 分成多個子集,每個Consumer Group 子集在 服務端 對應一個GroupCoordinator 對其進行管理,GroupCoordinator 是 kafkaServer 中用於管理Consumer Group 的組件,其具體內容在第4章中詳細介紹。消費者不再依賴Zookeeper。而只有GroupCoordinator 在Zookeeper上添加Watcher。消費者在加入或退出Consumer Group 時會修改Zookeeper中保存的元數據,這點與上下文描述的方案類似,此時會觸發GroupCoordinator 設置的Watcher通知GroupCoordinator 開始Rebalance 減少Watcher通知的量級。在Consumer加入Group也會先請求KafkaServer獲取相應GroupCoordinator 的位置
方案三:
在kafka 0.9版本後,對Rebalance重新設計了,將分區分配放到consumer這端進行依然使用 GroupCoordinator 處理。在之前的協議基礎上進行了修改,將 JoinGropRequest 拆分成了兩個階段,分別是 Join Group 和 Synchronizing Group State 階段。
當消費者找到GroupCoordinator之後,就會進入Join Group 階段,Consumer 首先向 GroupCoordinator 發送 JoinGropRequest 後會暫存消息,收集到全部消費者後,根據JoinGropRequest中的信息來確定Consumer Group 中可用的消費者,從中選取一個消費者成爲Group Leader ,同時還會指定分區策略,最後將這些信息封裝成功 JoinGroupResponse返回給消費者。
補充一下GroupCoordinator實際關係
總結
kafka吞吐量高的原因
1、生產者支持批量異步發送
2、消費者支持主動批量拉取消費,同時支持異步提交
3、Partition支持Topic的負載均衡(允許橫向拓展)
4、broker使用順序寫入與零拷貝技術
由於製作的時候信息來源多樣,再將知識點整合的時候難免會發生缺漏,如果有問題請及時指出,並且後續也會進一步完善這篇文章