Kafka設(shè)計原理

1. Kafka簡介

Kafka 是一種分布式的,基于發(fā)布/訂閱的消息系統(tǒng),主要設(shè)計目標(biāo)如下:

  • 以時間復(fù)雜度為 O(1) 的方式提供消息持久化能力,即使對 TB 級以上數(shù)據(jù)也能保證常數(shù)時間復(fù)雜度的訪問性能。
  • 高吞吐率。即使在非常廉價的商用機(jī)器上也能做到單機(jī)支持每秒 100K 條以上消息的傳輸。
  • 支持 Kafka Server 間的消息分區(qū),及分布式消費(fèi),同時保證每個 Partition 內(nèi)的消息順序傳輸
  • 同時支持離線數(shù)據(jù)處理和實時數(shù)據(jù)處理
  • Scale out:支持在線水平擴(kuò)展

1.1 為什么使用消息系統(tǒng)

  • 解耦
      在項目啟動之初來預(yù)測將來項目會碰到什么需求,是極其困難的。消息系統(tǒng)在處理過程中間插入了一個隱含的、基于數(shù)據(jù)的接口層,兩邊的處理過程都要實現(xiàn)這一接口。這允許你獨立的擴(kuò)展或修改兩邊的處理過程,只要確保它們遵守同樣的接口約束。

  • 冗余
      有些情況下,處理數(shù)據(jù)的過程會失敗。除非數(shù)據(jù)被持久化,否則將造成丟失。消息隊列把數(shù)據(jù)進(jìn)行持久化直到它們已經(jīng)被完全處理,通過這一方式規(guī)避了數(shù)據(jù)丟失風(fēng)險。許多消息隊列所采用的”插入-獲取-刪除”范式中,在把一個消息從隊列中刪除之前,需要你的處理系統(tǒng)明確的指出該消息已經(jīng)被處理完畢,從而確保你的數(shù)據(jù)被安全的保存直到你使用完畢。

  • 擴(kuò)展性
      因為消息隊列解耦了你的處理過程,所以增大消息入隊和處理的頻率是很容易的,只要另外增加處理過程即可。不需要改變代碼、不需要調(diào)節(jié)參數(shù)。擴(kuò)展就像調(diào)大電力按鈕一樣簡單。

  • 靈活性 & 峰值處理能力
      在訪問量劇增的情況下,應(yīng)用仍然需要繼續(xù)發(fā)揮作用,但是這樣的突發(fā)流量并不常見;如果為以能處理這類峰值訪問為標(biāo)準(zhǔn)來投入資源隨時待命無疑是巨大的浪費(fèi)。使用消息隊列能夠使關(guān)鍵組件頂住突發(fā)的訪問壓力,而不會因為突發(fā)的超負(fù)荷的請求而完全崩潰。

  • 可恢復(fù)性
      系統(tǒng)的一部分組件失效時,不會影響到整個系統(tǒng)。消息隊列降低了進(jìn)程間的耦合度,所以即使一個處理消息的進(jìn)程掛掉,加入隊列中的消息仍然可以在系統(tǒng)恢復(fù)后被處理。

  • 順序保證
      在大多使用場景下,數(shù)據(jù)處理的順序都很重要。大部分消息隊列本來就是排序的,并且能保證數(shù)據(jù)會按照特定的順序來處理。Kafka保證一個Partition內(nèi)的消息的有序性。

  • 緩沖
      在任何重要的系統(tǒng)中,都會有需要不同的處理時間的元素。例如,加載一張圖片比應(yīng)用過濾器花費(fèi)更少的時間。消息隊列通過一個緩沖層來幫助任務(wù)最高效率的執(zhí)行———寫入隊列的處理會盡可能的快速。該緩沖有助于控制和優(yōu)化數(shù)據(jù)流經(jīng)過系統(tǒng)的速度。

  • 異步通信
      很多時候,用戶不想也不需要立即處理消息。消息隊列提供了異步處理機(jī)制,允許用戶把一個消息放入隊列,但并不立即處理它。想向隊列中放入多少消息就放多少,然后在需要的時候再去處理它們。

1.2 常用Message Queue對比

  • RabbitMQ
      RabbitMQ是使用Erlang編寫的一個開源的消息隊列,本身支持很多的協(xié)議:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量級,更適合于企業(yè)級的開發(fā)。同時實現(xiàn)了Broker構(gòu)架,這意味著消息在發(fā)送給客戶端時先在中心隊列排隊。對路由,負(fù)載均衡或者數(shù)據(jù)持久化都有很好的支持。

  • Redis
      Redis是一個基于Key-Value對的NoSQL數(shù)據(jù)庫,開發(fā)維護(hù)很活躍。雖然它是一個Key-Value數(shù)據(jù)庫存儲系統(tǒng),但它本身支持MQ功能,所以完全可以當(dāng)做一個輕量級的隊列服務(wù)來使用。對于RabbitMQ和Redis的入隊和出隊操作,各執(zhí)行100萬次,每10萬次記錄一次執(zhí)行時間。測試數(shù)據(jù)分為128Bytes、512Bytes、1K和10K四個不同大小的數(shù)據(jù)。實驗表明:入隊時,當(dāng)數(shù)據(jù)比較小時Redis的性能要高于RabbitMQ,而如果數(shù)據(jù)大小超過了10K,Redis則慢的無法忍受;出隊時,無論數(shù)據(jù)大小,Redis都表現(xiàn)出非常好的性能,而RabbitMQ的出隊性能則遠(yuǎn)低于Redis。

  • ZeroMQ
      ZeroMQ號稱最快的消息隊列系統(tǒng),尤其針對大吞吐量的需求場景。ZMQ能夠?qū)崿F(xiàn)RabbitMQ不擅長的高級/復(fù)雜的隊列,但是開發(fā)人員需要自己組合多種技術(shù)框架,技術(shù)上的復(fù)雜度是對這MQ能夠應(yīng)用成功的挑戰(zhàn)。ZeroMQ具有一個獨特的非中間件的模式,你不需要安裝和運(yùn)行一個消息服務(wù)器或中間件,因為你的應(yīng)用程序?qū)缪葸@個服務(wù)器角色。你只需要簡單的引用ZeroMQ程序庫,可以使用NuGet安裝,然后你就可以愉快的在應(yīng)用程序之間發(fā)送消息了。但是ZeroMQ僅提供非持久性的隊列,也就是說如果宕機(jī),數(shù)據(jù)將會丟失。其中,Twitter的Storm 0.9.0以前的版本中默認(rèn)使用ZeroMQ作為數(shù)據(jù)流的傳輸(Storm從0.9版本開始同時支持ZeroMQ和Netty作為傳輸模塊)。

  • ActiveMQ
      ActiveMQ是Apache下的一個子項目。 類似于ZeroMQ,它能夠以代理人和點對點的技術(shù)實現(xiàn)隊列。同時類似于RabbitMQ,它少量代碼就可以高效地實現(xiàn)高級應(yīng)用場景。

  • Kafka/Jafka
      Kafka是Apache下的一個子項目,是一個高性能跨語言分布式發(fā)布/訂閱消息隊列系統(tǒng),而Jafka是在Kafka之上孵化而來的,即Kafka的一個升級版。具有以下特性:快速持久化,可以在O(1)的系統(tǒng)開銷下進(jìn)行消息持久化;高吞吐,在一臺普通的服務(wù)器上既可以達(dá)到10W/s的吞吐速率;完全的分布式系統(tǒng),Broker、Producer、Consumer都原生自動支持分布式,自動實現(xiàn)負(fù)載均衡;支持Hadoop數(shù)據(jù)并行加載,對于像Hadoop的一樣的日志數(shù)據(jù)和離線分析系統(tǒng),但又要求實時處理的限制,這是一個可行的解決方案。Kafka通過Hadoop的并行加載機(jī)制統(tǒng)一了在線和離線的消息處理。Apache Kafka相對于ActiveMQ是一個非常輕量級的消息系統(tǒng),除了性能非常好之外,還是一個工作良好的分布式系統(tǒng)。

2. Kafka架構(gòu)介紹

2.1. 基礎(chǔ)概念

  • Producer
      負(fù)責(zé)發(fā)布消息到Kafka broker
  • Consumer
      消息消費(fèi)者,向Kafka broker讀取消息的客戶端。
  • Topic
      每條發(fā)布到Kafka集群的消息都有一個類別,這個類別被稱為Topic。在 Kafka 中,消息以主題(Topic)來分類,每一個主題都對應(yīng)一個「消息隊列」,這有點兒類似于數(shù)據(jù)庫中的表。(物理上不同Topic的消息分開存儲,邏輯上一個Topic的消息雖然保存于一個或多個broker上但用戶只需指定消息的Topic即可生產(chǎn)或消費(fèi)數(shù)據(jù)而不必關(guān)心數(shù)據(jù)存于何處)
      但是如果我們把所有同類的消息都塞入到一個“中心”隊列中,勢必缺少可伸縮性,無論是生產(chǎn)者/消費(fèi)者數(shù)目的增加,還是消息數(shù)量的增加,都可能耗盡系統(tǒng)的性能或存儲。
  • Partition
      Parition是物理上的概念,每個Topic包含一個或多個Partition。
  • Broker
      Kafka集群包含一個或多個服務(wù)器,這種服務(wù)器被稱為broker。
  • Consumer Group
      每個Consumer屬于一個特定的Consumer Group(可為每個Consumer指定group name,若不指定group name則屬于默認(rèn)的group)。

2.2. Kafka拓?fù)浣Y(jié)構(gòu)


如上圖所示,一個典型的Kafka集群中包含若干Producer(可以是web前端產(chǎn)生的Page View,或者是服務(wù)器日志,系統(tǒng)CPU、Memory等),若干broker(Kafka支持水平擴(kuò)展,一般broker數(shù)量越多,集群吞吐率越高),若干Consumer Group,以及一個Zookeeper集群。Kafka通過Zookeeper管理集群配置,選舉leader,以及在Consumer Group發(fā)生變化時進(jìn)行rebalance。Producer使用push模式將消息發(fā)布到broker,Consumer使用pull模式從broker訂閱并消費(fèi)消息。

2.3. Topic & Partition

Topic 在邏輯上可以被認(rèn)為是一個 Queue,每條消費(fèi)都必須指定它的 Topic,可以簡單理解為必須指明把這條消息放進(jìn)哪個 Queue 里。我們把一類消息按照主題來分類,有點類似于數(shù)據(jù)庫中的表。

為了使得 Kafka 的吞吐率可以線性提高,物理上把 Topic 分成一個或多個 Partition。對應(yīng)到系統(tǒng)上就是一個或若干個目錄。


如果一個Topic對應(yīng)一個文件,那這個文件所在的機(jī)器I/O將會成為這個Topic的性能瓶頸,而有了Partition后,不同的消息可以并行寫入不同broker的不同Partition里,極大的提高了吞吐率

可以在$KAFKA_HOME/config/server.properties中通過配置項num.partitions來指定新建Topic的默認(rèn)Partition數(shù)量,也可在創(chuàng)建Topic時通過參數(shù)指定,同時也可以在Topic創(chuàng)建之后通過Kafka提供的工具修改。

假設(shè)我們現(xiàn)在 Kafka 集群只有一個 Broker,我們創(chuàng)建 2 個 Topic 名稱分別為:「Topic1」和「Topic2」,Partition 數(shù)量分別為 1、2。
那么我們的根目錄下就會創(chuàng)建如下三個文件夾:

    | --topic1-0
    | --topic2-0
    | --topic2-1

在 Kafka 的文件存儲中,同一個 Topic 下有多個不同的 Partition,每個 Partition 都為一個目錄。

而每一個目錄又被平均分配成多個大小相等的 Segment File 中,Segment File 又由 index file 和 data file 組成,他們總是成對出現(xiàn),后綴 ".index" 和 ".log" 分表表示 Segment 索引文件和數(shù)據(jù)文件。

現(xiàn)在假設(shè)我們設(shè)置每個 Segment 大小為 500 MB,并啟動生產(chǎn)者向 topic1 中寫入大量數(shù)據(jù),topic1-0 文件夾中就會產(chǎn)生類似如下的一些文件:

    | --topic1-0
        | --00000000000000000000.index
        | --00000000000000000000.log
        | --00000000000000368769.index
        | --00000000000000368769.log
        | --00000000000000737337.index
        | --00000000000000737337.log
        | --00000000000001105814.index
        | --00000000000001105814.log
    | --topic2-0
    | --topic2-1

Segment 是 Kafka 文件存儲的最小單位。Segment 文件命名規(guī)則:Partition 全局的第一個 Segment 從 0 開始,后續(xù)每個 Segment 文件名為上一個 Segment 文件最后一條消息的 offset 值。

數(shù)值最大為 64 位 long 大小,19 位數(shù)字字符長度,沒有數(shù)字用 0 填充。如 00000000000000368769.index 和 00000000000000368769.log。

以上面的一對 Segment File 為例,說明一下索引文件和數(shù)據(jù)文件對應(yīng)關(guān)系:


其中以索引文件中元數(shù)據(jù) <3, 497> 為例,依次在數(shù)據(jù)文件中表示第 3 個 Message(在全局 Partition 表示第 368769 + 3 = 368772 個 message)以及該消息的物理偏移地址為 497

注意該 Index 文件并不是從0開始,也不是每次遞增 1 的,這是因為 Kafka 采取稀疏索引存儲的方式,每隔一定字節(jié)的數(shù)據(jù)建立一條索引

它減少了索引文件大小,使得能夠把 Index 映射到內(nèi)存,降低了查詢時的磁盤 IO 開銷,同時也并沒有給查詢帶來太多的時間消耗。

因為其文件名為上一個 Segment 最后一條消息的 Offset ,所以當(dāng)需要查找一個指定 OffsetMessage 時,通過在所有 Segment 的文件名中進(jìn)行二分查找就能找到它歸屬的 Segment

再在其 Index 文件中找到其對應(yīng)到文件上的物理位置,就能拿出該 Message。

由于消息在 PartitionSegment 數(shù)據(jù)文件中是順序讀寫的,且消息消費(fèi)后不會刪除(刪除策略是針對過期的 Segment 文件),這是順序磁盤 IO 存儲設(shè)計師 Kafka 高性能很重要的原因。

Kafka 是如何準(zhǔn)確的知道 Message 的偏移的呢?這是因為在 Kafka 定義了標(biāo)準(zhǔn)的數(shù)據(jù)存儲結(jié)構(gòu),在 Partition 中的每一條 Message 都包含了以下三個屬性
Offset:表示 Message 在當(dāng)前 Partition 中的偏移量,是一個邏輯上的值,唯一確定了 Partition 中的一條 Message,可以簡單的認(rèn)為是一個 ID。
MessageSize:表示 Message 內(nèi)容 Data 的大小。
Data:Message 的具體內(nèi)容。

因為每條消息都被append到該P(yáng)artition中,屬于順序?qū)懘疟P,因此效率非常高(經(jīng)驗證,順序?qū)懘疟P效率比隨機(jī)寫內(nèi)存還要高,這是Kafka高吞吐率的一個很重要的保證)。

如何根據(jù)offset查找message

例如讀取 offset=368776的 message,需要通過下面2個步驟查找:


  • 第一步查找 segment file 上述圖為例,其中00000000000000000000.index 表示最開始的文件,起始偏移量(offset)為 0。第二個文件00000000000000368769.index 的消息量起始偏移量為368770 = 368769 + 1,其他后續(xù)文件依次類推,以起始偏移量命名并排序這些文件,只要根據(jù) offset 二分查找文件列表,就可以快速定位到具體文件。 當(dāng)offset=368776時定位到00000000000000368769.index | log。


  • 第二步通過 segment file 查找 message 通過第一步定位到 segment file,當(dāng) offset=368776時,依次定位到00000000000000368769.index 的元數(shù)據(jù)物理位置和
    00000000000000368769.log 的物理偏移地址,然后再通過00000000000000368769.log 順序查找直到offset=368776 為止。
如何根據(jù)timeindex查找message

Kafka 從0.10.0.0版本起,為分片日志文件中新增了一個 .timeindex 的索引文件,可以根據(jù)時間戳定位消息。同樣我們可以通過腳本 kafka-dump-log.sh 查看時間索引的文件內(nèi)容。


  • 首先定位分片,將 1570793423501 與每個分片的最大時間戳進(jìn)行對比(最大時間戳取時間索引文件的最后一條記錄時間,如果時間為 0 則取該日志分段的最近修改時間),直到找到大于或等于 1570793423501 的日志分段,因此會定位到時間索引文件00000000000003257573.timeindex,其最大時間戳為 1570793423505。
  • 重復(fù) Offset 找到 log 文件的步驟。
分區(qū)分配策略

Kafka提供了三個分區(qū)分配策略:RangeAssignor、RoundRobinAssignor以及StickyAssignor,下面簡單介紹下各個算法的實現(xiàn)。

  • RangeAssignor:kafka默認(rèn)會采用此策略進(jìn)行分區(qū)分配,主要流程如下:

    假設(shè)一個消費(fèi)組中存在兩個消費(fèi)者{C0,C1},該消費(fèi)組訂閱了三個主題{T1,T2,T3},每個主題分別存在三個分區(qū),一共就有9個分區(qū){TP1,TP2,...,TP9}。通過以上算法我們可以得到D=4,R=1,那么消費(fèi)組C0將消費(fèi)的分區(qū)為{TP1,TP2,TP3,TP4,TP5},C1將消費(fèi)分區(qū){TP6,TP7,TP8,TP9}。這里存在一個問題,如果不能均分,那么前面的幾個消費(fèi)者將會多消費(fèi)一個分區(qū)。

    1. 將所有訂閱主題下的分區(qū)進(jìn)行排序得到集合TP={TP0,Tp1,...,TPN+1}。
    2. 對消費(fèi)組中的所有消費(fèi)者根據(jù)名字進(jìn)行字典排序得到集合CG={C0,C1,...,CM+1}。
    3. 計算D=N/M,R=N%M。
    4. 消費(fèi)者Ci獲取消費(fèi)分區(qū)起始位置=D*i+min(i,R)Ci獲取的分區(qū)總數(shù)=D+(if (i+1>R)0 else 1)
  • RoundRobinAssignor:使用該策略需要滿足以下兩個條件:1) 消費(fèi)組中的所有消費(fèi)者應(yīng)該訂閱主題相同;2) 同一個消費(fèi)組的所有消費(fèi)者在實例化時給每個主題指定相同的流數(shù)。

    1. 對所有主題的所有分區(qū)根據(jù)主題+分區(qū)得到的哈希值進(jìn)行排序。
    2. 對所有消費(fèi)者按字典排序。
    3. 通過輪詢的方式將分區(qū)分配給消費(fèi)者。
  • StickyAssignor:該分配方式在0.11版本開始引入,主要是保證以下特性:

    1. 盡可能的保證分配均衡;
    2. 當(dāng)重新分配時,保留盡可能多的現(xiàn)有分配。

    其中第一條的優(yōu)先級要大于第二條。

2.4. Broker 和集群(Cluster)

一個 Kafka 服務(wù)器也稱為 Broker,它接受生產(chǎn)者發(fā)送的消息并存入磁盤;Broker 同時服務(wù)消費(fèi)者拉取分區(qū)消息的請求,返回目前已經(jīng)提交的消息。使用特定的機(jī)器硬件,一個 Broker 每秒可以處理成千上萬的分區(qū)和百萬量級的消息。

若干個 Broker 組成一個集群(Cluster),其中集群內(nèi)某個 Broker 會成為集群控制器(Cluster Controller),它負(fù)責(zé)管理集群,包括分配分區(qū)到 Broker、監(jiān)控 Broker 故障等。

在集群內(nèi),一個分區(qū)由一個 Broker 負(fù)責(zé),這個 Broker 也稱為這個分區(qū)的 Leader。

當(dāng)然一個分區(qū)可以被復(fù)制到多個 Broker 上來實現(xiàn)冗余,這樣當(dāng)存在 Broker 故障時可以將其分區(qū)重新分配到其他 Broker 來負(fù)責(zé)。如下圖所示:

對于傳統(tǒng)的message queue而言,一般會刪除已經(jīng)被消費(fèi)的消息,而Kafka集群會保留所有的消息,無論其被消費(fèi)與否。當(dāng)然,因為磁盤限制,不可能永久保留所有數(shù)據(jù)(實際上也沒必要),因此Kafka提供兩種策略刪除舊數(shù)據(jù):一是基于時間,二是基于Partition文件大小。例如可以通過配置$KAFKA_HOME/config/server.properties,讓Kafka刪除一周前的數(shù)據(jù),也可在Partition文件超過1GB時刪除舊數(shù)據(jù),配置如下所示:

# The minimum age of a log file to be eligible for deletion
log.retention.hours=168
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
# The interval at which log segments are checked to see if they can be deleted according to the retention policies
log.retention.check.interval.ms=300000
# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction.
log.cleaner.enable=false

這里要注意,因為Kafka讀取特定消息的時間復(fù)雜度為O(1),即與文件大小無關(guān),所以這里刪除過期文件與提高Kafka性能無關(guān)。選擇怎樣的刪除策略只與磁盤以及具體的需求有關(guān)。另外,Kafka會為每一個Consumer Group保留一些metadata信息——當(dāng)前消費(fèi)的消息的position,也即offset。

這個offset由Consumer控制。正常情況下Consumer會在消費(fèi)完一條消息后遞增該offset。當(dāng)然,Consumer也可將offset設(shè)成一個較小的值,重新消費(fèi)一些消息。因為offet由Consumer控制,所以Kafka broker是無狀態(tài)的,它不需要標(biāo)記哪些消息被哪些消費(fèi)過,也不需要通過broker去保證同一個Consumer Group只有一個Consumer能消費(fèi)某一條消息,因此也就不需要鎖機(jī)制,這也為Kafka的高吞吐率提供了有力保障。

2.5. Producer

Producer發(fā)送消息到broker時,會根據(jù)Paritition機(jī)制選擇將其存儲到哪一個Partition。如果Partition機(jī)制設(shè)置合理,所有消息可以均勻分布到不同的Partition里,這樣就實現(xiàn)了負(fù)載均衡。

  • 指明 Partition 的情況下,直接將給定的Value 作為 Partition 的值
  • 沒有指明 Partition 但有 Key 的情況下,將Key 的 Hash 值與分區(qū)數(shù)取余得到Partition值。
  • 既沒有 Partition 又沒有 Key 的情況下,第一次調(diào)用時隨機(jī)生成一個整數(shù)(后面每次調(diào)用都在這個整數(shù)上自增),將這個值與可用的分區(qū)數(shù)取余,得到 Partition 值,也就是常說的 Round-Robin 輪詢算法。

為保證 Producer 發(fā)送的數(shù)據(jù),能可靠地發(fā)送到指定的 Topic,Topic 的每個Partition 收到 Producer 發(fā)送的數(shù)據(jù)后,都需要向 Producer 發(fā)送 ACK。如果Producer 收到 ACK,就會進(jìn)行下一輪的發(fā)送,否則重新發(fā)送數(shù)據(jù)。

ack參數(shù)設(shè)置及意義

生產(chǎn)端往kafka集群發(fā)送消息時,可以通過request.required.acks參數(shù)來設(shè)置數(shù)據(jù)的可靠性級別

  • 1:默認(rèn)為1,表示在ISR中的leader副本成功接收到數(shù)據(jù)并確認(rèn)后再發(fā)送下一條消息,如果主節(jié)點宕機(jī)則可能出現(xiàn)數(shù)據(jù)丟失場景,詳細(xì)分析可參考前面提到的副本章節(jié)。

  • 0:表示生產(chǎn)端不需要等待節(jié)點的確認(rèn)就可以繼續(xù)發(fā)送下一批數(shù)據(jù),這種情況下數(shù)據(jù)傳輸效率最高,但是數(shù)據(jù)的可靠性最低。

  • -1:表示生產(chǎn)端需要等待ISR中的所有副本節(jié)點都收到數(shù)據(jù)之后才算消息寫入成功,可靠性最高,但是性能最低,如果服務(wù)端的min.insync.replicas值設(shè)置為1,那么在這種情況下允許ISR集合只有一個副本,因此也會存在數(shù)據(jù)丟失的情況。

冪等特性

冪等性:同一個操作任意執(zhí)行多次產(chǎn)生的影響或效果與一次執(zhí)行影響相同

冪等的關(guān)鍵在于服務(wù)端能否識別出請求是否重復(fù),然后過濾掉這些重復(fù)請求,通常情況下需要以下信息來實現(xiàn)冪等特性:

  • 唯一標(biāo)識:判斷某個請求是否重復(fù),需要有一個唯一性標(biāo)識,然后服務(wù)端就能根據(jù)這個唯一標(biāo)識來判斷是否為重復(fù)請求。
  • 記錄已經(jīng)處理過的請求:服務(wù)端需要記錄已經(jīng)處理過的請求,然后根據(jù)唯一標(biāo)識來判斷是否是重復(fù)請求,如果已經(jīng)處理過,則直接拒絕或者不做任何操作返回成功。
    kafka中Producer端的冪等性是指當(dāng)發(fā)送同一條消息時,消息在集群中只會被持久化一次,其冪等是在以下條件中才成立:
  • 只能保證生產(chǎn)端在單個會話內(nèi)的冪等,如果生產(chǎn)端因為某些原因意外掛掉然后重啟,此時是沒辦法保證冪等的,因為這時沒辦法獲取到之前的狀態(tài)信息,即無法做到垮會話級別的冪等。
  • 冪等性不能垮多個主題分區(qū),只能保證單個分區(qū)內(nèi)的冪等,涉及到多個消息分區(qū)時,中間的狀態(tài)并沒有同步。

如果要支持垮會話或者垮多個消息分區(qū)的情況,則需要使用kafka的事務(wù)性來實現(xiàn)。
為了實現(xiàn)生成端的冪等語義,引入了Producer ID(PID)Sequence Number的概念:

  • Producer ID(PID):每個生產(chǎn)者在初始化時都會分配一個唯一的PID,PID的分配對于用戶來說是透明的。
  • Sequence Number(序列號):對于給定的PID而言,序列號從0開始單調(diào)遞增,每個主題分區(qū)均會產(chǎn)生一個獨立序列號,生產(chǎn)者在發(fā)送消息時會給每條消息添加一個序列號。broker端緩存了已經(jīng)提交消息的序列號,只有比緩存分區(qū)中最后提交消息的序列號大1的消息才會被接受,其他會被拒絕。
生產(chǎn)端消息發(fā)送流程的冪等處理

下面簡單介紹下支持冪等的消息發(fā)送端工作流程

  1. 生產(chǎn)端通過Kafkaproducer會將數(shù)據(jù)添加到RecordAccumulator中,數(shù)據(jù)添加時會判斷是否需要新建一個ProducerBatch。

  2. 生產(chǎn)端后臺啟動發(fā)送線程,會判斷當(dāng)前的PID是否需要重置,重置的原因是因為某些消息分區(qū)的batch重試多次仍然失敗最后因為超時而被移除,這個時候序列號無法連續(xù),導(dǎo)致后續(xù)消息無法發(fā)送,因此會重置PID,并將相關(guān)緩存信息清空,這個時候消息會丟失。

  3. 發(fā)送線程判斷是否需要新申請PID,如果需要則會阻塞直到獲取到PID信息。

  4. 發(fā)送線程在調(diào)用sendProducerData()方法發(fā)送數(shù)據(jù)時,會進(jìn)行以下判斷:

  • 判斷主題分區(qū)是否可以繼續(xù)發(fā)送、PID是否有效、如果是重試batch需要判斷之前的batch是否發(fā)送完成,如果沒有發(fā)送完成則會跳過當(dāng)前主題分區(qū)的消息發(fā)送,直到前面的batch發(fā)送完成。

  • 如果對應(yīng)ProducerBatch沒有分配對應(yīng)的PID與序列號信息,則會在這里進(jìn)行設(shè)置。

服務(wù)端消息接受流程的冪等處理

服務(wù)端(broker)在收到生產(chǎn)端發(fā)送的數(shù)據(jù)寫請求之后,會進(jìn)行一些判斷來決定是否可以寫入數(shù)據(jù),這里也主要介紹關(guān)于冪等相關(guān)的操作流程。

  1. 如果請求設(shè)置了冪等特性,則會檢查是否對ClusterResource有IdempotentWrite權(quán)限,如果沒有,則會返回錯誤CLUSTER_AUTHORIZATION_FAILED。
  2. 檢查是否有PID信息
  3. 根據(jù)batch的序列號檢查該batch是否重復(fù),服務(wù)端會緩存每個PID對應(yīng)主題分區(qū)的最近5個batch信息,如果有重復(fù),則直接返回寫入成功,但是不會執(zhí)行真正的數(shù)據(jù)寫入操作。
  4. 如果有PID且非重復(fù)batch,則進(jìn)行以下操作:
    • 判斷該P(yáng)ID是否已經(jīng)存在緩存中。
    • 如果不存在則判斷序列號是否是從0開始,如果是則表示為新的PID,在緩存中記錄PID的信息(包括PID、epoch以及序列號信息),然后執(zhí)行數(shù)據(jù)寫入操作;如果不存在但是序列號不是從0開始,則直接返回錯誤,表示PID在服務(wù)端以及過期或者PID寫的數(shù)據(jù)已經(jīng)過期。
    • 如果PID存在,則會檢查PID的epoch版本是否與服務(wù)端一致,如果不一致且序列號不是從0開始,則返回錯誤。如果epoch不一致但是序列號是從0開始,則可以正常寫入。
    • 如果epoch版本一致,則會查詢緩存中最近一次序列號是否連續(xù),不連續(xù)則會返回錯誤,否則正常寫入。

2.6. Consumer

假設(shè)這么個場景:我們從 Kafka 中讀取消息,并且進(jìn)行檢查,最后產(chǎn)生結(jié)果數(shù)據(jù)。

我們可以創(chuàng)建一個消費(fèi)者實例去做這件事情,但如果生產(chǎn)者寫入消息的速度比消費(fèi)者讀取的速度快怎么辦呢?

這樣隨著時間增長,消息堆積越來越嚴(yán)重。對于這種場景,我們需要增加多個消費(fèi)者來進(jìn)行水平擴(kuò)展。

Kafka 消費(fèi)者是消費(fèi)組的一部分,當(dāng)多個消費(fèi)者形成一個消費(fèi)組來消費(fèi)主題時,每個消費(fèi)者會收到不同分區(qū)的消息。

假設(shè)有一個 T1 主題,該主題有 4 個分區(qū);同時我們有一個消費(fèi)組 G1,這個消費(fèi)組只有一個消費(fèi)者 C1。那么消費(fèi)者 C1 將會收到這 4 個分區(qū)的消息。

如果我們增加新的消費(fèi)者 C2 到消費(fèi)組 G1,那么每個消費(fèi)者將會分別收到兩個分區(qū)的消息。相當(dāng)于 T1 Topic 內(nèi)的 Partition 均分給了 G1 消費(fèi)的所有消費(fèi)者,在這里 C1 消費(fèi) P0 和 P2,C2 消費(fèi)P1 和 P3。

如果增加到 4 個消費(fèi)者,那么每個消費(fèi)者將會分別收到一個分區(qū)的消息。這時候每個消費(fèi)者都處理其中一個分區(qū),滿負(fù)載運(yùn)行。

但如果我們繼續(xù)增加消費(fèi)者到這個消費(fèi)組,剩余的消費(fèi)者將會空閑,不會收到任何消息。

總而言之,我們可以通過增加消費(fèi)組的消費(fèi)者來進(jìn)行水平擴(kuò)展提升消費(fèi)能力。

這也是為什么建議創(chuàng)建主題時使用比較多的分區(qū)數(shù),這樣可以在消費(fèi)負(fù)載高的情況下增加消費(fèi)者來提升性能。

另外,消費(fèi)者的數(shù)量不應(yīng)該比分區(qū)數(shù)多,因為多出來的消費(fèi)者是空閑的,沒有任何幫助。

如果我們的 C1 處理消息仍然還有瓶頸,我們?nèi)绾蝺?yōu)化和處理?
把 C1 內(nèi)部的消息進(jìn)行二次 Sharding,開啟多個Goroutine Worker 進(jìn)行消費(fèi),為了保障 Offset 提交的正確性,需要使用 WaterMark 機(jī)制,保障最小的 Offset 保存,才能往 Broker 提交。

2.7. Consumer Group

Kafka 一個很重要的特性就是,只需寫入一次消息,可以支持任意多的應(yīng)用讀取這
個消息。

使用Consumer high level API時,同一Topic的一條消息只能被同一個Consumer Group內(nèi)的一個Consumer消費(fèi),但多個Consumer Group可同時消費(fèi)這一消息。


這是Kafka用來實現(xiàn)一個Topic消息的廣播(發(fā)給所有的Consumer)和單播(發(fā)給某一個Consumer)的手段。一個Topic可以對應(yīng)多個Consumer Group。如果需要實現(xiàn)廣播,只要每個Consumer有一個獨立的Group就可以了。要實現(xiàn)單播只要所有的Consumer在同一個Group里。用Consumer Group還可以將Consumer進(jìn)行自由的分組而不需要多次發(fā)送消息到不同的Topic。

下面這個例子更清晰地展示了Kafka Consumer Group的特性。首先創(chuàng)建一個Topic (名為topic1,包含3個Partition),然后創(chuàng)建一個屬于group1的Consumer實例,并創(chuàng)建三個屬于group2的Consumer實例,最后通過Producer向topic1發(fā)送key分別為1,2,3的消息。結(jié)果發(fā)現(xiàn)屬于group1的Consumer收到了所有的這三條消息,同時group2中的3個Consumer分別收到了key為1,2,3的消息。如下圖所示。
2.7.1 Rebalance

可以看到,當(dāng)新的消費(fèi)者加入消費(fèi)組,它會消費(fèi)一個或多個分區(qū),而這些分區(qū)之前是由其他消費(fèi)者負(fù)責(zé)的。另外,當(dāng)消費(fèi)者離開消費(fèi)組(比如重啟、宕機(jī)等)時,它所消費(fèi)的分區(qū)會分配給其他分區(qū)。這種現(xiàn)象稱為重平衡(Rebalance)

重平衡是 Kafka 一個很重要的性質(zhì),這個性質(zhì)保證了高可用和水平擴(kuò)展。不過也需要注意到,在重平衡期間,所有消費(fèi)者都不能消費(fèi)消息,因此會造成整個消費(fèi)組短暫的不可用

而且,將分區(qū)進(jìn)行重平衡也會導(dǎo)致原來的消費(fèi)者狀態(tài)過期,從而導(dǎo)致消費(fèi)者需要重新更新狀態(tài),這段期間也會降低消費(fèi)性能。

消費(fèi)者通過定期發(fā)送心跳(Hearbeat)到一個作為組協(xié)調(diào)者(Group Coordinator)的 Broker 來保持在消費(fèi)組內(nèi)存活。這個 Broker 不是固定的,每個消費(fèi)組都可能不同。當(dāng)消費(fèi)者拉取消息或者提交時,便會發(fā)送心跳。如果消費(fèi)者超過一定時間沒有發(fā)送心跳,那么它的會話(Session)就會過期,組協(xié)調(diào)者會認(rèn)為該消費(fèi)者已經(jīng)宕機(jī),然后觸發(fā)重平衡。

可以看到,從消費(fèi)者宕機(jī)到會話過期是有一定時間的,這段時間內(nèi)該消費(fèi)者的分區(qū)都不能進(jìn)行消息消費(fèi)。通常情況下,我們可以進(jìn)行優(yōu)雅關(guān)閉,這樣消費(fèi)者會發(fā)送離開的消息到組協(xié)調(diào)者,這樣組協(xié)調(diào)者可以立即進(jìn)行重平衡而不需要等待會話過期。

在 0.10.1 版本,Kafka 對心跳機(jī)制進(jìn)行了修改,將發(fā)送心跳與拉取消息進(jìn)行分離,這樣使得發(fā)送心跳的頻率不受拉取的頻率影響
另外更高版本的 Kafka 支持配置一個消費(fèi)者多長時間不拉取消息但仍然保持存活,這個配置可以避免活鎖(livelock)。活鎖,是指應(yīng)用沒有故障但是由于某些原因不能進(jìn)一步消費(fèi)。
但是活鎖也很容易導(dǎo)致連鎖故障,當(dāng)消費(fèi)端下游的組件性能退化,那么消息消費(fèi)會變的很慢,會很容易出發(fā)livelock 的重新均衡機(jī)制,反而影響吞吐。

2.8. Push vs. Pull

作為一個消息系統(tǒng),Kafka遵循了傳統(tǒng)的方式,選擇由Producer向broker push消息并由Consumer從broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,采用push模式。事實上,push模式和pull模式各有優(yōu)劣。
  push模式很難適應(yīng)消費(fèi)速率不同的消費(fèi)者,因為消息發(fā)送速率是由broker決定的。push模式的目標(biāo)是盡可能以最快速度傳遞消息,但是這樣很容易造成Consumer來不及處理消息,典型的表現(xiàn)就是拒絕服務(wù)以及網(wǎng)絡(luò)擁塞。而pull模式則可以根據(jù)Consumer的消費(fèi)能力以適當(dāng)?shù)乃俾氏M(fèi)消息。
  對于Kafka而言,pull模式更合適。pull模式可簡化broker的設(shè)計,Consumer可自主控制消費(fèi)消息的速率,同時Consumer可以自己控制消費(fèi)方式——即可批量消費(fèi)也可逐條消費(fèi),同時還能選擇不同的提交方式從而實現(xiàn)不同的傳輸語義。

2.9. Kafka消息交付的保證性

有這么幾種可能的消息交付的保證性(delivery guarantee):

  1. At most once 消息可能會丟,但絕不會重復(fù)傳輸
  2. At least one 消息絕不會丟,但可能會重復(fù)傳輸
  3. Exactly once 每條消息肯定會被傳輸一次且僅傳輸一次,很多時候這是用戶所想要的
2.9.1 At most once
  1. 讀完消息先commit再處理消息。這種模式下,如果Consumer在commit后還沒來得及處理消息就crash了,下次重新開始工作后就無法讀到剛剛已提交而未處理的消息,這就對應(yīng)于 at most once (消息會丟,但不重復(fù))
2.9.2 At least one
  1. 當(dāng) ProducerBroker 發(fā)送數(shù)據(jù)后,會進(jìn)行 commit,如果commit成功,由于 Replica 副本機(jī)制的存在,則意味著消息不會丟失。但是 Producer 發(fā)送數(shù)據(jù)給 Broker 后,遇到網(wǎng)絡(luò)問題而造成通信中斷,那么 Producer 就無法準(zhǔn)確判斷該消息是否已經(jīng)被提交(commit),這就可能造成 at least once(消息絕不會丟,但可能會重復(fù)傳輸)
  2. 讀完消息先處理再commit。這種模式下,如果在處理完消息之后commit之前Consumer crash了,下次重新開始工作時還會處理剛剛未commit的消息,實際上該消息已經(jīng)被處理過了。這就對應(yīng)于 at least once (消息不丟,但被多次重復(fù)處理)
2.9.3 Exactly once
  1. 在 Kafka 0.11.0.0 之前, 如果 Producer 沒有收到消息 commit 的響應(yīng)結(jié)果,它只能重新發(fā)送消息,確保消息已經(jīng)被正確的傳輸?shù)?Broker,重新發(fā)送的時候會將消息再次寫入日志中;而在 0.11.0.0 版本之后,** Producer 支持冪等傳遞選項,保證重新發(fā)送不會導(dǎo)致消息在日志出現(xiàn)重復(fù)**。為了實現(xiàn)這個, BrokerProducer 分配了一個ID,發(fā)往同一 Partition 的消息會附帶Sequence Number。并通過每條消息的序列號進(jìn)行去重。也支持了類似事務(wù)語義來保證將消息發(fā)送到多個 Topic 分區(qū)中,保證所有消息要么都寫入成功,要么都失敗,這個主要用在 Topic 之間的 exactly once(每條消息肯定會被傳輸一次且僅傳輸一次)

    其中啟用冪等傳遞的方法配置:enable.idempotence = true

    啟用事務(wù)支持的方法配置:設(shè)置屬性 transcational.id = "指定值"

3. Kafka高可用設(shè)計

3.1 Replication

Kafka 在0.8以前的版本中,并不提供 HA 機(jī)制,一旦一個或多個 Broker 宕機(jī),則宕機(jī)期間其上所有 Partition 都無法繼續(xù)提供服務(wù)。若該 Broker 永遠(yuǎn)不能再恢復(fù),亦或磁盤故障,則其上數(shù)據(jù)將丟失。

在沒有 Replication 的情況下,一旦某機(jī)器宕機(jī)或者某個 Broker 停止工作則會造成整個系統(tǒng)的可用性降低。隨著集群規(guī)模的增加,整個集群中出現(xiàn)該類異常的幾率大大增加,因此對于生產(chǎn)系統(tǒng)而言 Replication 機(jī)制的引入非常重要。

為了更好的做負(fù)載均衡,Kafka盡量將所有的Partition均勻分配到整個集群上。一個典型的部署方式是一個Topic的Partition數(shù)量大于Broker的數(shù)量。同時為了提高Kafka的容錯能力,也需要將同一個Partition的Replica盡量分散到不同的機(jī)器。實際上,如果所有的Replica都在同一個Broker上,那一旦該Broker宕機(jī),該P(yáng)artition的所有Replica都無法工作,也就達(dá)不到HA的效果。同時,如果某個Broker宕機(jī)了,需要保證它上面的負(fù)載可以被均勻的分配到其它幸存的所有Broker上。
  
Kafka分配Replica的算法如下:

  • 將所有Broker(假設(shè)共n個Broker)和待分配的Partition排序
  • 將第i個Partition分配到第(i mod n)個Broker上
  • 將第i個Partition的第j個Replica分配到第((i + j) mod n)個Broker上

Kafka的Data Replication 需要解決如下問題:

  • 怎樣傳播消息
  • 在向Producer發(fā)送ACK前需要保證有多少個Replica已經(jīng)收到該消息
  • 怎樣處理某個Replica不工作的情況
  • 怎樣處理Failed Replica恢復(fù)回來的情況
3.2 怎樣傳播消息
  1. Producer在發(fā)布消息到某個Partition時,先通過 Metadata (通過 Broker 獲取并且緩存在 Producer 內(nèi)) 找到該 Partition 的Leader,Producer只將該消息發(fā)送到該P(yáng)artition的Leader。 Leader會將該消息寫入其本地Log。
  2. 每個Follower都從Leader pull數(shù)據(jù)。Follower存儲的數(shù)據(jù)順序與Leader保持一致。Follower在收到該消息后,立即向Leader發(fā)送ACK, 而后將數(shù)據(jù)寫入其Log。
  3. 一旦Leader收到了ISR中的所有Replica的ACK,該消息就被認(rèn)為已經(jīng)commit了,Leader將增加HW并且向Producer發(fā)送ACK。

為了提高性能,每個Follower在接收到數(shù)據(jù)后就立馬向Leader發(fā)送ACK,而非等到數(shù)據(jù)寫入Log中。因此,對于已經(jīng)commit的消息,Kafka只能保證它被存于多個Replica的內(nèi)存中,而不能保證它們被持久化到磁盤中,也就不能完全保證異常發(fā)生后該條消息一定能被Consumer消費(fèi)。但考慮到這種場景非常少見,可以認(rèn)為這種方式在性能和數(shù)據(jù)持久化上做了一個比較好的平衡。在將來的版本中,Kafka會考慮提供更高的持久性。

Consumer讀消息也是從Leader讀取,只有被commit過的消息(offset低于HW的消息)才會暴露給Consumer。

Kafka Replication的數(shù)據(jù)流如下圖所示:
3.3 向Producer發(fā)送ACK前需要保證有多少個Replica已經(jīng)收到該消息

Kafka處理失敗需要明確定義一個Broker是否“活著”。對于Kafka而言,Kafka存活包含兩個條件:

  1. 它必須維護(hù)與Zookeeper的session(這個通過Zookeeper的Heartbeat機(jī)制來實現(xiàn))
  2. 從副本的最后一條消息的 Offset 需要與主副本的最后一條消息 Offset 差值不超過設(shè)定閾值(replica.lag.max.messages)或者副本的 LEO 落后于主副本的 LEO 時長不大于設(shè)定閾值(replica.lag.time.max.ms),官方推薦使用后者判斷,并在新版本 Kafka0.10.0 移除了replica.lag.max.messages 參數(shù)。

Leader會跟蹤與其保持同步的Replica列表,該列表稱為ISR(即in-sync Replica)。如果一個Follower宕機(jī),或者落后太多,Leader將把它從ISR中移除。當(dāng)其再次滿足以上條件之后又會被重新加入集合中。

ISR 的引入主要是解決同步副本與異步復(fù)制兩種方案各自的缺陷:

  • 同步副本中如果有個副本宕機(jī)或者超時就會拖慢該副本組的整體性能。
  • 如果僅僅使用異步副本,當(dāng)所有的副本消息均遠(yuǎn)落后于主副本時,一旦主副本宕機(jī)重新選舉,那么就會存在消息丟失情況。

Follower可以批量的從Leader復(fù)制數(shù)據(jù),這樣極大的提高復(fù)制性能(批量寫磁盤),極大減少了Follower與Leader的差距

一條消息只有被ISR里的所有Follower都從Leader復(fù)制過去才會被認(rèn)為已提交。這樣就避免了部分?jǐn)?shù)據(jù)被寫進(jìn)了Leader,還沒來得及被任何Follower復(fù)制就宕機(jī)了,而造成數(shù)據(jù)丟失(Consumer無法消費(fèi)這些數(shù)據(jù))。而對于Producer而言,它可以選擇是否等待消息commit,這可以通過request.required.acks來設(shè)置。這種機(jī)制確保了只要ISR有一個或以上的Follower,一條被commit的消息就不會丟失。

3.4 主從數(shù)據(jù)同步流程詳解

初始時 Leader 和 Follower 的 HW(High Watermark)LEO(Log End Offset) 都是0。Leader 中的 remote LEO 指的就是Leader 端保存的 follower LEO,也被初始化成 0。

此時, Producer 沒有發(fā)送任何消息給 Leader,而 Follower 已經(jīng)開始不斷地給 Leader 發(fā)送Fetch 請求了,但因為沒有數(shù)據(jù)因此什么都不會發(fā)生。值得一提的是,F(xiàn)ollower 發(fā)送過來的Fetch 請求因為無數(shù)據(jù)而暫時會被寄存到 Leader 端的 purgatory 中,待 500ms(replica.fetch.wait.max.ms參數(shù))超時后會強(qiáng)制完成。倘若在寄存期間 Producer 端發(fā)送過來數(shù)據(jù),那么會Kafka 會自動喚醒該 FETCH 請求,讓 Leader 繼續(xù)處理之。

Follower 發(fā)送 Fetch 請求在 Leader 處理完 Producer 請求之后。Producer 給該 Topic 分區(qū)發(fā)送了一條消息。


  • 把消息寫入寫底層 Log(同時也就自動地更新了 Leader 的 LEO)。
  • 嘗試更新 Leader HW 值。我們已經(jīng)假設(shè)此時 Follower 尚未發(fā)送 Fetch 請求,那么 Leader 端保存的 remote LEO 依然是0,因此 Leader 會比較它自己的 LEO 值和 remote LEO 值,發(fā)現(xiàn)最小值是 0,與當(dāng)前 HW 值相同,故不會更新分區(qū) HW 值。

所以,Produce 請求處理完成后 Leader 端的 HW 值依然是0,而 LEO 是1,remoteLEO 是1。假設(shè)此時 Follower 發(fā)送了 Fetch 請求。

本例中當(dāng) Follower 發(fā)送 Fetch 請求時,Leader 端的處理依次是:
? 讀取底層 Log 數(shù)據(jù)。
? 更新 remote LEO = 0(為什么是 0? 因為此時 Follower 還沒有寫入這條消息。Leader 如何
確認(rèn) Follower 還未寫入呢?這是通過 Follower 發(fā)來的 Fetch 請求中的 Fetch Offset 來確定
的)。
? 嘗試更新分區(qū) HW —— 此時 Leader LEO = 1,remote LEO = 0,故分區(qū) HW 值= min(leader
LEO, follower remote LEO) = 0。
? 把數(shù)據(jù)和當(dāng)前分區(qū) HW 值(依然是0)發(fā)送給 Follower 副本。

而 Follower 副本接收到 Fetch Response 后依次執(zhí)行下列操作:
? 寫入本地 Log(同時更新 Follower LEO)。
? 更新 Follower HW —— 比較本地 LEO 和當(dāng)前 Leader HW 取小者,故 Follower HW = 0。

此時,第一輪 Fetch RPC 結(jié)束,我們會發(fā)現(xiàn)雖然 Leader 和 Follower 都已經(jīng)在 Log 中保存了這條消息,但分區(qū) HW 值尚未被更新。實際上,它是在第二輪 Fetch RPC 中被更新的。

Follower 發(fā)來了第二輪 Fetch 請求,Leader 端接收到后仍然會依次執(zhí)行下列操作:
? 讀取底層 Log 數(shù)據(jù)。
? 更新 remote LEO = 1(這次為什么是1了? 因為這輪 FETCH RPC 攜帶的 Fetch Offset 是1,那么為什么這輪攜帶的就是1了呢,因為上一輪結(jié)束后 Follower LEO 被更新為1了)。
? 嘗試更新分區(qū) HW —— 此時 Leader LEO = 1,remote LEO = 1,故分區(qū) HW 值= min(leader LEO, follower remote LEO) = 1。
? 把數(shù)據(jù)(實際上沒有數(shù)據(jù))和當(dāng)前分區(qū) HW 值(已更新為1)發(fā)送給 Follower 副本。

同樣地,F(xiàn)ollower 副本接收到 Fetch Response 后依次執(zhí)行下列操作:
? 寫入本地 Log,當(dāng)然沒東西可寫,故 Follower LEO 也不會變化,依然是1。
? 更新 Follower HW —— 比較本地 LEO 和當(dāng)前 Leader HW 取小者。由于此時兩者都是1,故更新 Follower HW = 1 。
? Producer 端發(fā)送消息后 Broker 端完整的處理流程就講完了。此時消息已經(jīng)成功地被復(fù)制到Leader 和 Follower 的 Log 中且分區(qū) HW 是1,表明 Consumer 能夠消費(fèi) offset = 0 的這條消息。下面我們來分析下 Produce 和 Fetch 請求交互的第二種情況。

第二種情況:Fetch 請求保存在 purgatory 中 Produce 請求到來。
這種情況實際上和第一種情況差不多。前面說過了,當(dāng) Leader 無法立即滿足 Fetch 返回要求的時候(比如沒有數(shù)據(jù)),那么該 Fetch 請求會被暫存到 Leader 端的purgatory 中,待時機(jī)成熟時會嘗試再次處理它。不過 Kafka 不會無限期地將其緩存著,默認(rèn)有個超時時間(500ms),一旦超時時間已過,則這個請求會被強(qiáng)制完成。不過我們要討論的場景是在寄存期間,Producer 發(fā)送 Produce 請求從而使之滿足了條件從而被喚醒。

此時,Leader 端處理流程如下:

  • Leader 寫入本地 Log(同時自動更新 Leader LEO)。
  • 嘗試喚醒在 purgatory 中寄存的 Fetch 請求。
  • 嘗試更新分區(qū) HW。
數(shù)據(jù)丟失場景(更新了LEO,但未更新HW時,主從先后故障)

初始情況為主副本 A 已經(jīng)寫入了兩條消息,對應(yīng) HW=1,LEO=2,LEOB=1,從副本 B 寫入了一條消息,對應(yīng)HW=1,LEO=1。
  • 此時從副本 B 向主副本 A 發(fā)起 fetchOffset=1 請求,主副本收到請求之后更新LEOB=1,表示副本 B 已經(jīng)收到了消息0,然后嘗試更新 HW 值,in(LEO,LEOB)=1,即不需要更新,然后將消息1以及當(dāng)前分區(qū) HW=1 返回給從副本 B,從副本 B 收到響應(yīng)之后寫入日志并更新LEO=2,然后更新其 HW=1,雖然已經(jīng)寫入了兩條消息,但是 HW 值需要在下一輪的請求才會更新為2。
  • 此時從副本 B 重啟,重啟之后會根據(jù) HW 值進(jìn)行日志截斷,即消息1會被刪除。
  • 從副本 B 向主副本 A 發(fā)送 fetchOffset=1 請求,如果此時主副本 A 沒有什么異常,則跟第二步一樣沒有什么問題,假設(shè)此時主副本也宕機(jī)了,那么從副本 B 會變成主副本。
  • 當(dāng)副本 A 恢復(fù)之后會變成從副本并根據(jù) HW 值進(jìn)行日志截斷,即把消息 1 丟失,此時消息 1 就永久丟失了。
數(shù)據(jù)不一致場景(更新了LEO,但未更新HW時,舊主故障,從成為主并寫入了新數(shù)據(jù),舊主恢復(fù)后成為從,主從HW一致但數(shù)據(jù)不一致)
  • 初始狀態(tài)為主副本 A 已經(jīng)寫入了兩條消息對應(yīng)HW=1,LEO=2,LEOB=1,從副本 B 也同步了兩條消息,對應(yīng) HW=1,LEO=2。
  • 此時從副本 B 向主副本發(fā)送 fetchOffset=2 請求,主副本 A 在收到請求后更新分區(qū) HW=2 并將該值返回給從副本 B,如果此時從副本 B 宕機(jī)則會導(dǎo)致HW 值寫入失敗。
  • 我們假設(shè)此時主副本 A 也宕機(jī)了,從副本 B 先恢復(fù)并成為主副本,此時會發(fā)生日志截斷,只保留消息 0,然后對外提供服務(wù),假設(shè)外部寫入了一個消息 1(這個消息與之前的消息 1不一樣,用不同的顏色標(biāo)識不同消息)。
  • 等副本 A 起來之后會變成從副本,不會發(fā)生日志截斷,因為 HW=2,但是對應(yīng)位移 1 的消息其實是不一致的。
Leader Eepoch

為了解決數(shù)據(jù)丟失及數(shù)據(jù)不一致的問題,在新版的 Kafka(0.11.0.0)引入了Leader Epoch 概念。

Leader Epoch 表示一個鍵值對 <epoch, offset>,其中 Eepoch 表示 Leader 主副本的版本號,從 0 開始編碼,當(dāng) Leader 每變更一次就會+1,Offset 表示該 Epoch 版本的主副本寫入第一條消息的位置。

比如 <0,0> 表示第一個主副本從位移 0 開始寫入消息,<1,100> 表示第二個主副本版本號為1并從位移 100 開始寫入消息,主副本會將該信息保存在緩存中并定期寫入到 CheckPoint 文件中,每次發(fā)生主副本切換都會去從緩存中查詢該信息。

引入了Leader Eepoch后的數(shù)據(jù)丟失場景:


如圖所示,當(dāng)從副本 B 重啟之后向主副本 A 發(fā)送offsetsForLeaderEpochRequest,Epoch 主從副本相等,則 A 返回當(dāng)前的 LEO=2,從副本 B 中沒有任何大于2 的位移,因此不需要截斷。

  • 當(dāng)從副本 B 向主副本 A 發(fā)送 fetchoffset=2 請求時,A宕機(jī),所以從副本 B 成為主副本,并更新 Epoch 值為<epoch=1, offset=2>,HW 值更新為 2。
  • 當(dāng) A 恢復(fù)之后成為從副本,并向 B 發(fā)送 fetcheOffset=2請求,B 返回 HW=2,則從副本 A 更新 HW=2。
  • 主副本 B 接受外界的寫請求,從副本 A 向主副本 A 不斷
    發(fā)起數(shù)據(jù)同步請求。

從上可以看出引入 Leader Epoch 值之后避免了前面提到的數(shù)據(jù)丟失情況,但是這里需要注意的是如果在上面的第一步,從副本 B 起來之后向主副本 A 發(fā)送offsetsForLeaderEpochRequest 請求失敗,即主副本 A同時也宕機(jī)了,那么消息 1 就會丟失,具體可見下面數(shù)據(jù)不一致場景中有提到。
引入了Leader Eepoch后的數(shù)據(jù)不一致場景:

  • 從副本 B 恢復(fù)之后向主副本 A 發(fā)送offsetsForLeaderEpochRequest 請求,由于主
    副本也宕機(jī)了,因此副本 B 將變成主副本并將消息1 截斷,此時接受到新消息 1 的寫入。
  • 副本 A 恢復(fù)之后變成從副本并向主副本 A 發(fā)送offsetsForLeaderEpochRequest 請求,請求的Epoch 值小于主副本 B,因此主副本 B 會返回epoch=1 時的開始位移,即 lastoffset=1,因此從副本 A 會截斷消息 1。
  • 從副本 A 從主副本 B 拉取消息,并更新 Epoch 值<epoch=1, offset=1>。

可以看出 Epoch 的引入可以避免數(shù)據(jù)不一致,但是兩個副本均宕機(jī),則還是存在數(shù)據(jù)丟失的場景。

3.5 Leader Election

引入 Replication 之后,同一個 Partition 可能會有多個 Replica,而這時需要在這些Replication 之間選出一個 Leader,Producer 和 Consumer 只與這個 Leader 交互,其它 Replica 作為 Follower 從 Leader 中復(fù)制數(shù)據(jù)。

因為需要保證同一個 Partition 的多個 Replica 之間的數(shù)據(jù)一致性(其中一個宕機(jī)后其它 Replica必須要能繼續(xù)服務(wù)并且即不能造成數(shù)據(jù)重復(fù)也不能造成數(shù)據(jù)丟失)。

如果沒有一個 Leader,所有 Replica 都可同時讀/寫數(shù)據(jù),那就需要保證多個 Replica 之間互相(N×N 條通路)同步數(shù)據(jù),數(shù)據(jù)的一致性和有序性非常難保證,大大增加了 Replication 實現(xiàn)的復(fù)雜性,同時也增加了出現(xiàn)異常的幾率。而引入 Leader 后,只有 Leader 負(fù)責(zé)數(shù)據(jù)讀寫,F(xiàn)ollower 只向 Leader 順序 Fetch 數(shù)據(jù)(N 條通路),系統(tǒng)更加簡單且高效。

由于 Kafka 集群依賴 ZooKeeper 集群,所以最簡單最直觀的方案是,所有 Follower都在 ZooKeeper 上設(shè)置一個 Watch,一旦 Leader 宕機(jī),其對應(yīng)的 Ephemeral Znode 會自動刪除,此時所有 Follower 都嘗試創(chuàng)建該節(jié)點,而創(chuàng)建成功者(ZooKeeper 保證只有一個能創(chuàng)建成功)即是新的 Leader,其它 Replica 即為Follower。

前面的方案有以下缺點:

  • Split-Brain (腦裂): 這是由 ZooKeeper 的特性引起的,雖然 ZooKeeper 能保證所有Watch 按順序觸發(fā),但并不能保證同一時刻所有 Replica “看”到的狀態(tài)是一樣的,這就可能造成不同 Replica 的響應(yīng)不一致 。
  • Herd Effect (羊群效應(yīng)): 如果宕機(jī)的那個 Broker 上的 Partition 比較多,會造成多個Watch 被觸發(fā),造成集群內(nèi)大量的調(diào)整。
  • ZooKeeper( 負(fù)載過重) : 每個 Replica 都要為此在 ZooKeeper 上注冊一個 Watch,當(dāng)集群規(guī)模增加到幾千個 Partition 時 ZooKeeper 負(fù)載會過重。
Controller

Kafka 的 Leader Election 方案解決了上述問題,它在所有 Broker 中選出一個Controller,所有 Partition 的 Leader 選舉都由 Controller 決定。Controller 會將Leader 的改變直接通過 RPC 的方式(比 ZooKeeper Queue 的方式更高效)通知需為此作為響應(yīng)的 Broker。

Kafka 集群 Controller 的選舉過程如下 :

  • 每個 Broker 都會在 Controller Path (/controller)上注冊一個 Watch。
  • 當(dāng)前 Controller 失敗時,對應(yīng)的 Controller Path 會自動消失(因為它是 Ephemeral Node),此時該 Watch 被 fire,所有“活”著的 Broker 都會去競選成為新的 Controller(創(chuàng)建新的Controller Path),但是只會有一個競選成功(這點由 ZooKeeper 保證)。
  • 競選成功者即為新的 Leader,競選失敗者則重新在新的 Controller Path 上注冊 Watch。因為ZooKeeper 的 Watch 是一次性的,被 fire 一次之后即失效,所以需要重新注冊。
    Kafka Partition Leader 的選舉過程如下 (由 Controller 執(zhí)行):
  • 從 ZooKeeper 中讀取當(dāng)前分區(qū)的所有 ISR(in-sync replicas)集合。
  • 調(diào)用配置的分區(qū)選擇算法選擇分區(qū)的 Leader。

Kafka在Zookeeper中動態(tài)維護(hù)了一個ISR(in-sync replicas),這個ISR里的所有Replica都跟上了leader,只有ISR里的成員才有被選為Leader的可能。在這種模式下,對于f+1個Replica,一個Partition能在保證不丟失已經(jīng)commit的消息的前提下容忍f個Replica的失敗。在大多數(shù)使用場景中,這種模式是非常有利的。事實上,為了容忍f個Replica的失敗,Majority Vote和ISR在commit前需要等待的Replica數(shù)量是一樣的,但是ISR需要的總的Replica的個數(shù)幾乎是Majority Vote的一半。

3.6. 如何處理所有Replica都不工作

上文提到,在ISR中至少有一個follower時,Kafka可以確保已經(jīng)commit的數(shù)據(jù)不丟失,但如果某個Partition的所有Replica都宕機(jī)了,就無法保證數(shù)據(jù)不丟失了。這種情況下有兩種可行的方案:

  • 等待ISR中的任一個Replica“活”過來,并且選它作為Leader(強(qiáng)一致性,不可用時間相對較長)
  • 選擇第一個“活”過來的Replica(不一定是ISR中的)作為Leader(高可用性)

Kafka0.8.*使用了第二種方式。根據(jù)Kafka的文檔,在以后的版本中,Kafka支持用戶通過配置選擇這兩種方式中的一種,從而根據(jù)不同的使用場景選擇高可用性還是強(qiáng)一致性。

3.7 broker故障恢復(fù)過程
  1. Controller在Zookeeper注冊Watch,一旦有Broker宕機(jī)(這是用宕機(jī)代表任何讓系統(tǒng)認(rèn)為其die的情景,包括但不限于機(jī)器斷電,網(wǎng)絡(luò)不可用,GC導(dǎo)致的Stop The World,進(jìn)程crash等),其在Zookeeper對應(yīng)的znode會自動被刪除,Zookeeper會fire Controller注冊的watch,Controller讀取最新的幸存的Broker
  2. Controller決定set_p,該集合包含了宕機(jī)的所有Broker上的所有Partition
  3. 對set_p中的每一個Partition執(zhí)行以下操作:
    3.1. /brokers/topics/[topic]/partitions/[partition]/state讀取該Partition當(dāng)前的ISR
    3.2. 決定該P(yáng)artition的新Leader。如果當(dāng)前ISR中有至少一個Replica還幸存,則選擇其中一個作為新Leader,新的ISR則包含當(dāng)前ISR中所有幸存的Replica。否則選擇該P(yáng)artition中任意一個幸存的Replica作為新的Leader以及ISR(該場景下可能會有潛在的數(shù)據(jù)丟失)。如果該P(yáng)artition的所有Replica都宕機(jī)了,則將新的Leader設(shè)置為-1。
    3.3. 將新的Leader,ISR和新的leader_epoch及controller_epoch寫入/brokers/topics/[topic]/partitions/[partition]/state。注意,該操作只有其version在3.1至3.3的過程中無變化時才會執(zhí)行,否則跳轉(zhuǎn)到3.1
  4. 直接通過RPC向set_p相關(guān)的Broker發(fā)送LeaderAndISRRequest命令。Controller可以在一個RPC操作中發(fā)送多個命令從而提高效率

4.kafka為什么高性能

架構(gòu)層面:
? Partition 級別并行:Broker、Disk、Consumer 端
? ISR:避免同步個別副本時拖慢整體副本組性能,同時還能避免主從節(jié)點間數(shù)據(jù)落后過多導(dǎo)致的消息丟失

I/O 層面:
? Batch 讀寫:減少I/O次數(shù),增加吞吐量
? 磁盤順序 I/O:在某些情況下,順序磁盤訪問比隨機(jī)內(nèi)存訪問更快
? Page Cache:將Index及消息緩存到Page Cache中,提升處理效率
? Zero Copy:減少內(nèi)核態(tài)與用戶態(tài)之間的I/O次數(shù)
? 壓縮:log壓縮及消息壓縮,節(jié)省磁盤空間,節(jié)省字節(jié)大小


References:
https://kafka.apache.org/documentation/#design
http://www.lxweimin.com/p/bde902c57e80
https://mp.weixin.qq.com/s/fX26tCdYSMgwM54_2CpVrw
https://zhuanlan.zhihu.com/p/27587872
https://mp.weixin.qq.com/s/X301soSDWRfOemQhk9AuPw
http://www.jasongj.com/2015/08/09/KafkaColumn1/
http://www.jasongj.com/2015/08/09/KafkaColumn2/
http://www.jasongj.com/2015/08/09/KafkaColumn3/
http://www.jasongj.com/2015/08/09/KafkaColumn4/
http://www.jasongj.com/2015/08/09/KafkaColumn5/
http://www.jasongj.com/2015/08/09/KafkaColumn6/
http://www.jasongj.com/2015/08/09/KafkaColumn7/
https://www.cnblogs.com/wxd0108/p/6519973.html
https://cloud.tencent.com/developer/article/1589157
https://zhuanlan.zhihu.com/p/459610418

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 前言 Kafka最初由Linkedin公司開發(fā),是一個分布式、支持分區(qū)的(partition)、多副本的(repl...
    walkalone_7487閱讀 741評論 0 1
  • 可以實時處理大量數(shù)據(jù),滿足各種需求場景. Hadoop 批處理系統(tǒng)。 Storm/Spark 流式處理引擎 web...
    小周愛吃瓜閱讀 251評論 0 0
  • 講一講分中間件 問題 什么是分布式消息中間件? 消息中間件的作用是什么? 消息中間件的使用場景是什么? 消息中間件...
    MageByte_青葉閱讀 997評論 0 16
  • 一、概述 (一)、kafka的定義 1、定義 1)kafka傳統(tǒng)的定義:kafka是一個分布式的基于發(fā)布/訂閱模式...
    rainple閱讀 737評論 0 0
  • 1、概述 Kafka起初是由LinkedIn公司采用Scala語言開發(fā)的一個多分區(qū)、多副本且基于Zookeeper...
    昆侖楓閱讀 502評論 0 0