Kafka的實現細節

Kafka的實現細節

一、Topic和Partition

在Kafka中的每一條消息都有一個topic。一般來說在我們應用中產生不同類型的數據,都可以設置不同的主題。一個主題一般會有多個消息的訂閱者,當生產者發佈消息到某個主題時,訂閱了這個主題的消費者都可以接收到生產者寫入的新消息。

  kafka爲每個主題維護了分佈式的分區(partition)日誌文件,每個partition在kafka存儲層面是append log。任何發佈到此partition的消息都會被追加到log文件的尾部,在分區中的每條消息都會按照時間順序分配到一個單調遞增的順序編號,也就是我們的offset,offset是一個long型的數字,我們通過這個offset可以確定一條在該partition下的唯一消息。在partition下面是保證了有序性,但是在topic下面沒有保證有序性。

 

 

 

在上圖中在我們的生產者會決定發送到哪個Partition。

  如果沒有Key值則進行輪詢發送。

  如果有Key值,對Key值進行Hash,然後對分區數量取餘,保證了同一個Key值的會被路由到同一個分區,如果想隊列的強順序一致性,可以讓所有的消息都設置爲同一個Key。

二、消費模型

消息由生產者發送到kafka集羣后,會被消費者消費。一般來說我們的消費模型有兩種:推送模型(psuh)和拉取模型(pull)

  基於推送模型的消息系統,由消息代理記錄消費狀態。消息代理將消息推送到消費者後,標記這條消息爲已經被消費,但是這種方式無法很好地保證消費的處理語義。比如當我們把已經把消息發送給消費者之後,由於消費進程掛掉或者由於網絡原因沒有收到這條消息,如果我們在消費代理將其標記爲已消費,這個消息就永久丟失了。如果我們利用生產者收到消息後回覆這種方法,消息代理需要記錄消費狀態,這種不可取。如果採用push,消息消費的速率就完全由消費代理控制,一旦消費者發生阻塞,就會出現問題。

  Kafka採取拉取模型(poll),由自己控制消費速度,以及消費的進度,消費者可以按照任意的偏移量進行消費。比如消費者可以消費已經消費過的消息進行重新處理,或者消費最近的消息等等。

三、網絡模型

3.1 KafkaClient --單線程Selector

 

 

 

單線程模式適用於併發鏈接數小,邏輯簡單,數據量小。

  在kafka中,consumer和producer都是使用的上面的單線程模式。這種模式不適合kafka的服務端,在服務端中請求處理過程比較複雜,會造成線程阻塞,一旦出現後續請求就會無法處理,會造成大量請求超時,引起雪崩。而在服務器中應該充分利用多線程來處理執行邏輯。

3.2 Kafka--server -- 多線程Selector

 

 

 

在kafka服務端採用的是多線程的Selector模型,Acceptor運行在一個單獨的線程中,對於讀取操作的線程池中的線程都會在selector註冊read事件,負責服務端讀取請求的邏輯。成功讀取後,將請求放入message queue共享隊列中。然後在寫線程池中,取出這個請求,對其進行邏輯處理,即使某個請求線程阻塞了,還有後續的縣城從消息隊列中獲取請求並進行處理,在寫線程中處理完邏輯處理,由於註冊了OP_WIRTE事件,所以還需要對其發送響應。

四、高可靠分佈式存儲模型

在Kafka中保證高可靠模型的依靠的是副本機制,有了副本機制之後,就算機器宕機也不會發生數據丟失。

4.1高性能的日誌存儲

kafka一個topic下面的所有消息都是以partition的方式分佈式的存儲在多個節點上。同時在kafka的機器上,每個Partition其實都會對應一個日誌目錄,在目錄下面會對應多個日誌分段(LogSegment)。LogSegment文件由兩部分組成,分別爲“.index”文件和“.log”文件,分別表示爲segment索引文件和數據文件。這兩個文件的命令規則爲:partition全局的第一個segment從0開始,後續每個segment文件名爲上一個segment文件最後一條消息的offset值,數值大小爲64位,20位數字字符長度,沒有數字用0填充,如下,假設有1000條消息,每個LogSegment大小爲100,下面展現了900-1000的索引和Log:

 

 

 

由於kafka消息數據太大,如果全部建立索引,即佔了空間又增加了耗時,所以kafka選擇了稀疏索引的方式,這樣的話索引可以直接進入內存,加快偏查詢速度。

  簡單介紹一下如何讀取數據,如果我們要讀取第911條數據首先第一步,找到他是屬於哪一段的,根據二分法查找到他屬於的文件,找到0000900.index和00000900.log之後,然後去index中去查找 (911-900) =11這個索引或者小於11最近的索引,在這裏通過二分法我們找到了索引是[10,1367]然後我們通過這條索引的物理位置1367,開始往後找,直到找到911條數據。

  上面講的是如果要找某個offset的流程,但是我們大多數時候並不需要查找某個offset,只需要按照順序讀即可,而在順序讀中,操作系統會對內存和磁盤之間添加page cahe,也就是我們平常見到的預讀操作,所以我們的順序讀操作時速度很快。但是kafka有個問題,如果分區過多,那麼日誌分段也會很多,寫的時候由於是批量寫,其實就會變成隨機寫了,隨機I/O這個時候對性能影響很大。所以一般來說Kafka不能有太多的partition。針對這一點,RocketMQ把所有的日誌都寫在一個文件裏面,就能變成順序寫,通過一定優化,讀也能接近於順序讀。

★★★可以思考一下:1.爲什麼需要分區,也就是說主題只有一個分區,難道不行嗎?2.日誌爲什麼需要分段

日誌策略

日誌保留策略

無論消費者是否已經消費了消息,kafka都會一直保存這些消息,但並不會像數據庫那樣長期保存。爲了避免磁盤被佔滿,kafka會配置響應的保留策略(retention policy),以實現週期性地刪除陳舊的消息 kafka有兩種“保留策略”:

  1. 根據消息保留的時間,當消息在kafka中保存的時間超過了指定時間,就可以被刪除;

  2. 根據topic存儲的數據大小,當topic所佔的日誌文件大小大於一個閥值,則可以開始刪除最舊的消息

日誌壓縮策略

在很多場景中,消息的key與value的值之間的對應關係是不斷變化的,就像數據庫中的數據會不斷被修改一樣,消費者只關心key對應的最新的value。我們可以開啓日誌壓縮功能,kafka定期將相同key的消息進行合併,只保留最新的value值

 

 

 

4.2 副本機制

Kafka的副本機制是多個服務端節點對其他節點的主題分區的日誌進行復制。當集羣中的某個節點出現故障,訪問故障節點的請求會被轉移到其他正常節點(這一過程通常叫Reblance),kafka每個主題的每個分區都有一個主副本以及0個或者多個副本,副本保持和主副本的數據同步,當主副本出故障時就會被替代。

 

 

 

在Kafka中並不是所有的副本都能被拿來替代主副本,所以在kafka的leader節點中維護着一個ISR(In sync Replicas)集合,翻譯過來也叫正在同步中集合,在這個集合中的需要滿足兩個條件:

  節點必須和ZK保持連接

  在同步的過程中這個副本不能落後主副本太多

  另外還有個AR(Assigned Replicas)用來標識副本的全集,OSR用來表示由於落後被剔除的副本集合,所以公式如下:ISR = leader + 沒有落後太多的副本; AR = OSR+ ISR;

  這裏先要說下兩個名詞:HW(high watermark)是consumer能夠看到的此partition的位置,LEO( log end offset)是每個partition的log最後一條Message的位置。HW能保證leader所在的broker失效,該消息仍然可以從新選舉的leader中獲取,不會造成消息丟失。

  當producer向leader發送數據時,可以通過request.required.acks參數來設置數據可靠性的級別:

  1(默認):這意味着producer在ISR中的leader已成功收到的數據並得到確認後發送下一條message。如果leader宕機了,則會丟失數據。

  0:這意味着producer無需等待來自broker的確認而繼續發送下一批消息。這種情況下數據傳輸效率最高,但是數據可靠性確是最低的。

  -1:producer需要等待ISR中的所有follower都確認接收到數據後纔算一次發送完成,可靠性最高。但是這樣也不能保證數據不丟失,比如當ISR中只有leader時(其他節點都和zk斷開連接,或者都沒追上),這樣就變成了acks=1的情況。

副本數據同步細節(HW和LEO)

 

 

 

4.3 數據操作

爲避免broker掛後造成數據丟失,kafka實現了高可用方式。

  • 基於partition實現Replica。並與zookeeper配合實現Leader的選舉。

  • 通過算法,將partition的Leader與Fellowers分散於不同的broker。

replica實現

在“brokers的物理結構”中,replication有多個follewers,分散於不同的brokers。通過增量日誌實現。

 

 

 

partition的log記錄是順序的,通過server.properties中log.retention.hours參數定義日誌保留時長,過期則刪除。新寫入的message append記錄在partition中。

爲提升效率,

  • follewers會在message未寫入log時,讀到message則將ACK發送給Leader,因此只能保證存在Replica,不能保證數據一定持久化了。

  • 批量複製

ISR(副本同步隊列)

ISR是In-Sync Replicate 記錄與Leader保持同步的列表。

維護的是有資格的follower節點

  1. 副本的所有節點都必須要和zookeeper保持連接狀態

  2. 副本的最後一條消息的offset和leader副本的最後一條消息的offset之間的差值不能超過指定的閥值,這個閥值是可以設置的(replica.lag.max.messages)

 

4.4 leader 選舉(Leader Election )

判斷Replica活着,(1)與zk有心跳通訊;(2)與Leader通訊及時。兩者有一不滿足,fellower都會從ISR中移除。

選舉算法

一般的leader選舉算法,有Majority Vote/Zab/Raft/PacificA。kafka採用的即PacificA,kafka維護多個ISR,但不不像Majorty Vote算法,限制最少的2N+1節點和N+1以上投票。

即使只有1個follewer,也可完成Leader選舉。

選舉過程(詳解)

 

 

五、Kafka的高吐量的因素

  1. 順序寫的方式存儲數據 ;

  2. 批量發送: 在異步發送模式中。kafka允許進行批量發送,也就是先講消息緩存到內存中,然後一次請求批量發送出去。這樣減少了磁盤頻繁io以及網絡IO造成的性能瓶頸 batch.size 每批次發送的數據大小 linger.ms 間隔時間

  3. 零拷貝:消息從發送到落地保存,broker維護的消息日誌本身就是文件目錄,每個文件都是二進制保存,生產者和消費者使用相同的格式來處理。在消費者獲取消息時,服務器先從硬盤讀取數據到內存,然後把內存中的數據原封不懂的通過socket發送給消費者。雖然這個操作描述起來很簡單,但實際上經歷了很多步驟

 

 

1、操作系統將數據從磁盤讀入到內核空間的頁緩存 2、應用程序將數據從內核空間讀入到用戶空間緩存中 3、應用程序將數據寫回到內核空間到socket緩存中 4、操作系統將數據從socket緩衝區複製到網卡緩衝區,以便將數據經網絡發出

 

 

通過“零拷貝”技術可以去掉這些沒必要的數據複製操作,同時也會減少上下文切換次數

 

// 通過多種方式操作Kafka的消息讀取

https://blog.csdn.net/u011784767/article/details/78663168

六、文件存儲機制

 

 

 

七、消息確認(確認offset)

自動提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
手動提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
​
 @Override
public void doWork() {
    consumer.subscribe(Arrays.asList(topic));
​
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
        for (ConsumerRecord<String, String> record : records) {
            System.out.println("partition=" + record.partition() + ",offset =  " + record.offset() + ", key = " + record.key() + ", value = " + record.value());
            this.msgList.add(record);
        }
​
        if (msgList.size() >= 5) {
            System.out.println("Execute commit Message....");
            // 手動提交offset
            consumer.commitAsync(); // 異步提交
            // consumer.commitSync(); //  同步提交
            // 消費完成,提交offset (原子)
            this.msgList.clear();
        }
    }
​
​
}

 

八、 Kafka 消息可靠性(offset)

1、Kafka 消息的問題

Kafka就比較適合高吞吐量並且允許少量數據丟失的場景,如果非要保證“消息只讀取一次”,可以使用JMS。

Kafka Producer 消息發送有兩種方式(配置參數 producer.type):

producer.type=sync(默認值): 後臺線程中消息發送是同步方式,對應的類爲 kafka.producer.SyncProducer; producer.type=async: 後臺線程中消息發送是異步方式,對應的類爲 kafka.producer.AyncProducer;優點是可批量發送消息(消息個數達到 batch.num.messages=200 或時間達到 “ 時發送)、吞吐量佳,缺點是發送不及時可能導致丟失; 對於同步方式(producer.type=sync)?Kafka Producer 消息發送有三種確認方式(配置參數 acks):

acks=0: producer 不等待 Leader 確認,只管發出即可;最可能丟失消息,適用於高吞吐可丟失的業務; acks=1(默認值): producer 等待 Leader 寫入本地日誌後就確認;之後 Leader 向 Followers 同步時,如果 Leader 宕機會導致消息沒同步而丟失,producer 卻依舊認爲成功; acks=all/-1: producer 等待 Leader 寫入本地日誌、而且 Leader 向 Followers 同步完成後纔會確認;最可靠。 Kafka Consumer 有兩個接口:

Low-level API: 消費者自己維護 offset 等值,可以完全控制; High-level API: 封裝了對 parition 和 offset 的管理,使用簡單;可能遇到 Consumer 取出消息並更新了 offset,但未處理消息即宕機,從而相當於消息丟失; Kafka 支持 3 種消息傳遞語義:

最多一次 -消息可能會丟失,但永遠不會重新發送。consumer.poll(); consumer.commitOffset(); processMsg(messages); 至少一次 -消息永遠不會丟失,但可能會重新傳遞。consumer.poll(); processMsg(messages); consumer.commitOffset(); 恰恰一次 - 這就是人們真正想要的,每條信息只傳遞一次。以事務來保證。

2 消息重複

根本原因:已經消費了數據,但是 offset 沒提交。 外在原因:(1)消費數據後、提交 offset 前,線程被殺; (2)設置 offset 爲自動提交,consumer.close() 之前 consumer.unsubscribe(); (3)consumer 取了一批數據,尚未處理完畢時,達到了 session.timeout.ms,導致沒有接收心跳而掛掉,自動提交offset失敗,下次會重複消費本批消息; 解決辦法:(1)唯一 ID 保存在外部介質中,每次消費時根據它判斷是否已處理; (2)如果在統計用,丟失幾條關係不大,則無需理會; (3)如果消費者來不及處理,可以這樣優化:增加分區以提高並行能力;增加消費者線程;關閉自動提交 enable.auto.commit=false

3 消息丟失

根本原因:已經提交了 offset,但數據在內存中尚未處理,線程就被殺掉。

消息丟失解決方案:

同步模式下,確認機制設置爲-1(不可爲1),即讓消息寫入Leader和Follower之後再確認消息發送成功; 異步模式下,設置爲不限制阻塞超時時間(不可爲acks=0),當緩衝區滿時不清空緩衝池,而是讓生產者一直處於阻塞狀態;

4 消息亂序 (如何保證kafka中消息按照順序消費)

傳統的隊列,在並行處理時,由於網絡故障或速度差異,儘管服務器傳遞是有序的,但消費者接收的順序可能不一致; Kafka 在主題內部有分區,並行處理時,每個分區僅由消費者組中的一個消費者使用,確保了消費者是該分區的唯一讀者,並按順序使用這些數據。

但是它也僅僅是保證Topic的一個分區順序處理,不能保證跨分區的消息先後處理順序,除非只提供一個分區。

九、Kafka的分區分配策略

partition.assignmentStrategy 指定分區策略

Range 範圍分區(默認的)

假如有10個分區,3個消費者,把分區按照序號排列0,1,2,3,4,5,6,7,8,9;消費者爲C1,C2,C3,那麼用分區數除以消費者數來決定每個Consumer消費幾個Partition,除不盡的前面幾個消費者將會多消費一個 最後分配結果如下

C1:0,1,2,3 C2:4,5,6 C3:7,8,9

如果有11個分區將會是:

C1:0,1,2,3 C2:4,5,6,7 C3:8,9,10

假如我們有兩個主題T1,T2,分別有10個分區,最後的分配結果將會是這樣:

C1:T1(0,1,2,3) T2(0,1,2,3) C2:T1(4,5,6) T2(4,5,6) C3:T1(7,8,9) T2(7,8,9)

在這種情況下,C1多消費了兩個分區

RoundRobin 輪詢分區

把所有的partition和consumer列出來,然後輪詢consumer和partition,儘可能的讓把partition均勻的分配給consumer

假如有3個Topic T0(三個分區P0-0,P0-1,P0-2),T1(兩個分區P1-0,P1-1),T2(四個分區P2-0,P2-1,P2-2,P2-3)

有三個消費者:C0(訂閱了T0,T1),C1(訂閱了T1,T2),C2(訂閱了T0,T2)!

那麼分區過程如下圖所示

 

 

分區將會按照一定的順序排列起來,消費者將會組成一個環狀的結構,然後開始輪詢。 P0-0分配給C0 P0-1分配給C1但是C1並沒訂閱T0,於是跳過C1把P0-1分配給C2, P0-2分配給C0 P1-0分配給C1, P1-1分配給C0, P2-0分配給C1, P2-1分配給C2, P2-2分配給C1, p2-3分配給C2

C0: P0-0,P0-2,P1-1 C1:P1-0,P2-0,P2-2 C2:P0-1,P2-1,P2-3

什麼時候觸發分區分配策略: 1.同一個Consumer Group內新增或減少Consumer

2.Topic分區發生變化

Rebalance的執行

kafka提供了一個角色Coordinator來執行。當Consumer Group的第一個Consumer啓動的時候,他會向kafka集羣中的任意一臺broker發送GroupCoordinatorRequest請求,broker會返回一個負載最小的broker設置爲coordinator,之後該group的所有成員都會和coordinator進行協調通信

整個Rebalance分爲兩個過程 jionGroup和sysncJion

joinGroup過程

在這一步中,所有的成員都會向coordinator發送JionGroup請求,請求內容包括group_id,member_id.protocol_metadata等,coordinator會從中選出一個consumer作爲leader,並且把組成員信息和訂閱消息,leader信息,rebanlance的版本信息發送給consumer

Synchronizing Group State階段

組成員向coordinator發送SysnGroupRequet請求,但是隻有leader會發送分區分配的方案(分區分配的方案其實是在消費者確定的),當coordinator收到leader發送的分區分配方案後,會通過SysnGroupResponse把方案同步到各個consumer中

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