Kafka學習筆記(三)——架構深入

之前搭建好了Kafka的學習環境,瞭解了具體的配置文件內容,並且測試了生產者、消費者的控制檯使用方式,也學習了基本的API。那麼下一步,應該學習一下具體的內部流程~

1、Kafka的工作流程


大致的工作流程圖如下:

如圖所示哈,整個工作環境包括:一個生產者(producer),一個消費者組(含有三個消費者),一個主題:A,三個節點(broker),三個分區(partition)和兩個副本(副本數=leader數+follower數)。

分析一下大致工作流程:

  • Producer是消息的生產者,首先Producer從集羣中獲取分區的leader,完了以後producer將消息發送給leader,leader將消息寫入到本地文件。之後,follower要從leader這裏主動同步數據。
  • 圖中所示,每一個分區中都有消息的編號,稱爲偏移量(offset),它的作用是可以讓消費者追蹤消息在分區裏的位置。注意:這個偏移量不是全局的,而是分區獨立使用的。因此,Kafka只保證區內消息有序(生產順序和消費順序相同)。
  • Kafka中消息是以topic進行分類的,生產者生產消息,消費者消費消息,都是面向topic的。
  • topic是邏輯上的概念,而partition是物理上的概念,每個partition對應於一個log文件,該log文件中存儲的就是producer生產的數據。Producer生產的數據會被不斷追加到該log文件末端(log文件太大的時候會被切分,之後再說),且每條數據都有自己的offset。
  • 消費者組中的每個消費者,都會實時記錄自己消費到了哪個offset,出錯復活之後,從上次的位置繼續消費。

在網上找了一個圖,還挺通俗的:

2、Kafka的文件存儲機制


Producer 將數據寫入 Kafka 後,集羣就需要對數據進行保存了。Kafka 將數據保存在磁盤,可能在我們的一般的認知裏,寫入磁盤是比較耗時的操作,不適合這種高併發的組件。Kafka 初始會單獨開闢一塊磁盤空間,順序寫入數據(效率比隨機寫入高)。

2.1 Partition 結構

前面說過了每個 Topic 都可以分爲一個或多個 Partition,如果你覺得 Topic 比較抽象,那 Partition 就是比較具體的東西了!

Partition 在服務器上的表現形式就是一個一個的文件夾,由於生產者生產的消息會不斷追加到log文件末尾,爲防止log文件過大導致數據定位效率低下,Kafka採取了分片和索引機制,將每個partition分爲多個segment

每組 Segment 文件又包含 .index 文件、.log 文件、.timeindex 文件(早期版本中沒有)三個文件。

log和index文件位於一個文件夾下,該文件夾的命名規則爲:topic名稱+分區序號。例如,simon這個topic有2個分區,則其對應的文件夾爲simon-0,simon-1:

log 文件就是實際存儲 Message 的地方,而 indextimeindex 文件爲索引文件,用於檢索消息。

2.2 Message 結構

上面說到 log 文件就實際是存儲 Message 的地方,我們在 Producer 往 Kafka 寫入的也是一條一條的 Message。

那存儲在 log 中的 Message 是什麼樣子的呢?消息主要包含消息體、消息大小、Offset、壓縮類型……等等!

我們重點需要知道的是下面三個:

  • Offset:Offset 是一個佔 8byte 的有序 id 號,它可以唯一確定每條消息在 Parition 內的位置;
  • 消息大小:消息大小佔用 4byte,用於描述消息的大小;
  • 消息體:消息體存放的是實際的消息數據(被壓縮過),佔用的空間根據具體的消息而不一樣。

index和log文件以當前segment的第一條消息的offset命名,下圖爲index文件和log文件的結構示意圖:

例如:進入simon-0目錄下查看

2.3 存儲策略

無論消息是否被消費,Kafka 都會保存所有的消息。那對於舊數據有什麼刪除策略呢?

  • 基於時間,默認配置是 168 小時(7 天);
  • 基於大小,默認配置是 1073741824。

需要注意的是,Kafka 讀取特定消息的時間複雜度是 O(1),所以這裏刪除過期的文件並不會提高 Kafka 的性能!

3、Kafka的生產者


3.1 分區策略

1)爲什麼要分區呢 ?

  • 方便在集羣中擴展,每個Partition可以通過調整以適應它所在的機器,而一個topic又可以有多個Partition組成,因此整個集羣就可以適應任意大小的數據了;【提高負載能力】
  • 可以提高併發,因爲可以以Partition爲單位讀寫了

2)分區的原則
我們需要將producer發送的數據封裝成一個ProducerRecord對象。

(1)指明 partition 的情況下,直接將指明的值直接作爲 partiton 值;
(2)沒有指明 partition 值但有 key 的情況下,將 key 的 hash 值與 topic 的 partition 數進行取餘得到 partition 值;
(3)既沒有 partition 值又沒有 key 值的情況下,第一次調用時隨機生成一個整數(後面每次調用在這個整數上自增),將這個值與 topic 可用的 partition 總數取餘得到 partition 值,也就是常說的 round-robin 算法。

3.2 數據可靠性保證

爲保證producer發送的數據,能可靠的發送到指定的topic,topic的每個partition收到producer發送的數據後,都需要向producer發送ack(acknowledgement確認收到),如果producer收到ack,就會進行下一輪的發送,否則重新發送數據。

1)副本數據同步策略

方案 優點 缺點
半數以上完成同步,就發送ack 延遲低 選舉新的leader時,容忍n臺節點的故障,需要2n+1個副本
全部完成同步,才發送ack 選舉新的leader時,容忍n臺節點的故障,需要n+1個副本 延遲高

Kafka選擇了第二種方案,原因如下:

  • 同樣爲了容忍n臺節點的故障,第一種方案需要2n+1個副本,而第二種方案只需要n+1個副本,而Kafka的每個分區都有大量的數據,第一種方案會造成大量數據的冗餘。

  • 雖然第二種方案的網絡延遲會比較高,但網絡延遲對Kafka的影響較小。

留下一個疑問:既然是全部follower同步完成才發Ack,那麼如果有一個follower在同步之前掛了,豈不是永遠無法發送ack?哎,這就有了ISR機制~~

2)ISR

採用第二種方案之後,設想一下:leader收到數據,所有的follower都開始同步數據,但是有一個follower,因爲某種故障遲遲無法與leader同步,那麼leader就要一直等下去,直到它同步完成,才能發生ack,這樣的問題如何解決呢?

Leader維護了一個動態的in-sync replica set (ISR),意爲和leader保持同步的follower集合。當ISR中的follower完成數據的同步之後,leader就會給follower發送ack。如果follower長時間未向leader同步數據,則該follower將被踢出ISR,該時間閾值由replica.lag.time.max.ms參數設定。Leader發生故障之後,就會從ISR中選舉新的leader。

注意:在舊的版本中有兩種踢出策略,一個是follower同步消息過慢,另一種是follower和leader的數據量相差過大(默認是10000),新的版本僅保留了前者。原因是:producer發送數據的單位是batch,假如一個batch中包含有12000條數據的話,那麼ISR中的所有follower都會被踢出,待同步完成之後又會被加回來。如此往復幾次的話,就會造成大量的資源開銷,所以這個策略被棄用了。

3)ack應答機制

按照思路繼續往下走,當follower同步完成數據之後,leader返回給producer信號,通知它已經收到了,你可以繼續往下發數據。

但是,並不是所有的follower很快就能同步完成數據,對於某些不太重要的數據,對數據的可靠性要求不是很高,能夠容忍數據的少量丟失,所以沒必要等ISR中的follower全部接收成功。

所以Kafka爲用戶提供了三種可靠性級別,用戶根據對可靠性和延遲的要求進行權衡,選擇以下的配置。

acks參數配置:
acks:

  • 0【不需要確認,直接連發】:producer不等待broker的ack,這一操作提供了一個最低的延遲,broker一接收到還沒有寫入磁盤就已經返回,當broker故障時有可能丟失數據

  • 1【只等leader寫完】:producer等待broker的ack,partition的leader落盤成功後返回ack,如果在follower同步成功之前leader故障,那麼將會丟失數據

  • -1【等leader和follower都寫完】:producer等待broker的ack,partition的leader和ISR中的follower全部落盤成功後才返回ack。但是如果在follower同步完成後,broker發送ack之前,leader發生故障,那麼會造成數據重複。(但是如果ISR中只有一個leader,就和acks=1的效果一樣了)

4)故障處理細節

假設一個場景:有一個leader寫了10條數據,ISR中有F1(寫了8條數據),F2(寫了9條數據)。如果leader掛了,選擇了F1成爲新的leader,那麼問題就顯而易見了,F1和F2的數據不一致了。如果選擇F2當leader,但是原來的leader又詐屍了,活過來了,那麼數據又出現不一致了。這麼一來,消費者不得瘋了嘛。。。仁慈而又偉大的Kafka開發人員怎麼會想不到這個問題呢,且看:

如下圖:leader中現在有19條數據,F1和F2分別有12、15條數據,某一時刻,出故障了~

注意:LEO:指每個副本最大的offset
HW:指的是消費者能見到的最大的offset,ISR中最小的LEO [保證消費者消費的一致性]

① follower故障:
follower發生故障後會被臨時踢出ISR,待該follower恢復後,follower會讀取本地磁盤記錄的上次的HW,並將log文件高於HW的部分截取掉(過長,就把它切掉~),從HW開始向leader進行同步。等該follower的LEO大於等於該Partition的HW,即follower追上leader之後,就可以重新加入ISR了。

② leader故障
leader發生故障之後,會從ISR中選出一個新的leader。之後,爲保證多個副本之間的數據一致性,其餘的follower會先將各自的log文件高於HW的部分截掉,然後從新的leader同步數據。(老大通知小弟,把個人主義切掉,再把比我少的補上~)

注意:不要搞混,這隻能保證副本之間的數據一致性,ack機制才能保證數據不丟失或者不重複。

3.3 Exactly Once語義

將服務器的 ACK 級別設置爲-1,可以保證 Producer 到 Server 之間不會丟失數據,但是有可能重複數據,即 At Least Once 語義。相對的,將服務器 ACK 級別設置爲 0,可以保證生產者每條消息只會被髮送一次,即 At Most Once 語義。

At Least Once 可以保證數據不丟失,但是不能保證數據不重複;相對的,At Least Once可以保證數據不重複,但是不能保證數據不丟失。但是,對於一些非常重要的信息,比如說交易數據,下游數據消費者要求數據既不重複也不丟失,即 Exactly Once 語義。在 0.11 版本以前的 Kafka,對此是無能爲力的,只能保證數據不丟失,再在下游消費者對數據做全局去重。對於多個下游應用的情況,每個都需要單獨做全局去重,這就對性能造成了很大影響。

0.11 版本的 Kafka,引入了一項重大特性:冪等性。所謂的冪等性就是指 Producer 不論向 Server 發送多少次重複數據,Server 端都只會持久化一條。冪等性結合 At Least Once 語義,就構成了 Kafka 的 Exactly Once 語義。即:
At Least Once + 冪等性 = Exactly Once

要啓用冪等性,只需要將 Producer 的參數中 enable.idompotence 設置爲 true 即可。Kafka的冪等性實現其實就是將原來下游需要做的去重放在了數據上游。開啓冪等性的 Producer 在初始化的時候會被分配一個 PID,發往同一 Partition 的消息會附帶 Sequence Number。而Broker 端會對<PID, Partition, SeqNumber>做緩存,當具有相同主鍵的消息提交時,Broker 只會持久化一條。但是 PID 重啓就會變化,同時不同的 Partition 也具有不同主鍵,所以冪等性無法保證跨分區跨會話的 Exactly Once。

4、Kafka消費者


4.1 消費方式

consumer採用pull(拉)模式從broker中讀取數據。

push(推)模式很難適應消費速率不同的消費者,因爲消息發送速率是由broker決定的。它的目標是儘可能以最快速度傳遞消息,但是這樣很容易造成consumer來不及處理消息,典型的表現就是拒絕服務以及網絡擁塞。而pull模式則可以根據consumer的消費能力以適當的速率消費消息。

pull模式不足之處是,如果kafka沒有數據,消費者可能會陷入循環中,一直返回空數據。針對這一點,Kafka的消費者在消費數據時會傳入一個時長參數timeout,如果當前沒有數據可供消費,consumer會等待一段時間之後再返回,這段時長即爲timeout。

4.2 分區分配策略

一個consumer group中有多個consumer,一個 topic有多個partition,所以必然會涉及到partition的分配問題,即確定那個partition由哪個consumer來消費。同一組裏的不同消費者,不能消費同一分區。
Kafka有兩種分配策略,RoundRobin和Range。

  • RoundRobin模式:按組劃分

  • Range模式:按主題劃分

什麼時候會觸發這些策略呢?一句話:當消費者組裏的消費者個數發送變化的時候會重新分配。

4.3 offset的維護

由於consumer在消費過程中可能會出現斷電宕機等故障,consumer恢復後,需要從故障前的位置的繼續消費,所以consumer需要實時記錄自己消費到了哪個offset,以便故障恢復後繼續消費。

假設有這麼一個場景:一個消費者組裏有消費者A,他消費着Topic t的三個分區t0、t1、t2分區裏的數據,並且都消費到了0ffset=10。此時,消費者組裏加入了消費者B,那麼就會觸發分區分配策略。假設分區t2被分給了B。那麼,B應該從哪裏開始消費呢?是從頭還是從offset=10 ? --很明顯哈,是從offset=10接着消費。如果是從頭消費呢,一旦擴充消費者組,每次都從頭消費,造成了多次重複消費相同的數據。注意:消費者組 + 主題 + 分區 = 唯一確定一個offset !!!

Kafka 0.9版本之前,consumer默認將offset保存在Zookeeper中,從0.9版本開始,consumer默認將offset保存在Kafka一個內置的topic中,該Topic爲__consumer_offsets.

Kafka的信息將被保存在zookeeper中,詳細存儲情況如下圖:

5、Kafka高效讀寫數據

5.1 順序讀寫

Kafka的producer生產數據,要寫入到log文件中,寫的過程是一直追加到文件末端,爲順序寫。官網有數據表明,同樣的磁盤,順序寫能到到600M/s,而隨機寫只有100k/s。這與磁盤的機械機構有關,順序寫之所以快,是因爲其省去了大量磁頭尋址的時間。

5.2 零拷貝技術

零拷貝技術,可以有效的減少上下文切換和拷貝次數。kafka的設計實現,涉及到很多的底層技術,若希望能夠把它吃透,需要花大量的時間,大量的精力。現在還是小菜雞一名,今後任重道遠 ~

5.3 Page Cache

爲了優化讀寫性能,Kafka利用了操作系統本身的Page Cache,就是利用操作系統自身的內存而不是JVM空間內存。通過操作系統的Page Cache,Kafka的讀寫操作基本上是基於內存的,讀寫速度得到了極大的提升。

6、Zookeeper在Kafka中的作用

Kafka集羣中有一個broker會被選舉爲Controller,負責管理集羣broker的上下線,所有topic的分區副本分配和leader選舉等工作。Controller的管理工作都是依賴於Zookeeper的。

簡述一下partition的leader的選舉過程:

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