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寫入消息的方式。
kafka寫入消息
在服務端,會生成指定的目錄文件進行數據落地

  • 目錄命名規則
    <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技術後就如同下圖:
    零拷貝技術2
    由於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零拷貝技術
  • 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裏面並且壓縮好準備發送。除此之外,使用者可以通過重寫SerializerPartitioner 去實現消息的指定序列化方式,以及消息發送給哪個Partitioner ,其中Serializer kafka默認提供了 StringSerializerStringDeserializer 等基本數據結構序列化器 實現序列化和反序列化,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 對其進行管理,GroupCoordinatorkafkaServer 中用於管理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 GroupSynchronizing Group State 階段。
當消費者找到GroupCoordinator之後,就會進入Join Group 階段,Consumer 首先向 GroupCoordinator 發送 JoinGropRequest 後會暫存消息,收集到全部消費者後,根據JoinGropRequest中的信息來確定Consumer Group 中可用的消費者,從中選取一個消費者成爲Group Leader ,同時還會指定分區策略,最後將這些信息封裝成功 JoinGroupResponse返回給消費者。
在這裏插入圖片描述
補充一下GroupCoordinator實際關係
在這裏插入圖片描述

總結

kafka吞吐量高的原因

1、生產者支持批量異步發送
2、消費者支持主動批量拉取消費,同時支持異步提交
3、Partition支持Topic的負載均衡(允許橫向拓展)
4、broker使用順序寫入與零拷貝技術

由於製作的時候信息來源多樣,再將知識點整合的時候難免會發生缺漏,如果有問題請及時指出,並且後續也會進一步完善這篇文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章