分佈式專題-分佈式消息通信之Kafka03-Kafka原理分析(下)

前言

關於分佈式消息通信,我們主要講三個中間件:

  • ActiveMQ
  • RabbitMQ
  • Kafka

本節主要對Kafka的原理做一個分析

  1. 初識Kafka
  2. Kafka原理分析(上)
  3. Kafka原理分析(下)

消息的文件存儲機制

前面我們知道了一個 topic 的多個 partition 在物理磁盤上的保存路徑,那麼我們再來分析日誌的存儲方式。通過如下命令找到對應 partition 下的日誌內容

[root@localhost ~]# ls /tmp/kafka-logs/firstTopic-1/

00000000000000000000.index 00000000000000000000.log 00000000000000000000.timeindex leader-epoch-checkpoint

在這裏插入圖片描述
kafka 是通過分段的方式將 Log 分爲多個 LogSegment, LogSegment 是一個邏輯上的概念,一個 LogSegment 對應磁盤上的一個日誌文件和一個索引文件,其中日誌文件是用來記錄消息的。索引文件是用來保存消息的索引。那麼這個 LogSegment 是什麼呢?

LogSegment

假設 kafka 以 partition 爲最小存儲單位,那麼我們可以想象當 kafka producer 不斷髮送消息,必然會引起 partition 文件的無線擴張,這樣對於消息文件的維護以及被消費的消息的清理帶來非常大的挑戰,所以 kafka 以 segment 爲單位又把 partition 進行細分。每個 partition 相當於一個巨型文件被平均分配到多個大小相等的 segment 數據文件中(每個 segment 文件中的消息不一定相等),這種特性方便已經被消費的消息的清理,提高磁盤的利用率。

log.segment.bytes=107370 (設置分段大小 ),默認是1gb,我們把這個值調小以後,可以看到日誌分段的效果,抽取其中 3 個分段來進行分析
在這裏插入圖片描述
segment file 由 2 大部分組成,分別爲 index file 和 data file,此 2 個文件一一對應,成對出現,後綴".index"和“.log”分別表示爲 segment 索引文件、數據文件.

segment 文件命名規則:partion 全局的第一個 segment 從 0 開始,後續每個 segment 文件名爲上一個 segment 文件最後一條消息的 offset 值進行遞增。數值最大爲 64 位 long 大小,20 位數字字符長度,沒有數字用 0 填充

查看 segment 文件命名規則

➢ 通過下面這條命令可以看到 kafka 消息日誌的內容

sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test0/00000000000000000000.log --print-data-log

輸出結果爲:

offset: 5376 position: 102124 CreateTime: 1531477349287
isvalid: true keysize: -1 valuesize: 12 magic: 2
compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false headerKeys: [] payload: message_5376

第一個 log 文件的最後一個 offset 爲:5376,所以下一個 segment 的文件命名爲: 00000000000000005376.log。對應的 index 爲 00000000000000005376.index

segment中index和log的對應關係

從所有分段中,找一個分段進行分析

爲了提高查找消息的性能,爲每一個日誌文件添加 2 個索引索引文件:OffsetIndex 和 TimeIndex,分別對應*.index以及*.timeindex, TimeIndex 索引文件格式:它是映射時間戳和相對 offset

查 看 索 引 內 容 : sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.index --print-data-log
在這裏插入圖片描述
如圖所示,index 中存儲了索引以及物理偏移量。 log 存儲了消息的內容。索引文件的元數據執行對應數據文件中 message 的物理偏移地址。舉個簡單的案例來說,以[4053,80899]爲例,在 log 文件中,對應的是第 4053 條記錄,物理偏移量( position )爲 80899. position 是 ByteBuffer 的指針位置

在 partition 中如何通過 offset 查找 message

查找的算法是

  1. 根據 offset 的值,查找 segment 段中的 index 索引文件。由於索引文件命名是以上一個文件的最後一個 offset 進行命名的,所以,使用二分查找算法能夠根據 offset 快速定位到指定的索引文件。

  2. 找到索引文件後,根據 offset 進行定位,找到索引文件中的符合範圍的索引。(kafka 採用稀疏索引的方式來提高查找性能)

  3. 得到 position 以後,再到對應的 log 文件中,從 position出開始查找 offset 對應的消息,將每條消息的 offset 與目標 offset 進行比較,直到找到消息

比如說,我們要查找 offset=2490 這條消息,那麼先找到 00000000000000000000.index, 然後找到[2487,49111]這個索引,再到 log 文件中,根據 49111 這個 position 開始查找,比較每條消息的 offset 是否大於等於 2490。最後查找到對應的消息以後返回

Log 文件的消息內容分析

前面我們通過 kafka 提供的命令,可以查看二進制的日誌文件信息,一條消息,會包含很多的字段。

offset: 5371 position: 102124 CreateTime: 1531477349286
isvalid: true keysize: -1 valuesize: 12 magic: 2
compresscodec: NONE producerId: -1 producerEpoch: -
1 sequence: -1 isTransactional: false headerKeys: []
payload: message_5371

offset 和 position 這兩個前面已經講過了、 createTime 表示創建時間、keysize 和 valuesize 表示 key 和 value 的大小、 compresscodec 表示壓縮編碼、payload:表示消息的具體內容

日誌的清除策略以及壓縮策略

日誌清除策略

前面提到過,日誌的分段存儲,一方面能夠減少單個文件內容的大小,另一方面,方便 kafka 進行日誌清理。日誌的清理策略有兩個

  1. 根據消息的保留時間,當消息在 kafka 中保存的時間超過了指定的時間,就會觸發清理過程

  2. 根據 topic 存儲的數據大小,當 topic 所佔的日誌文件大小大於一定的閥值,則可以開始刪除最舊的消息。kafka會啓動一個後臺線程,定期檢查是否存在可以刪除的消息

通過 log.retention.bytes 和 log.retention.hours 這兩個參數來設置,當其中任意一個達到要求,都會執行刪除。默認的保留時間是:7 天

日誌壓縮策略

Kafka 還提供了“日誌壓縮(Log Compaction)”功能,通過這個功能可以有效的減少日誌文件的大小,緩解磁盤緊張的情況,在很多實際場景中,消息的 key 和 value 的值之間的對應關係是不斷變化的,就像數據庫中的數據會不斷被修改一樣,消費者只關心 key 對應的最新的 value。因此,我們可以開啓 kafka 的日誌壓縮功能,服務端會在後臺啓動啓動 Cleaner 線程池,定期將相同的 key 進行合併,只保留最新的 value 值。日誌的壓縮原理是
在這裏插入圖片描述

partition的高可用副本機制

我們已經知道 Kafka 的每個 topic 都可以分爲多個 Partition,並且多個 partition 會均勻分佈在集羣的各個節點下。雖然這種方式能夠有效的對數據進行分片,但是對於每個 partition 來說,都是單點的,當其中一個 partition 不可用的時候,那麼這部分消息就沒辦法消費。所以 kafka 爲了提高 partition 的可靠性而提供了副本的概念(Replica),通過副本機制來實現冗餘備份。

每個分區可以有多個副本,並且在副本集合中會存在一個 leader 的副本,所有的讀寫請求都是由 leader 副本來進行處理。剩餘的其他副本都做爲 follower 副本,follower 副本會從 leader 副本同步消息日誌。這個有點類似 zookeeper 中 leader 和 follower 的概念,但是具體的時間方式還是有比較大的差異。所以我們可以認爲,副本集會存在一主多從的關係。

一般情況下,同一個分區的多個副本會被均勻分配到集羣中的不同 broker 上,當 leader 副本所在的 broker 出現故障後,可以重新選舉新的 leader 副本繼續對外提供服務。通過這樣的副本機制來提高 kafka 集羣的可用性。

副本分配算法

將所有 N Broker 和待分配的 i 個 Partition 排序.

將第 i 個 Partition 分配到第(i mod n)個 Broker 上.

將第 i 個 Partition 的第 j 個副本分配到第((i + j) mod n)個 Broker 上.

創建一個帶副本機制的 topic

通過下面的命令去創建帶 2 個副本的 topic

./kafka-topics.sh --create --zookeeper 192.168.11.156:2181 --replication-factor 2 --partitions 3 – topic secondTopic

然後我們可以在/tmp/kafka-log 路徑下看到對應 topic 的副本信息了。我們通過一個圖形的方式來表達。

➢ 針對 secondTopic 這個 topic 的 3 個分區對應的 3 個副本
在這裏插入圖片描述
如何知道那個各個分區中對應的 leader 是誰呢?

在 zookeeper 服務器上,通過如下命令去獲取對應分區的信息, 比如下面這個是獲取 secondTopic 第 1 個分區的狀態信息。

get /brokers/topics/secondTopic/partitions/1/state

➢ {“controller_epoch”:12,“leader”:0,“version”:1,“leader_ep och”:0,“isr”:[0,1]}

leader 表示當前分區的 leader 是那個 broker-id。下圖中。

綠色線條的表示該分區中的 leader 節點。其他節點就爲 follower
在這裏插入圖片描述
Kafka 提供了數據複製算法保證,如果 leader 發生故障或掛掉,一個新 leader 被選舉並被接受客戶端的消息成功寫入。Kafka 確保從同步副本列表中選舉一個副本爲 leader; leader 負責維護和跟蹤 ISR(in-Sync replicas , 副本同步隊列)中所有 follower 滯後的狀態。當 producer 發送一條消息到 broker 後,leader 寫入消息並複製到所有 follower。消息提交之後才被成功複製到所有的同步副本。

➢ 既然有副本機制,就一定涉及到數據同步的概念,那接下來分析下數據是如何同步的?

需要注意的是,大家不要把 zookeeper 的 leader 和 follower 的同步機制和 kafka 副本的同步機制搞混了。雖然從思想層面來說是一樣的,但是原理層面的實現是完全不同的。

kafka 副本機制中的幾個概念

Kafka 分區下有可能有很多個副本(replica)用於實現冗餘,從而進一步實現高可用。副本根據角色的不同可分爲 3 類: leader 副本:響應 clients 端讀寫請求的副本

follower 副本:被動地備份 leader 副本中的數據,不能響應 clients 端讀寫請求。

ISR 副本:包含了 leader 副本和所有與 leader 副本保持同步的 follower 副本——如何判定是否與 leader 同步後面會提到每個 Kafka 副本對象都有兩個重要的屬性:LEO 和HW。注意是所有的副本,而不只是 leader 副本。

LEO:即日誌末端位移(log end offset),記錄了該副本底層日誌(log)中下一條消息的位移值。注意是下一條消息!也就是說,如果 LEO=10,那麼表示該副本保存了 10 條消息,位移值範圍是[0, 9]。另外,leader LEO 和 follower LEO 的更新是有區別的。我們後面會詳細說

HW:即上面提到的水位值。對於同一個副本對象而言,其HW 值不會大於 LEO 值。小於等於 HW 值的所有消息都被認爲是“已備份”的(replicated)。同理,leader 副本和follower 副本的 HW 更新是有區別的

副本協同機制

剛剛提到了,消息的讀寫操作都只會由 leader 節點來接收和處理。follower 副本只負責同步數據以及當 leader 副本所在的 broker 掛了以後,會從 follower 副本中選取新的leader。

在這裏插入圖片描述
寫請求首先由 Leader 副本處理,之後 follower 副本會從 leader 上拉取寫入的消息,這個過程會有一定的延遲,導致 follower 副本中保存的消息略少於 leader 副本,但是隻要沒有超出閾值都可以容忍。但是如果一個 follower 副本出現異常,比如宕機、網絡斷開等原因長時間沒有同步到消息,那這個時候,leader 就會把它踢出去。kafka 通過 ISR 集合來維護一個分區副本信息

ISR

ISR 表示目前“可用且消息量與 leader 相差不多的副本集合,這是整個副本集合的一個子集”。怎麼去理解可用和相差不多這兩個詞呢?具體來說,ISR 集合中的副本必須滿足兩個條件

  1. 副本所在節點必須維持着與 zookeeper 的連接

  2. 副本最後一條消息的 offset 與 leader 副本的最後一條消息的 offset 之 間 的 差 值 不 能 超 過 指 定 的 閾 值(replica.lag.time.max.ms)

replica.lag.time.max.ms:如果該 follower 在此時間間隔內一直沒有追上過 leader 的所有消息,則該 follower 就會被剔除 isr 列表

➢ ISR數據保存在Zookeeper的 /brokers/topics//partitions//state節點中

HW&LEO

關於 follower 副本同步的過程中,還有兩個關鍵的概念,HW(HighWatermark)和 LEO(Log End Offset). 這兩個參數跟 ISR 集合緊密關聯。HW 標記了一個特殊的 offset,當消費者處理消息的時候,只能拉去到 HW 之前的消息,HW之後的消息對消費者來說是不可見的。也就是說,取 partition 對應 ISR 中最小的 LEO 作爲 HW,consumer 最多隻能消費到 HW 所在的位置。每個 replica 都有 HW, leader 和 follower 各自維護更新自己的 HW 的狀態。一條消息只有被 ISR 裏的所有 Follower 都從 Leader 複製過去纔會被認爲已提交。這樣就避免了部分數據被寫進了Leader,還沒來得及被任何 Follower 複製就宕機了,而造成數據丟失(Consumer 無法消費這些數據)。而對於 Producer 而言,它可以選擇是否等待消息 commit,這可以通過 acks 來設置。這種機制確保了只要 ISR 有一個或以上的 Follower,一條被 commit 的消息就不會丟失。

數據的同步過程

瞭解了副本的協同過程以後,還有一個最重要的機制,就是數據的同步過程。它需要解決

  1. 怎麼傳播消息

  2. 在向消息發送端返回 ack 之前需要保證多少個 Replica 已經接收到這個消息數據的處理過程是Producer 在發佈消息到某個 Partition 時,先通過ZooKeeper 找 到 該 Partition 的 Leader 【 get /brokers/topics//partitions/2/state】,然後無論該 Topic 的 Replication Factor 爲多少(也即該 Partition 有多少個 Replica),Producer 只將該消息發送到該 Partition 的Leader。Leader 會將該消息寫入其本地 Log。每個 Follower 都從 Leader pull 數據。這種方式上,Follower 存儲的數據順序與 Leader 保持一致。Follower 在收到該消息並寫入其 Log 後,向 Leader 發送 ACK。一旦 Leader 收到了 ISR 中的所有 Replica 的 ACK,該消息就被認爲已經 commit 了, Leader 將增加 HW(HighWatermark)並且向 Producer 發送ACK。

初始狀態

初始狀態下,leader 和 follower 的 HW 和 LEO 都是 0, leader 副本會保存 remote LEO,表示所有 follower LEO,也會被初始化爲 0,這個時候,producer 沒有發送消息。 follower 會不斷地個 leader 發送 FETCH 請求,但是因爲沒有數據,這個請求會被 leader 寄存,當在指定的時間之後會 強 制 完 成 請 求 , 這 個 時 間 配 置 是(replica.fetch.wait.max.ms),如果在指定時間內 producer 有消息發送過來,那麼 kafka 會喚醒 fetch 請求,讓 leader 繼續處理
在這裏插入圖片描述
這裏會分兩種情況,第一種是 leader 處理完 producer 請求之後,follower 發送一個 fetch 請求過來、第二種是 follower 阻塞在 leader 指定時間之內,leader 副本收到 producer 的請求。這兩種情況下處理方式是不一樣的。先來看第一種情況

follower 的 fetch 請求是當 leader 處理消息以後執行的

  • 生產者發送一條消息

➢ leader 處理完 producer 請求之後,follower 發送一個

fetch 請求過來 。狀態圖如下
在這裏插入圖片描述
leader 副本收到請求以後,會做幾件事情

  1. 把消息追加到 log 文件,同時更新 leader 副本的 LEO

  2. 嘗試更新 leader HW 值。這個時候由於 follower 副本還沒有發送 fetch 請求,那麼 leader 的 remote LEO 仍然是 0。leader 會比較自己的 LEO 以及 remote LEO 的值發現最小值是 0,與 HW 的值相同,所以不會更新 HW

  • follower fetch 消息

在這裏插入圖片描述
follower 發送 fetch 請求,leader 副本的處理邏輯是:

  1. 讀取 log 數據、更新 remote LEO=0(follower 還沒有寫入這條消息,這個值是根據 follower 的 fetch 請求中的 offset 來確定的)

  2. 嘗試更新 HW,因爲這個時候 LEO 和 remoteLEO 還是不一致,所以仍然是 HW=0

  3. 把消息內容和當前分區的 HW 值發送給 follower 副本 follower 副本收到 response 以後
    3.1. 將消息寫入到本地 log,同時更新 follower 的 LEO
    3.2. 更新 follower HW,本地的 LEO 和 leader 返回的 HW進行比較取小的值,所以仍然是 0

第一次交互結束以後,HW 仍然還是 0,這個值會在下一次 follower 發起 fetch 請求時被更新
在這裏插入圖片描述
follower 發第二次 fetch 請求,leader 收到請求以後

  1. 讀取 log 數據

  2. 更新 remote LEO=1, 因爲這次 fetch 攜帶的 offset 是 1.

  3. 更新當前分區的 HW,這個時候 leader LEO 和 remote LEO 都是 1,所以 HW 的值也更新爲 1

  4. 把數據和當前分區的 HW 值返回給 follower 副本,這個時候如果沒有數據,則返回爲空

follower 副本收到 response 以後

  1. 如果有數據則寫本地日誌,並且更新 LEO

  2. 更新 follower 的 HW 值到目前爲止,數據的同步就完成了,意味着消費端能夠消費 offset=0 這條消息。

follower 的 fetch 請求是直接從阻塞過程中觸發

前面說過,由於 leader 副本暫時沒有數據過來,所以 follower 的 fetch 會被阻塞,直到等待超時或者 leader 接收到新的數據。當 leader 收到請求以後會喚醒處於阻塞的 fetch 請求。處理過程基本上和前面說的一直

1.leader 將消息寫入本地日誌,更新 Leader 的 LEO

2.喚醒 follower 的 fetch 請求

3.更新 HW

kafka 使用 HW 和 LEO 的方式來實現副本數據的同步,本身是一個好的設計,但是在這個地方會存在一個數據丟失的問題,當然這個丟失只出現在特定的背景下。我們回想一下,HW 的值是在新的一輪 FETCH 中才會被更新。我們分析下這個過程爲什麼會出現數據丟失

數據丟失的問題

前提:min.insync.replicas=1 的時候。->設定 ISR 中的最小副本數是多少,默認值爲 1, 當且僅當 acks 參數設置爲-1(表示需要所有副本確認)時,此參數才生效. 表達的含義是,至少需要多少個副本同步才能表示消息是提交的所以,當 min.insync.replicas=1 的時候一旦消息被寫入 leader 端 log 即被認爲是“已提交”,而延遲一輪 FETCH RPC 更新 HW 值的設計使得 follower HW 值是異步延遲更新的,倘若在這個過程中 leader 發生變更,那麼成爲新 leader 的 follower 的 HW 值就有可能是過期的,使得 clients 端認爲是成功提交的消息被刪除。
在這裏插入圖片描述

數據丟失的解決方案

在 kafka0.11.0.0 版本以後,提供了一個新的解決方案,使用 leader epoch 來解決這個問題,leader epoch 實際上是一對之(epoch,offset), epoch 表示 leader 的版本號,從 0 開始,當 leader 變更過 1 次時 epoch 就會+1,而 offset 則對應於該 epoch 版本的 leader 寫入第一條消息的位移。比如說

(0,0) ; (1,50); 表示第一個 leader 從 offset=0 開始寫消息,一共寫了 50 條,第二個 leader 版本號是 1,從 50 條處開始寫消息。這個信息保存在對應分區的本地磁盤文件中,文 件 名 爲 : /tml/kafka-log/topic/leader-epoch-checkpoint

leader broker 中會保存這樣的一個緩存,並定期地寫入到一個 checkpoint 文件中。

當 leader 寫 log 時它會嘗試更新整個緩存——如果這個 leader 首次寫消息,則會在緩存中增加一個條目;否則就不做更新。而每次副本重新成爲 leader 時會查詢這部分緩存,獲取出對應 leader 版本的 offset

在這裏插入圖片描述
如何處理所有的 Replica 不工作的情況

在 ISR 中至少有一個 follower 時,Kafka 可以確保已經 commit 的數據不丟失,但如果某個 Partition 的所有 Replica 都宕機了,就無法保證數據不丟失了

  1. 等待 ISR 中的任一個 Replica“活”過來,並且選它作爲Leader

  2. 選擇第一個“活”過來的 Replica(不一定是 ISR 中的)作爲Leader

這就需要在可用性和一致性當中作出一個簡單的折衷。如果一定要等待 ISR 中的 Replica“活”過來,那不可用的時間就可能會相對較長。而且如果 ISR 中的所有 Replica 都無法“活”過來了,或者數據都丟失了,這個 Partition 將永遠不可用。

選擇第一個“活”過來的 Replica 作爲 Leader,而這個 Replica 不是 ISR 中的 Replica,那即使它並不保證已經包含了所有已 commit 的消息,它也會成爲 Leader 而作爲 consumer 的數據源(前文有說明,所有讀寫都由 Leader 完成)。在我們課堂講的版本中,使用的是第一種策略。

ISR 的設計原理

在所有的分佈式存儲中,冗餘備份是一種常見的設計方式,而常用的模式有同步複製和異步複製,按照 kafka 這個副本模型來說

如果採用同步複製,那麼需要要求所有能工作的 Follower 副本都複製完,這條消息纔會被認爲提交成功,一旦有一個 follower 副本出現故障,就會導致 HW 無法完成遞增,消息就無法提交,消費者就獲取不到消息。這種情況下,故障的 Follower 副本會拖慢整個系統的性能,設置導致系統不可用如果採用異步複製,leader 副本收到生產者推送的消息後,就認爲次消息提交成功。follower 副本則異步從 leader 副本同步。這種設計雖然避免了同步複製的問題,但是假設所有 follower 副本的同步速度都比較慢他們保存的消息量遠遠落後於 leader 副本。而此時 leader 副本所在的 broker 突然宕機,則會重新選舉新的 leader 副本,而新的 leader 副本中沒有原來 leader 副本的消息。這就出現了消息的丟失。

kafka 權衡了同步和異步的兩種策略,採用 ISR 集合,巧妙解決了兩種方案的缺陷:當 follower 副本延遲過高,leader 副本則會把該 follower 副本提出 ISR 集合,消息依然可以快速提交。當 leader 副本所在的 broker 突然宕機,會優先將 ISR 集合中 follower 副本選舉爲 leader,新 leader 副本包含了 HW 之前的全部消息,這樣就避免了消息的丟失。

後記

kafka-demo下載地址

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