Kafka原理及應用

Kafka剖析(一):Kafka背景及架構介紹


Kafka是由LinkedIn開發的一個分佈式的消息系統,使用Scala編寫,它以可水平擴展和高吞吐率而被廣泛使用。目前越來越多的開源分佈式處理系統如Cloudera、Apache Storm、Spark都支持與Kafka集成。InfoQ一直在緊密關注Kafka的應用以及發展,“Kafka剖析”專欄將會從架構設計、實現、應用場景、性能等方面深度解析Kafka。

背景介紹

Kafka創建背景

Kafka是一個消息系統,原本開發自LinkedIn,用作LinkedIn的活動流(Activity Stream)和運營數據處理管道(Pipeline)的基礎。現在它已被多家不同類型的公司 作爲多種類型的數據管道和消息系統使用。

活動流數據是幾乎所有站點在對其網站使用情況做報表時都要用到的數據中最常規的部分。活動數據包括頁面訪問量(Page View)、被查看內容方面的信息以及搜索情況等內容。這種數據通常的處理方式是先把各種活動以日誌的形式寫入某種文件,然後週期性地對這些文件進行統計分析。運營數據指的是服務器的性能數據(CPU、IO使用率、請求時間、服務日誌等等數據)。運營數據的統計方法種類繁多。


Kafka簡介

近年來,活動和運營數據處理已經成爲了網站軟件產品特性中一個至關重要的組成部分,這就需要一套稍微更加複雜的基礎設施對其提供支持。

Kafka是一種分佈式的,基於發佈/訂閱的消息系統。主要設計目標如下:

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

爲何使用消息系統

  • 解耦

    在項目啓動之初來預測將來項目會碰到什麼需求,是極其困難的。消息系統在處理過程中間插入了一個隱含的、基於數據的接口層,兩邊的處理過程都要實現這一接口。這允許你獨立的擴展或修改兩邊的處理過程,只要確保它們遵守同樣的接口約束。

  • 冗餘

    有些情況下,處理數據的過程會失敗。除非數據被持久化,否則將造成丟失。消息隊列把數據進行持久化直到它們已經被完全處理,通過這一方式規避了數據丟失風險。許多消息隊列所採用的"插入-獲取-刪除"範式中,在把一個消息從隊列中刪除之前,需要你的處理系統明確的指出該消息已經被處理完畢,從而確保你的數據被安全的保存直到你使用完畢。

  • 擴展性

    因爲消息隊列解耦了你的處理過程,所以增大消息入隊和處理的頻率是很容易的,只要另外增加處理過程即可。不需要改變代碼、不需要調節參數。擴展就像調大電力按鈕一樣簡單。

  • 靈活性 & 峯值處理能力

    在訪問量劇增的情況下,應用仍然需要繼續發揮作用,但是這樣的突發流量並不常見;如果爲以能處理這類峯值訪問爲標準來投入資源隨時待命無疑是巨大的浪費。使用消息隊列能夠使關鍵組件頂住突發的訪問壓力,而不會因爲突發的超負荷的請求而完全崩潰。

  • 可恢復性

    系統的一部分組件失效時,不會影響到整個系統。消息隊列降低了進程間的耦合度,所以即使一個處理消息的進程掛掉,加入隊列中的消息仍然可以在系統恢復後被處理。

  • 順序保證

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

  • 緩衝

    在任何重要的系統中,都會有需要不同的處理時間的元素。例如,加載一張圖片比應用過濾器花費更少的時間。消息隊列通過一個緩衝層來幫助任務最高效率的執行———寫入隊列的處理會盡可能的快速。該緩衝有助於控制和優化數據流經過系統的速度。

  • 異步通信

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

常用Message Queue對比

  • RabbitMQ

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

  • Redis

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

  • ZeroMQ

    ZeroMQ號稱最快的消息隊列系統,尤其針對大吞吐量的需求場景。ZeroMQ能夠實現RabbitMQ不擅長的高級/複雜的隊列,但是開發人員需要自己組合多種技術框架,技術上的複雜度是對這MQ能夠應用成功的挑戰。ZeroMQ具有一個獨特的非中間件的模式,你不需要安裝和運行一個消息服務器或中間件,因爲你的應用程序將扮演這個服務器角色。你只需要簡單的引用ZeroMQ程序庫,可以使用NuGet安裝,然後你就可以愉快的在應用程序之間發送消息了。但是ZeroMQ僅提供非持久性的隊列,也就是說如果宕機,數據將會丟失。其中,Twitter的Storm 0.9.0以前的版本中默認使用ZeroMQ作爲數據流的傳輸(Storm從0.9版本開始同時支持ZeroMQ和Netty作爲傳輸模塊)。

  • ActiveMQ

    ActiveMQ是Apache下的一個子項目。 類似於ZeroMQ,它能夠以代理人和點對點的技術實現隊列。同時類似於RabbitMQ,它少量代碼就可以高效地實現高級應用場景。

  • Kafka/Jafka

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

Kafka架構

Terminology

  • Broker

    Kafka集羣包含一個或多個服務器,這種服務器被稱爲broker

  • Topic

    每條發佈到Kafka集羣的消息都有一個類別,這個類別被稱爲Topic。(物理上不同Topic的消息分開存儲,邏輯上一個Topic的消息雖然保存於一個或多個broker上但用戶只需指定消息的Topic即可生產或消費數據而不必關心數據存於何處)

  • Partition

    Parition是物理上的概念,每個Topic包含一個或多個Partition.

  • Producer

    負責發佈消息到Kafka broker

  • Consumer

    消息消費者,向Kafka broker讀取消息的客戶端。

  • Consumer Group

    每個Consumer屬於一個特定的Consumer Group(可爲每個Consumer指定group name,若不指定group name則屬於默認的group)。

Kafka拓撲結構

如上圖所示,一個典型的Kafka集羣中包含若干Producer(可以是web前端產生的Page View,或者是服務器日誌,系統CPU、Memory等),若干broker(Kafka支持水平擴展,一般broker數量越多,集羣吞吐率越高),若干Consumer Group,以及一個Zookeeper集羣。Kafka通過Zookeeper管理集羣配置,選舉leader,以及在Consumer Group發生變化時進行rebalance。Producer使用push模式將消息發佈到broker,Consumer使用pull模式從broker訂閱並消費消息。

Topic & Partition

Topic在邏輯上可以被認爲是一個queue,每條消費都必須指定它的Topic,可以簡單理解爲必須指明把這條消息放進哪個queue裏。爲了使得Kafka的吞吐率可以線性提高,物理上把Topic分成一個或多個Partition,每個Partition在物理上對應一個文件夾,該文件夾下存儲這個Partition的所有消息和索引文件。若創建topic1和topic2兩個topic,且分別有13個和19個分區,則整個集羣上會相應會生成共32個文件夾(本文所用集羣共8個節點,此處topic1和topic2 replication-factor均爲1),如下圖所示。

每個日誌文件都是一個log entrie序列,每個log entrie包含一個4字節整型數值(值爲N+5),1個字節的"magic value",4個字節的CRC校驗碼,其後跟N個字節的消息體。每條消息都有一個當前Partition下唯一的64字節的offset,它指明瞭這條消息的起始位置。磁盤上存儲的消息格式如下:

message length : 4 bytes (value: 1+4+n)
"magic" value : 1 byte 
crc : 4 bytes 
payload : n bytes 

這個log entries並非由一個文件構成,而是分成多個segment,每個segment以該segment第一條消息的offset命名並以“.kafka”爲後綴。另外會有一個索引文件,它標明瞭每個segment下包含的log entry的offset範圍,如下圖所示。

因爲每條消息都被append到該Partition中,屬於順序寫磁盤,因此效率非常高(經驗證,順序寫磁盤效率比隨機寫內存還要高,這是Kafka高吞吐率的一個很重要的保證)

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

  
# 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讀取特定消息的時間複雜度爲O(1),即與文件大小無關,所以這裏刪除過期文件與提高Kafka性能無關。選擇怎樣的刪除策略只與磁盤以及具體的需求有關。另外,Kafka會爲每一個Consumer Group保留一些metadata信息——當前消費的消息的position,也即offset。這個offset由Consumer控制。正常情況下Consumer會在消費完一條消息後遞增該offset。當然,Consumer也可將offset設成一個較小的值,重新消費一些消息。因爲offet由Consumer控制,所以Kafka broker是無狀態的,它不需要標記哪些消息被哪些消費過,也不需要通過broker去保證同一個Consumer Group只有一個Consumer能消費某一條消息,因此也就不需要鎖機制,這也爲Kafka的高吞吐率提供了有力保障。

Producer消息路由

Producer發送消息到broker時,會根據Paritition機制選擇將其存儲到哪一個Partition。如果Partition機制設置合理,所有消息可以均勻分佈到不同的Partition裏,這樣就實現了負載均衡。如果一個Topic對應一個文件,那這個文件所在的機器I/O將會成爲這個Topic的性能瓶頸,而有了Partition後,不同的消息可以並行寫入不同broker的不同Partition裏,極大的提高了吞吐率。可以在$KAFKA_HOME/config/server.properties中通過配置項num.partitions來指定新建Topic的默認Partition數量,也可在創建Topic時通過參數指定,同時也可以在Topic創建之後通過Kafka提供的工具修改。

在發送一條消息時,可以指定這條消息的key,Producer根據這個key和Partition機制來判斷應該將這條消息發送到哪個Parition。Paritition機制可以通過指定Producer的paritition. class這一參數來指定,該class必須實現kafka.producer.Partitioner接口。本例中如果key可以被解析爲整數則將對應的整數與Partition總數取餘,該消息會被髮送到該數對應的Partition。(每個Parition都會有個序號,序號從0開始)

import kafka.producer.Partitioner;
import kafka.utils.VerifiableProperties;

public class JasonPartitioner<T> implements Partitioner {

    public JasonPartitioner(VerifiableProperties verifiableProperties) {}

    @Override
    public int partition(Object key, int numPartitions) {
        try {
            int partitionNum = Integer.parseInt((String) key);
            return Math.abs(Integer.parseInt((String) key) % numPartitions);
        } catch (Exception e) {
            return Math.abs(key.hashCode() % numPartitions);
        }
    }
}

如果將上例中的類作爲partition.class,並通過如下代碼發送20條消息(key分別爲0,1,2,3)至topic3(包含4個Partition)。

public void sendMessage() throws InterruptedException{
  for(int i = 1; i <= 5; i++){
        List messageList = new ArrayList<KeyedMessage<String, String>>();
        for(int j = 0; j < 4; j++){
            messageList.add(new KeyedMessage<String, String>("topic2", j+"", "The " + i + " message for key " + j));
        }
        producer.send(messageList);
    }
  producer.close();
}

則key相同的消息會被髮送並存儲到同一個partition裏,而且key的序號正好和Partition序號相同。(Partition序號從0開始,本例中的key也從0開始)。下圖所示是通過Java程序調用Consumer後打印出的消息列表。

Consumer Group

(本節所有描述都是基於Consumer hight level API而非low level API)。

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

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

實際上,Kafka的設計理念之一就是同時提供離線處理和實時處理。根據這一特性,可以使用Storm這種實時流處理系統對消息進行實時在線處理,同時使用Hadoop這種批處理系統進行離線處理,還可以同時將數據實時備份到另一個數據中心,只需要保證這三個操作所使用的Consumer屬於不同的Consumer Group即可。下圖是Kafka在Linkedin的一種簡化部署示意圖。

下面這個例子更清晰地展示了Kafka Consumer Group的特性。首先創建一個Topic (名爲topic1,包含3個Partition),然後創建一個屬於group1的Consumer實例,並創建三個屬於group2的Consumer實例,最後通過Producer向topic1發送key分別爲1,2,3的消息。結果發現屬於group1的Consumer收到了所有的這三條消息,同時group2中的3個Consumer分別收到了key爲1,2,3的消息。如下圖所示。

Push vs. Pull

作爲一個消息系統,Kafka遵循了傳統的方式,選擇由Producer向broker push消息並由Consumer從broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,採用push模式。事實上,push模式和pull模式各有優劣。

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

對於Kafka而言,pull模式更合適。pull模式可簡化broker的設計,Consumer可自主控制消費消息的速率,同時Consumer可以自己控制消費方式——即可批量消費也可逐條消費,同時還能選擇不同的提交方式從而實現不同的傳輸語義。

Kafka delivery guarantee

有這麼幾種可能的delivery guarantee:

  • At most once 消息可能會丟,但絕不會重複傳輸
  • At least one 消息絕不會丟,但可能會重複傳輸
  • Exactly once 每條消息肯定會被傳輸一次且僅傳輸一次,很多時候這是用戶所想要的。

    當Producer向broker發送消息時,一旦這條消息被commit,因數replication的存在,它就不會丟。但是如果Producer發送數據給broker後,遇到網絡問題而造成通信中斷,那Producer就無法判斷該條消息是否已經commit。雖然Kafka無法確定網絡故障期間發生了什麼,但是Producer可以生成一種類似於主鍵的東西,發生故障時冪等性的重試多次,這樣就做到了Exactly once。截止到目前(Kafka 0.8.2版本,2015-03-04),這一Feature還並未實現,有希望在Kafka未來的版本中實現。(所以目前默認情況下一條消息從Producer到broker是確保了At least once,可通過設置Producer異步發送實現At most once)。

    接下來討論的是消息從broker到Consumer的delivery guarantee語義。(僅針對Kafka consumer high level API)。Consumer在從broker讀取消息後,可以選擇commit,該操作會在Zookeeper中保存該Consumer在該Partition中讀取的消息的offset。該Consumer下一次再讀該Partition時會從下一條開始讀取。如未commit,下一次讀取的開始位置會跟上一次commit之後的開始位置相同。當然可以將Consumer設置爲autocommit,即Consumer一旦讀到數據立即自動commit。如果只討論這一讀取消息的過程,那Kafka是確保了Exactly once。但實際使用中應用程序並非在Consumer讀取完數據就結束了,而是要進行進一步處理,而數據處理與commit的順序在很大程度上決定了消息從broker和consumer的delivery guarantee semantic。

  • 讀完消息先commit再處理消息。這種模式下,如果Consumer在commit後還沒來得及處理消息就crash了,下次重新開始工作後就無法讀到剛剛已提交而未處理的消息,這就對應於At most once

  • 讀完消息先處理再commit。這種模式下,如果在處理完消息之後commit之前Consumer crash了,下次重新開始工作時還會處理剛剛未commit的消息,實際上該消息已經被處理過了。這就對應於At least once。在很多使用場景下,消息都有一個主鍵,所以消息的處理往往具有冪等性,即多次處理這一條消息跟只處理一次是等效的,那就可以認爲是Exactly once。(筆者認爲這種說法比較牽強,畢竟它不是Kafka本身提供的機制,主鍵本身也並不能完全保證操作的冪等性。而且實際上我們說delivery guarantee 語義是討論被處理多少次,而非處理結果怎樣,因爲處理方式多種多樣,我們不應該把處理過程的特性——如是否冪等性,當成Kafka本身的Feature)

  • 如果一定要做到Exactly once,就需要協調offset和實際操作的輸出。精典的做法是引入兩階段提交。如果能讓offset和操作輸入存在同一個地方,會更簡潔和通用。這種方式可能更好,因爲許多輸出系統可能不支持兩階段提交。比如,Consumer拿到數據後可能把數據放到HDFS,如果把最新的offset和數據本身一起寫到HDFS,那就可以保證數據的輸出和offset的更新要麼都完成,要麼都不完成,間接實現Exactly once。(目前就high level API而言,offset是存於Zookeeper中的,無法存於HDFS,而low level API的offset是由自己去維護的,可以將之存於HDFS中)

總之,Kafka默認保證At least once,並且允許通過設置Producer異步提交來實現At most once。而Exactly once要求與外部存儲系統協作,幸運的是Kafka提供的offset可以非常直接非常容易得使用這種方式。


Kafka High Availability (上)

Kafka在0.8以前的版本中,並不提供High Availablity機制,一旦一個或多個Broker宕機,則宕機期間其上所有Partition都無法繼續提供服務。若該Broker永遠不能再恢復,亦或磁盤故障,則其上數據將丟失。而Kafka的設計目標之一即是提供數據持久化,同時對於分佈式系統來說,尤其當集羣規模上升到一定程度後,一臺或者多臺機器宕機的可能性大大提高,對Failover要求非常高。因此,Kafka從0.8開始提供High Availability機制。本文從Data Replication和Leader Election兩方面介紹了Kafka的HA機制。

Kafka爲何需要High Available

爲何需要Replication

在Kafka在0.8以前的版本中,是沒有Replication的,一旦某一個Broker宕機,則其上所有的Partition數據都不可被消費,這與Kafka數據持久性及Delivery Guarantee的設計目標相悖。同時Producer都不能再將數據存於這些Partition中。

  • 如果Producer使用同步模式則Producer會在嘗試重新發送message.send.max.retries(默認值爲3)次後拋出Exception,用戶可以選擇停止發送後續數據也可選擇繼續選擇發送。而前者會造成數據的阻塞,後者會造成本應發往該Broker的數據的丟失。
  • 如果Producer使用異步模式,則Producer會嘗試重新發送message.send.max.retries(默認值爲3)次後記錄該異常並繼續發送後續數據,這會造成數據丟失並且用戶只能通過日誌發現該問題。同時,Kafka的Producer並未對異步模式提供callback接口。

由此可見,在沒有Replication的情況下,一旦某機器宕機或者某個Broker停止工作則會造成整個系統的可用性降低。隨着集羣規模的增加,整個集羣中出現該類異常的機率大大增加,因此對於生產系統而言Replication機制的引入非常重要。

注意:本文所述Leader Election主要指Replica之間的Leader Election。爲何需要Leader Election

引入Replication之後,同一個Partition可能會有多個Replica,而這時需要在這些Replication之間選出一個Leader,Producer和Consumer只與這個Leader交互,其它Replica作爲Follower從Leader中複製數據。

因爲需要保證同一個Partition的多個Replica之間的數據一致性(其中一個宕機後其它Replica必須要能繼續服務並且即不能造成數據重複也不能造成數據丟失)。如果沒有一個Leader,所有Replica都可同時讀/寫數據,那就需要保證多個Replica之間互相(N×N條通路)同步數據,數據的一致性和有序性非常難保證,大大增加了Replication實現的複雜性,同時也增加了出現異常的機率。而引入Leader後,只有Leader負責數據讀寫,Follower只向Leader順序Fetch數據(N條通路),系統更加簡單且高效。

Kafka HA設計解析

如何將所有Replica均勻分佈到整個集羣

爲了更好的做負載均衡,Kafka儘量將所有的Partition均勻分配到整個集羣上。一個典型的部署方式是一個Topic的Partition數量大於Broker的數量。同時爲了提高Kafka的容錯能力,也需要將同一個Partition的Replica儘量分散到不同的機器。實際上,如果所有的Replica都在同一個Broker上,那一旦該Broker宕機,該Partition的所有Replica都無法工作,也就達不到HA的效果。同時,如果某個Broker宕機了,需要保證它上面的負載可以被均勻的分配到其它倖存的所有Broker上。

Kafka分配Replica的算法如下:

  1. 將所有Broker(假設共n個Broker)和待分配的Partition排序
  2. 將第i個Partition分配到第(i mod n)個Broker上
  3. 將第i個Partition的第j個Replica分配到第((i + j) mode n)個Broker上

Data Replication

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

  • 怎樣Propagate消息
  • 在向Producer發送ACK前需要保證有多少個Replica已經收到該消息
  • 怎樣處理某個Replica不工作的情況
  • 怎樣處理Failed Replica恢復回來的情況

Propagate消息

Producer在發佈消息到某個Partition時,先通過ZooKeeper找到該Partition的Leader,然後無論該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並且向Producer發送ACK。

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

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

Kafka Replication的數據流如下圖所示:

ACK前需要保證有多少個備份

和大部分分佈式系統一樣,Kafka處理失敗需要明確定義一個Broker是否“活着”。對於Kafka而言,Kafka存活包含兩個條件,一是它必須維護與ZooKeeper的session(這個通過ZooKeeper的Heartbeat機制來實現)。二是Follower必須能夠及時將Leader的消息複製過來,不能“落後太多”。

Leader會跟蹤與其保持同步的Replica列表,該列表稱爲ISR(即in-sync Replica)。如果一個Follower宕機,或者落後太多,Leader將把它從ISR中移除。這裏所描述的“落後太多”指Follower複製的消息落後於Leader後的條數超過預定值(該值可在$KAFKA_HOME/config/server.properties中通過replica.lag.max.messages配置,其默認值是4000)或者Follower超過一定時間(該值可在$KAFKA_HOME/config/server.properties中通過replica.lag.time.max.ms來配置,其默認值是10000)未向Leader發送fetch請求。

Kafka的複製機制既不是完全的同步複製,也不是單純的異步複製。事實上,完全同步複製要求所有能工作的Follower都複製完,這條消息纔會被認爲commit,這種複製方式極大的影響了吞吐率(高吞吐率是Kafka非常重要的一個特性)。而異步複製方式下,Follower異步的從Leader複製數據,數據只要被Leader寫入log就被認爲已經commit,這種情況下如果Follower都複製完都落後於Leader,而如果Leader突然宕機,則會丟失數據。而Kafka的這種使用ISR的方式則很好的均衡了確保數據不丟失以及吞吐率。Follower可以批量的從Leader複製數據,這樣極大的提高複製性能(批量寫磁盤),極大減少了Follower與Leader的差距。

需要說明的是,Kafka只解決fail/recover,不處理“Byzantine”(“拜占庭”)問題。一條消息只有被ISR裏的所有Follower都從Leader複製過去纔會被認爲已提交。這樣就避免了部分數據被寫進了Leader,還沒來得及被任何Follower複製就宕機了,而造成數據丟失(Consumer無法消費這些數據)。而對於Producer而言,它可以選擇是否等待消息commit,這可以通過request.required.acks來設置。這種機制確保了只要ISR有一個或以上的Follower,一條被commit的消息就不會丟失。

Leader Election算法

上文說明了Kafka是如何做Replication的,另外一個很重要的問題是當Leader宕機了,怎樣在Follower中選舉出新的Leader。因爲Follower可能落後許多或者crash了,所以必須確保選擇“最新”的Follower作爲新的Leader。一個基本的原則就是,如果Leader不在了,新的Leader必須擁有原來的Leader commit過的所有消息。這就需要作一個折衷,如果Leader在標明一條消息被commit前等待更多的Follower確認,那在它宕機之後就有更多的Follower可以作爲新的Leader,但這也會造成吞吐率的下降。

一種非常常用的選舉leader的方式是“Majority Vote”(“少數服從多數”),但Kafka並未採用這種方式。這種模式下,如果我們有2f+1個Replica(包含Leader和Follower),那在commit之前必須保證有f+1個Replica複製完消息,爲了保證正確選出新的Leader,fail的Replica不能超過f個。因爲在剩下的任意f+1個Replica裏,至少有一個Replica包含有最新的所有消息。這種方式有個很大的優勢,系統的latency只取決於最快的幾個Broker,而非最慢那個。Majority Vote也有一些劣勢,爲了保證Leader Election的正常進行,它所能容忍的fail的follower個數比較少。如果要容忍1個follower掛掉,必須要有3個以上的Replica,如果要容忍2個Follower掛掉,必須要有5個以上的Replica。也就是說,在生產環境下爲了保證較高的容錯程度,必須要有大量的Replica,而大量的Replica又會在大數據量下導致性能的急劇下降。這就是這種算法更多用在ZooKeeper這種共享集羣配置的系統中而很少在需要存儲大量數據的系統中使用的原因。例如HDFS的HA Feature是基於majority-vote-based journal,但是它的數據存儲並沒有使用這種方式。

實際上,Leader Election算法非常多,比如ZooKeeper的ZabRaftViewstamped Replication。而Kafka所使用的Leader Election算法更像微軟的PacificA算法。

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

雖然Majority Vote與ISR相比有不需等待最慢的Broker這一優勢,但是Kafka作者認爲Kafka可以通過Producer選擇是否被commit阻塞來改善這一問題,並且節省下來的Replica和磁盤使得ISR模式仍然值得。

如何處理所有Replica都不工作

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

  • 等待ISR中的任一個Replica“活”過來,並且選它作爲Leader
  • 選擇第一個“活”過來的Replica(不一定是ISR中的)作爲Leader

這就需要在可用性和一致性當中作出一個簡單的折衷。如果一定要等待ISR中的Replica“活”過來,那不可用的時間就可能會相對較長。而且如果ISR中的所有Replica都無法“活”過來了,或者數據都丟失了,這個Partition將永遠不可用。選擇第一個“活”過來的Replica作爲Leader,而這個Replica不是ISR中的Replica,那即使它並不保證已經包含了所有已commit的消息,它也會成爲Leader而作爲consumer的數據源(前文有說明,所有讀寫都由Leader完成)。Kafka0.8.*使用了第二種方式。根據Kafka的文檔,在以後的版本中,Kafka支持用戶通過配置選擇這兩種方式中的一種,從而根據不同的使用場景選擇高可用性還是強一致性。

如何選舉Leader

最簡單最直觀的方案是,所有Follower都在ZooKeeper上設置一個Watch,一旦Leader宕機,其對應的ephemeral znode會自動刪除,此時所有Follower都嘗試創建該節點,而創建成功者(ZooKeeper保證只有一個能創建成功)即是新的Leader,其它Replica即爲Follower。

但是該方法會有3個問題:

  • split-brain 這是由ZooKeeper的特性引起的,雖然ZooKeeper能保證所有Watch按順序觸發,但並不能保證同一時刻所有Replica“看”到的狀態是一樣的,這就可能造成不同Replica的響應不一致
  • herd effect 如果宕機的那個Broker上的Partition比較多,會造成多個Watch被觸發,造成集羣內大量的調整
  • ZooKeeper負載過重 每個Replica都要爲此在ZooKeeper上註冊一個Watch,當集羣規模增加到幾千個Partition時ZooKeeper負載會過重。

Kafka 0.8.*的Leader Election方案解決了上述問題,它在所有broker中選出一個controller,所有Partition的Leader選舉都由controller決定。controller會將Leader的改變直接通過RPC的方式(比ZooKeeper Queue的方式更高效)通知需爲爲此作爲響應的Broker。同時controller也負責增刪Topic以及Replica的重新分配。

HA相關ZooKeeper結構

首先聲明本節所示ZooKeeper結構中,實線框代表路徑名是固定的,而虛線框代表路徑名與業務相關

admin (該目錄下znode只有在有相關操作時纔會存在,操作結束時會將其刪除)

/admin/preferred_replica_election數據結構

{
   "fields":[
      {
         "name":"version",
         "type":"int",
         "doc":"version id"
      },
      {
         "name":"partitions",
         "type":{
            "type":"array",
            "items":{
               "fields":[
                  {
                     "name":"topic",
                     "type":"string",
                     "doc":"topic of the partition for which preferred replica election should be triggered"
                  },
                  {
                     "name":"partition",
                     "type":"int",
                     "doc":"the partition for which preferred replica election should be triggered"
                  }
               ],
            }
            "doc":"an array of partitions for which preferred replica election should be triggered"
         }
      }
   ]
}

Example:

{
  "version": 1,
  "partitions":
     [
        {
            "topic": "topic1",
            "partition": 8         
        },
        {
            "topic": "topic2",
            "partition": 16        
        }
     ]            
}

/admin/reassign_partitions用於將一些Partition分配到不同的broker集合上。對於每個待重新分配的Partition,Kafka會在該znode上存儲其所有的Replica和相應的Broker id。該znode由管理進程創建並且一旦重新分配成功它將會被自動移除。其數據結構如下:

{ 
"fields":[ 
{ 
"name":"version", 
"type":"int", 
"doc":"version id" 
}, 
{ 
"name":"partitions", 
"type":{ 
"type":"array", 
"items":{ 
"fields":[ 
{ 
"name":"topic", 
"type":"string", 
"doc":"topic of the partition to be reassigned" 
}, 
{ 
"name":"partition", 
"type":"int", 
"doc":"the partition to be reassigned" 
}, 
{ 
"name":"replicas", 
"type":"array", 
"items":"int", 
"doc":"a list of replica ids" 
} 
], 
} 
"doc":"an array of partitions to be reassigned to new replicas" 
} 
} 
] 
}
Example:
{
  "version": 1,
  "partitions":
     [
        {
            "topic": "topic3",
            "partition": 1,
            "replicas": [1, 2, 3]
        }
     ]            
}

/admin/delete_topics數據結構:

Schema:
{ "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
      {"name": "topics",
       "type": { "type": "array", "items": "string", "doc": "an array of topics to be deleted"}
      } ]
}

Example:
{
  "version": 1,
  "topics": ["topic4", "topic5"]
}

brokers

broker(即/brokers/ids/[brokerId])存儲“活着”的broker信息。數據結構如下:

Schema:
{ "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
      {"name": "host", "type": "string", "doc": "ip address or host name of the broker"},
      {"name": "port", "type": "int", "doc": "port of the broker"},
      {"name": "jmx_port", "type": "int", "doc": "port for jmx"}
    ]
}

Example:
{
    "jmx_port":-1,
    "host":"node1",
    "version":1,
    "port":9092
}

topic註冊信息(/brokers/topics/[topic]),存儲該topic的所有partition的所有replica所在的broker id,第一個replica即爲preferred replica,對一個給定的partition,它在同一個broker上最多隻有一個replica,因此broker id可作爲replica id。數據結構如下:

Schema:
{ "fields" :
    [ {"name": "version", "type": "int", "doc": "version id"},
      {"name": "partitions",
       "type": {"type": "map",
                "values": {"type": "array", "items": "int", "doc": "a list of replica ids"},
                "doc": "a map from partition id to replica list"},
      }
    ]
}
Example:
{
    "version":1,
    "partitions":
        {"12":[6],
        "8":[2],
        "4":[6],
        "11":[5],
        "9":[3],
        "5":[7],
        "10":[4],
        "6":[8],
        "1":[3],
        "0":[2],
        "2":[4],
        "7":[1],
        "3":[5]}
}

partition state(/brokers/topics/[topic]/partitions/[partitionId]/state) 結構如下:

Schema:
{ "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
      {"name": "isr",
       "type": {"type": "array",
                "items": "int",
                "doc": "an array of the id of replicas in isr"}
      },
      {"name": "leader", "type": "int", "doc": "id of the leader replica"},
      {"name": "controller_epoch", "type": "int", "doc": "epoch of the controller that last updated the leader and isr info"},
      {"name": "leader_epoch", "type": "int", "doc": "epoch of the leader"}
    ]
}

Example:
{
    "controller_epoch":29,
    "leader":2,
    "version":1,
    "leader_epoch":48,
    "isr":[2]
}

controller 
/controller -> int (broker id of the controller)存儲當前controller的信息

Schema:
{ "fields":
    [ {"name": "version", "type": "int", "doc": "version id"},
      {"name": "brokerid", "type": "int", "doc": "broker id of the controller"}
    ]
}
Example:
{
    "version":1,
  "brokerid":8
}

/controller_epoch -> int (epoch)直接以整數形式存儲controller epoch,而非像其它znode一樣以JSON字符串形式存儲。

broker failover過程簡介

  1. Controller在ZooKeeper註冊Watch,一旦有Broker宕機(這是用宕機代表任何讓系統認爲其die的情景,包括但不限於機器斷電,網絡不可用,GC導致的Stop The World,進程crash等),其在ZooKeeper對應的znode會自動被刪除,ZooKeeper會fire Controller註冊的watch,Controller讀取最新的倖存的Broker。
  2. Controller決定set_p,該集合包含了宕機的所有Broker上的所有Partition。
  3. 對set_p中的每一個Partition

    3.1 從/brokers/topics/[topic]/partitions/[partition]/state讀取該Partition當前的ISR

    3.2 決定該Partition的新Leader。如果當前ISR中有至少一個Replica還倖存,則選擇其中一個作爲新Leader,新的ISR則包含當前ISR中所有幸存的Replica。否則選擇該Partition中任意一個倖存的Replica作爲新的Leader以及ISR(該場景下可能會有潛在的數據丟失)。如果該Partition的所有Replica都宕機了,則將新的Leader設置爲-1。

    3.3 將新的Leader,ISR和新的leader_epochcontroller_epoch寫入/brokers/topics/[topic]/partitions/[partition]/state。注意,該操作只有其version在3.1至3.3的過程中無變化時纔會執行,否則跳轉到3.1

  4. 直接通過RPC向set_p相關的Broker發送LeaderAndISRRequest命令。Controller可以在一個RPC操作中發送多個命令從而提高效率。

    broker failover順序圖如下所示。

Kafka設計解析(三):Kafka High Availability (下)

Kafka是由LinkedIn開發的一個分佈式的消息系統,使用Scala編寫,它以可水平擴展和高吞吐率而被廣泛使用。目前越來越多的開源分佈式處理系統如Cloudera、Apache Storm、Spark都支持與Kafka集成。InfoQ一直在緊密關注Kafka的應用以及發展,“Kafka剖析”專欄將會從架構設計、實現、應用場景、性能等方面深度解析Kafka。

本文在上篇文章基礎上,更加深入講解了Kafka的HA機制,主要闡述了HA相關各種場景,如Broker failover、Controller failover、Topic創建/刪除、Broker啓動、Follower從Leader fetch數據等詳細處理過程。同時介紹了Kafka提供的與Replication相關的工具,如重新分配Partition等。

Broker Failover過程

Controller對Broker failure的處理過程

  1. Controller在ZooKeeper的/brokers/ids節點上註冊Watch。一旦有Broker宕機(本文用宕機代表任何讓Kafka認爲其Broker die的情景,包括但不限於機器斷電,網絡不可用,GC導致的Stop The World,進程crash等),其在ZooKeeper對應的Znode會自動被刪除,ZooKeeper會fire Controller註冊的Watch,Controller即可獲取最新的倖存的Broker列表。
  2. Controller決定set_p,該集合包含了宕機的所有Broker上的所有Partition。
  3. 對set_p中的每一個Partition:

    3.1 從/brokers/topics/[topic]/partitions/[partition]/state讀取該Partition當前的ISR。

    3.2 決定該Partition的新Leader。如果當前ISR中有至少一個Replica還倖存,則選擇其中一個作爲新Leader,新的ISR則包含當前ISR中所有幸存的Replica。否則選擇該Partition中任意一個倖存的Replica作爲新的Leader以及ISR(該場景下可能會有潛在的數據丟失)。如果該Partition的所有Replica都宕機了,則將新的Leader設置爲-1。

    3.3 將新的Leader,ISR和新的leader_epochcontroller_epoch寫入/brokers/topics/[topic]/partitions/[partition]/state。注意,該操作只有Controller版本在3.1至3.3的過程中無變化時纔會執行,否則跳轉到3.1。

  4. 直接通過RPC向set_p相關的Broker發送LeaderAndISRRequest命令。Controller可以在一個RPC操作中發送多個命令從而提高效率。

    Broker failover順序圖如下所示。

LeaderAndIsrRequest結構如下

LeaderAndIsrResponse結構如下

創建/刪除Topic

  1. Controller在ZooKeeper的/brokers/topics節點上註冊Watch,一旦某個Topic被創建或刪除,則Controller會通過Watch得到新創建/刪除的Topic的Partition/Replica分配。
  2. 對於刪除Topic操作,Topic工具會將該Topic名字存於/admin/delete_topics。若delete.topic.enable爲true,則Controller註冊在/admin/delete_topics上的Watch被fire,Controller通過回調向對應的Broker發送StopReplicaRequest,若爲false則Controller不會在/admin/delete_topics上註冊Watch,也就不會對該事件作出反應。
  3. 對於創建Topic操作,Controller從/brokers/ids讀取當前所有可用的Broker列表,對於set_p中的每一個Partition:

    3.1 從分配給該Partition的所有Replica(稱爲AR)中任選一個可用的Broker作爲新的Leader,並將AR設置爲新的ISR(因爲該Topic是新創建的,所以AR中所有的Replica都沒有數據,可認爲它們都是同步的,也即都在ISR中,任意一個Replica都可作爲Leader)

    3.2 將新的Leader和ISR寫入/brokers/topics/[topic]/partitions/[partition]

  4. 直接通過RPC向相關的Broker發送LeaderAndISRRequest。

    創建Topic順序圖如下所示。

Broker響應請求流程

Broker通過kafka.network.SocketServer及相關模塊接受各種請求並作出響應。整個網絡通信模塊基於Java NIO開發,並採用Reactor模式,其中包含1個Acceptor負責接受客戶請求,N個Processor負責讀寫數據,M個Handler處理業務邏輯。

Acceptor的主要職責是監聽並接受客戶端(請求發起方,包括但不限於Producer,Consumer,Controller,Admin Tool)的連接請求,並建立和客戶端的數據傳輸通道,然後爲該客戶端指定一個Processor,至此它對該客戶端該次請求的任務就結束了,它可以去響應下一個客戶端的連接請求了。其核心代碼如下。

Processor主要負責從客戶端讀取數據並將響應返回給客戶端,它本身並不處理具體的業務邏輯,並且其內部維護了一個隊列來保存分配給它的所有SocketChannel。Processor的run方法會循環從隊列中取出新的SocketChannel並將其SelectionKey.OP_READ註冊到selector上,然後循環處理已就緒的讀(請求)和寫(響應)。Processor讀取完數據後,將其封裝成Request對象並將其交給RequestChannel。

RequestChannel是Processor和KafkaRequestHandler交換數據的地方,它包含一個隊列requestQueue用來存放Processor加入的Request,KafkaRequestHandler會從裏面取出Request來處理;同時它還包含一個respondQueue,用來存放KafkaRequestHandler處理完Request後返還給客戶端的Response。

Processor會通過processNewResponses方法依次將requestChannel中responseQueue保存的Response取出,並將對應的SelectionKey.OP_WRITE事件註冊到selector上。當selector的select方法返回時,對檢測到的可寫通道,調用write方法將Response返回給客戶端。

KafkaRequestHandler循環從RequestChannel中取Request並交給kafka.server.KafkaApis處理具體的業務邏輯。

LeaderAndIsrRequest響應過程

對於收到的LeaderAndIsrRequest,Broker主要通過ReplicaManager的becomeLeaderOrFollower處理,流程如下:

  1. 若請求中controllerEpoch小於當前最新的controllerEpoch,則直接返回ErrorMapping.StaleControllerEpochCode。
  2. 對於請求中partitionStateInfos中的每一個元素,即((topic, partitionId), partitionStateInfo):

    2.1 若partitionStateInfo中的leader epoch大於當前ReplicManager中存儲的(topic, partitionId)對應的partition的leader epoch,則:

    2.1.1 若當前brokerid(或者說replica id)在partitionStateInfo中,則將該partition及partitionStateInfo存入一個名爲partitionState的HashMap中

    2.1.2 否則說明該Broker不在該Partition分配的Replica list中,將該信息記錄於log中

    2.2 否則將相應的Error code(ErrorMapping.StaleLeaderEpochCode)存入Response中

  3. 篩選出partitionState中Leader與當前Broker ID相等的所有記錄存入partitionsTobeLeader中,其它記錄存入partitionsToBeFollower中。
  4. 若partitionsTobeLeader不爲空,則對其執行makeLeaders方。
  5. 若partitionsToBeFollower不爲空,則對其執行makeFollowers方法。
  6. 若highwatermak線程還未啓動,則將其啓動,並將hwThreadInitialized設爲true。
  7. 關閉所有Idle狀態的Fetcher。

LeaderAndIsrRequest處理過程如下圖所示

Broker啓動過程

Broker啓動後首先根據其ID在ZooKeeper的/brokers/idszonde下創建臨時子節點(Ephemeral node),創建成功後Controller的ReplicaStateMachine註冊其上的Broker Change Watch會被fire,從而通過回調KafkaController.onBrokerStartup方法完成以下步驟:

  1. 向所有新啓動的Broker發送UpdateMetadataRequest,其定義如下。

  2. 將新啓動的Broker上的所有Replica設置爲OnlineReplica狀態,同時這些Broker會爲這些Partition啓動high watermark線程。
  3. 通過partitionStateMachine觸發OnlinePartitionStateChange。

Controller Failover

Controller也需要Failover。每個Broker都會在Controller Path (/controller)上註冊一個Watch。當前Controller失敗時,對應的Controller Path會自動消失(因爲它是Ephemeral Node),此時該Watch被fire,所有“活”着的Broker都會去競選成爲新的Controller(創建新的Controller Path),但是隻會有一個競選成功(這點由ZooKeeper保證)。競選成功者即爲新的Leader,競選失敗者則重新在新的Controller Path上註冊Watch。因爲ZooKeeper的Watch是一次性的,被fire一次之後即失效,所以需要重新註冊。

Broker成功競選爲新Controller後會觸發KafkaController.onControllerFailover方法,並在該方法中完成如下操作:

  1. 讀取並增加Controller Epoch。
  2. 在ReassignedPartitions Patch(/admin/reassign_partitions)上註冊Watch。
  3. 在PreferredReplicaElection Path(/admin/preferred_replica_election)上註冊Watch。
  4. 通過partitionStateMachine在Broker Topics Patch(/brokers/topics)上註冊Watch。
  5. delete.topic.enable設置爲true(默認值是false),則partitionStateMachine在Delete Topic Patch(/admin/delete_topics)上註冊Watch。
  6. 通過replicaStateMachine在Broker Ids Patch(/brokers/ids)上註冊Watch。
  7. 初始化ControllerContext對象,設置當前所有Topic,“活”着的Broker列表,所有Partition的Leader及ISR等。
  8. 啓動replicaStateMachine和partitionStateMachine。
  9. 將brokerState狀態設置爲RunningAsController。
  10. 將每個Partition的Leadership信息發送給所有“活”着的Broker。
  11. auto.leader.rebalance.enable配置爲true(默認值是true),則啓動partition-rebalance線程。
  12. delete.topic.enable設置爲true且Delete Topic Patch(/admin/delete_topics)中有值,則刪除相應的Topic。

Partition重新分配

管理工具發出重新分配Partition請求後,會將相應信息寫到/admin/reassign_partitions上,而該操作會觸發ReassignedPartitionsIsrChangeListener,從而通過執行回調函數KafkaController.onPartitionReassignment來完成以下操作:

  1. 將ZooKeeper中的AR(Current Assigned Replicas)更新爲OAR(Original list of replicas for partition) + RAR(Reassigned replicas)。
  2. 強制更新ZooKeeper中的leader epoch,向AR中的每個Replica發送LeaderAndIsrRequest。
  3. 將RAR - OAR中的Replica設置爲NewReplica狀態。
  4. 等待直到RAR中所有的Replica都與其Leader同步。
  5. 將RAR中所有的Replica都設置爲OnlineReplica狀態。
  6. 將Cache中的AR設置爲RAR。
  7. 若Leader不在RAR中,則從RAR中重新選舉出一個新的Leader併發送LeaderAndIsrRequest。若新的Leader不是從RAR中選舉而出,則還要增加ZooKeeper中的leader epoch。
  8. 將OAR - RAR中的所有Replica設置爲OfflineReplica狀態,該過程包含兩部分。第一,將ZooKeeper上ISR中的OAR - RAR移除並向Leader發送LeaderAndIsrRequest從而通知這些Replica已經從ISR中移除;第二,向OAR - RAR中的Replica發送StopReplicaRequest從而停止不再分配給該Partition的Replica。
  9. 將OAR - RAR中的所有Replica設置爲NonExistentReplica狀態從而將其從磁盤上刪除。
  10. 將ZooKeeper中的AR設置爲RAR。
  11. 刪除/admin/reassign_partition

注意:最後一步纔將ZooKeeper中的AR更新,因爲這是唯一一個持久存儲AR的地方,如果Controller在這一步之前crash,新的Controller仍然能夠繼續完成該過程。

以下是Partition重新分配的案例,OAR = {1,2,3},RAR = {4,5,6},Partition重新分配過程中ZooKeeper中的AR和Leader/ISR路徑如下

AR leader/isr Sttep
{1,2,3} 1/{1,2,3} (initial state)
{1,2,3,4,5,6} 1/{1,2,3} (step 2)
{1,2,3,4,5,6} 1/{1,2,3,4,5,6} (step 4)
{1,2,3,4,5,6} 4/{1,2,3,4,5,6} (step 7)
{1,2,3,4,5,6} 4/{4,5,6} (step 8)
{4,5,6} 4/{4,5,6} (step 10)

Follower從Leader Fetch數據

Follower通過向Leader發送FetchRequest獲取消息,FetchRequest結構如下

從FetchRequest的結構可以看出,每個Fetch請求都要指定最大等待時間和最小獲取字節數,以及由TopicAndPartition和PartitionFetchInfo構成的Map。實際上,Follower從Leader數據和Consumer從Broker Fetch數據,都是通過FetchRequest請求完成,所以在FetchRequest結構中,其中一個字段是clientID,並且其默認值是ConsumerConfig.DefaultClientId。

Leader收到Fetch請求後,Kafka通過KafkaApis.handleFetchRequest響應該請求,響應過程如下:

  1. replicaManager根據請求讀出數據存入dataRead中。
  2. 如果該請求來自Follower則更新其相應的LEO(log end offset)以及相應Partition的High Watermark
  3. 根據dataRead算出可讀消息長度(單位爲字節)並存入bytesReadable中。
  4. 滿足下面4個條件中的1個,則立即將相應的數據返回
    • Fetch請求不希望等待,即fetchRequest.macWait <= 0
    • Fetch請求不要求一定能取到消息,即fetchRequest.numPartitions <= 0,也即requestInfo爲空
    • 有足夠的數據可供返回,即bytesReadable >= fetchRequest.minBytes
    • 讀取數據時發生異常
  5. 若不滿足以上4個條件,FetchRequest將不會立即返回,並將該請求封裝成DelayedFetch。檢查該DeplayedFetch是否滿足,若滿足則返回請求,否則將該請求加入Watch列表

Leader通過以FetchResponse的形式將消息返回給Follower,FetchResponse結構如下

Replication工具

Topic Tool

$KAFKA_HOME/bin/kafka-topics.sh,該工具可用於創建、刪除、修改、查看某個Topic,也可用於列出所有Topic。另外,該工具還可修改某個Topic的以下配置。

unclean.leader.election.enable
delete.retention.ms
segment.jitter.ms
retention.ms
flush.ms
segment.bytes
flush.messages
segment.ms
retention.bytes
cleanup.policy
segment.index.bytes
min.cleanable.dirty.ratio
max.message.bytes
file.delete.delay.ms
min.insync.replicas
index.interval.bytes

Replica Verification Tool

$KAFKA_HOME/bin/kafka-replica-verification.sh,該工具用來驗證所指定的一個或多個Topic下每個Partition對應的所有Replica是否都同步。可通過topic-white-list這一參數指定所需要驗證的所有Topic,支持正則表達式。

Preferred Replica Leader Election Tool

用途

有了Replication機制後,每個Partition可能有多個備份。某個Partition的Replica列表叫作AR(Assigned Replicas),AR中的第一個Replica即爲“Preferred Replica”。創建一個新的Topic或者給已有Topic增加Partition時,Kafka保證Preferred Replica被均勻分佈到集羣中的所有Broker上。理想情況下,Preferred Replica會被選爲Leader。以上兩點保證了所有Partition的Leader被均勻分佈到了集羣當中,這一點非常重要,因爲所有的讀寫操作都由Leader完成,若Leader分佈過於集中,會造成集羣負載不均衡。但是,隨着集羣的運行,該平衡可能會因爲Broker的宕機而被打破,該工具就是用來幫助恢復Leader分配的平衡。

事實上,每個Topic從失敗中恢復過來後,它默認會被設置爲Follower角色,除非某個Partition的Replica全部宕機,而當前Broker是該Partition的AR中第一個恢復回來的Replica。因此,某個Partition的Leader(Preferred Replica)宕機並恢復後,它很可能不再是該Partition的Leader,但仍然是Preferred Replica。

原理

1. 在ZooKeeper上創建/admin/preferred_replica_election節點,並存入需要調整Preferred Replica的Partition信息。

2. Controller一直Watch該節點,一旦該節點被創建,Controller會收到通知,並獲取該內容。

3. Controller讀取Preferred Replica,如果發現該Replica當前並非是Leader並且它在該Partition的ISR中,Controller向該Replica發送LeaderAndIsrRequest,使該Replica成爲Leader。如果該Replica當前並非是Leader,且不在ISR中,Controller爲了保證沒有數據丟失,並不會將其設置爲Leader。

用法

$KAFKA_HOME/bin/kafka-preferred-replica-election.sh --zookeeper localhost:2181

在包含8個Broker的Kafka集羣上,創建1個名爲topic1,replication-factor爲3,Partition數爲8的Topic,使用如下命令查看其Partition/Replica分佈。

$KAFKA_HOME/bin/kafka-topics.sh --describe --topic topic1 --zookeeper localhost:2181

查詢結果如下圖所示,從圖中可以看到,Kafka將所有Replica均勻分佈到了整個集羣,並且Leader也均勻分佈。

手動停止部分Broker,topic1的Partition/Replica分佈如下圖所示。從圖中可以看到,由於Broker 1/2/4都被停止,Partition 0的Leader由原來的1變爲3,Partition 1的Leader由原來的2變爲5,Partition 2的Leader由原來的3變爲6,Partition 3的Leader由原來的4變爲7。

再重新啓動ID爲1的Broker,topic1的Partition/Replica分佈如下。可以看到,雖然Broker 1已經啓動(Partition 0和Partition5的ISR中有1),但是1並不是任何一個Parititon的Leader,而Broker 5/6/7都是2個Partition的Leader,即Leader的分佈不均衡——一個Broker最多是2個Partition的Leader,而最少是0個Partition的Leader。

運行該工具後,topic1的Partition/Replica分佈如下圖所示。由圖可見,除了Partition 1和Partition 3由於Broker 2和Broker 4還未啓動,所以其Leader不是其Preferred Repliac外,其它所有Partition的Leader都是其Preferred Replica。同時,與運行該工具前相比,Leader的分配更均勻——一個Broker最多是2個Parittion的Leader,最少是1個Partition的Leader。

啓動Broker 2和Broker 4,Leader分佈與上一步相比並未變化,如下圖所示。

再次運行該工具,所有Partition的Leader都由其Preferred Replica承擔,Leader分佈更均勻——每個Broker承擔1個Partition的Leader角色。

除了手動運行該工具使Leader分配均勻外,Kafka還提供了自動平衡Leader分配的功能,該功能可通過將auto.leader.rebalance.enable設置爲true開啓,它將週期性檢查Leader分配是否平衡,若不平衡度超過一定閾值則自動由Controller嘗試將各Partition的Leader設置爲其Preferred Replica。檢查週期由leader.imbalance.check.interval.seconds指定,不平衡度閾值由leader.imbalance.per.broker.percentage指定。

Kafka Reassign Partitions Tool

用途

該工具的設計目標與Preferred Replica Leader Election Tool有些類似,都旨在促進Kafka集羣的負載均衡。不同的是,Preferred Replica Leader Election只能在Partition的AR範圍內調整其Leader,使Leader分佈均勻,而該工具還可以調整Partition的AR。

Follower需要從Leader Fetch數據以保持與Leader同步,所以僅僅保持Leader分佈的平衡對整個集羣的負載均衡來說是不夠的。另外,生產環境下,隨着負載的增大,可能需要給Kafka集羣擴容。向Kafka集羣中增加Broker非常簡單方便,但是對於已有的Topic,並不會自動將其Partition遷移到新加入的Broker上,此時可用該工具達到此目的。某些場景下,實際負載可能遠小於最初預期負載,此時可用該工具將分佈在整個集羣上的Partition重裝分配到某些機器上,然後可以停止不需要的Broker從而實現節約資源的目的。

需要說明的是,該工具不僅可以調整Partition的AR位置,還可調整其AR數量,即改變該Topic的replication factor。

原理

該工具只負責將所需信息存入ZooKeeper中相應節點,然後退出,不負責相關的具體操作,所有調整都由Controller完成。

1. 在ZooKeeper上創建/admin/reassign_partitions節點,並存入目標Partition列表及其對應的目標AR列表。

2. Controller註冊在/admin/reassign_partitions上的Watch被fire,Controller獲取該列表。

3. 對列表中的所有Partition,Controller會做如下操作:

  • 啓動RAR - AR中的Replica,即新分配的Replica。(RAR = Reassigned Replicas, AR = Assigned Replicas)
  • 等待新的Replica與Leader同步
  • 如果Leader不在RAR中,從RAR中選出新的Leader
  • 停止並刪除AR - RAR中的Replica,即不再需要的Replica
  • 刪除/admin/reassign_partitions節點

用法

該工具有三種使用模式

  • generate模式,給定需要重新分配的Topic,自動生成reassign plan(並不執行)
  • execute模式,根據指定的reassign plan重新分配Partition
  • verify模式,驗證重新分配Partition是否成功

下面這個例子將使用該工具將Topic的所有Partition重新分配到Broker 4/5/6/7上,步驟如下:

1. 使用generate模式,生成reassign plan

指定需要重新分配的Topic ({"topics":[{"topic":"topic1"}],"version":1}),並存入/tmp/topics-to-move.json文件中,然後執行如下命令

$KAFKA_HOME/bin/kafka-reassign-partitions.sh --zookeeper localhost:2181
--topics-to-move-json-file /tmp/topics-to-move.json 
--broker-list "4,5,6,7" --generate

結果如下圖所示

2. 使用execute模式,執行reassign plan

將上一步生成的reassignment plan存入/tmp/reassign-plan.json文件中,並執行

$KAFKA_HOME/bin/kafka-reassign-partitions.sh --zookeeper localhost:2181 
--reassignment-json-file /tmp/reassign-plan.json --execute

此時,ZooKeeper上/admin/reassign_partitions節點被創建,且其值與/tmp/reassign-plan.json文件的內容一致。

3. 使用verify模式,驗證reassign是否完成

執行verify命令

$KAFKA_HOME/bin/kafka-reassign-partitions.sh --zookeeper localhost:2181 
--reassignment-json-file /tmp/reassign-plan.json --verify

結果如下所示,從圖中可看出topic1的所有Partititon都根據reassign plan重新分配成功。

接下來用Topic Tool再次驗證。

bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic topic1

結果如下圖所示,從圖中可看出topic1的所有Partition都被重新分配到Broker 4/5/6/7,且每個Partition的AR與reassign plan一致。

需要說明的是,在使用execute之前,並不一定要使用generate模式自動生成reassign plan,使用generate模式只是爲了方便。事實上,某些場景下,generate模式生成的reassign plan並不一定能滿足需求,此時用戶可以自己設置reassign plan。

State Change Log Merge Tool

用途

該工具旨在從整個集羣的Broker上收集狀態改變日誌,並生成一個集中的格式化的日誌以幫助診斷狀態改變相關的故障。每個Broker都會將其收到的狀態改變相關的的指令存於名爲state-change.log的日誌文件中。某些情況下,Partition的Leader election可能會出現問題,此時我們需要對整個集羣的狀態改變有個全局的瞭解從而診斷故障並解決問題。該工具將集羣中相關的state-change.log日誌按時間順序合併,同時支持用戶輸入時間範圍和目標Topic及Partition作爲過濾條件,最終將格式化的結果輸出。

用法

bin/kafka-run-class.sh kafka.tools.StateChangeLogMerger 
--logs /opt/kafka_2.11-0.8.2.1/logs/state-change.log 
--topic topic1 --partitions 0,1,2,3,4,5,6,7

Kafka設計解析(四):Kafka Consumer解析

High Level Consumer

很多時候,客戶程序只是希望從Kafka讀取數據,不太關心消息offset的處理。同時也希望提供一些語義,例如同一條消息只被某一個Consumer消費(單播)或被所有Consumer消費(廣播)。因此,Kafka High Level Consumer提供了一個從Kafka消費數據的高層抽象,從而屏蔽掉其中的細節並提供豐富的語義。

Consumer Group

High Level Consumer將從某個Partition讀取的最後一條消息的offset存於ZooKeeper中(Kafka從0.8.2版本開始同時支持將offset存於Zookeeper中與將offset存於專用的Kafka Topic中)。這個offset基於客戶程序提供給Kafka的名字來保存,這個名字被稱爲Consumer Group。Consumer Group是整個Kafka集羣全局的,而非某個Topic的。每一個High Level Consumer實例都屬於一個Consumer Group,若不指定則屬於默認的Group。ZooKeeper中Consumer相關節點如下圖所示:

很多傳統的Message Queue都會在消息被消費完後將消息刪除,一方面避免重複消費,另一方面可以保證Queue的長度比較短,提高效率。而如上文所述,Kafka並不刪除已消費的消息,爲了實現傳統Message Queue消息只被消費一次的語義,Kafka保證每條消息在同一個Consumer Group裏只會被某一個Consumer消費。與傳統Message Queue不同的是,Kafka還允許不同Consumer Group同時消費同一條消息,這一特性可以爲消息的多元化處理提供支持。

實際上,Kafka的設計理念之一就是同時提供離線處理和實時處理。根據這一特性,可以使用Storm這種實時流處理系統對消息進行實時在線處理,同時使用 Hadoop這種批處理系統進行離線處理,還可以同時將數據實時備份到另一個數據中心,只需要保證這三個操作所使用的Consumer在不同的 Consumer Group即可。下圖展示了Kafka在LinkedIn的一種簡化部署模型。

爲了更清晰展示Kafka Consumer Group的特性,筆者進行了一項測試。創建一個Topic (名爲topic1),再創建一個屬於group1的Consumer實例,並創建三個屬於group2的Consumer實例,然後通過 Producer向topic1發送Key分別爲1,2,3的消息。結果發現屬於group1的Consumer收到了所有的這三條消息,同時 group2中的3個Consumer分別收到了Key爲1,2,3的消息,如下圖所示。

(點擊放大圖像)

注:上圖中每個黑色區域代表一個Consumer實例,每個實例只創建一個MessageStream。實際上,本實驗將Consumer應用程序打成jar包,並在4個不同的命令行終端中傳入不同的參數運行。

High Level Consumer Rebalance

注:本節所講述Rebalance相關內容均基於Kafka High Level Consumer。

Kafka保證同一Consumer Group中只有一個Consumer會消費某條消息,實際上,Kafka保證的是穩定狀態下每一個Consumer實例只會消費某一個或多個特定 Partition的數據,而某個Partition的數據只會被某一個特定的Consumer實例所消費。也就是說Kafka對消息的分配是以 Partition爲單位分配的,而非以每一條消息作爲分配單元。這樣設計的劣勢是無法保證同一個Consumer Group裏的Consumer均勻消費數據,優勢是每個Consumer不用都跟大量的Broker通信,減少通信開銷,同時也降低了分配難度,實現也更簡單。另外,因爲同一個Partition裏的數據是有序的,這種設計可以保證每個Partition裏的數據可以被有序消費。

如果某Consumer Group中Consumer(每個Consumer只創建1個MessageStream)數量少於Partition數量,則至少有一個 Consumer會消費多個Partition的數據,如果Consumer的數量與Partition數量相同,則正好一個Consumer消費一個 Partition的數據。而如果Consumer的數量多於Partition的數量時,會有部分Consumer無法消費該Topic下任何一條消息。

如下例所示,如果topic1有0,1,2共三個Partition,當group1只有一個Consumer(名爲consumer1)時,該 Consumer可消費這3個Partition的所有數據。

增加一個Consumer(consumer2)後,其中一個Consumer(consumer1)可消費2個Partition的數據(Partition 0和Partition 1),另外一個Consumer(consumer2)可消費另外一個Partition(Partition 2)的數據。

再增加一個Consumer(consumer3)後,每個Consumer可消費一個Partition的數據。consumer1消費partition0,consumer2消費partition1,consumer3消費partition2。

再增加一個Consumer(consumer4)後,其中3個Consumer可分別消費一個Partition的數據,另外一個Consumer(consumer4)不能消費topic1的任何數據。

此時關閉consumer1,其餘3個Consumer可分別消費一個Partition的數據。

接着關閉consumer2,consumer3可消費2個Partition,consumer4可消費1個Partition。

再關閉consumer3,僅存的consumer4可同時消費topic1的3個Partition。

Consumer Rebalance的算法如下:

  • 將目標Topic下的所有Partirtion排序,存於PT
  • 對某Consumer Group下所有Consumer排序,存於CG,第i個Consumer記爲Ci
  • N=size(PT)/size(CG),向上取整
  • 解除Ci對原來分配的Partition的消費權(i從0開始)
  • 將第i∗N到(i+1)∗N−1個Partition分配給Ci

目前,最新版(0.8.2.1)Kafka的Consumer Rebalance的控制策略是由每一個Consumer通過在Zookeeper上註冊Watch完成的。每個Consumer被創建時會觸發 Consumer Group的Rebalance,具體啓動流程如下:

  • High Level Consumer啓動時將其ID註冊到其Consumer Group下,在Zookeeper上的路徑爲/consumers/[consumer group]/ids/[consumer id]
  • /consumers/[consumer group]/ids上註冊Watch
  • /brokers/ids上註冊Watch
  • 如果Consumer通過Topic Filter創建消息流,則它會同時在/brokers/topics上也創建Watch
  • 強制自己在其Consumer Group內啓動Rebalance流程

在這種策略下,每一個Consumer或者Broker的增加或者減少都會觸發 Consumer Rebalance。因爲每個Consumer只負責調整自己所消費的Partition,爲了保證整個Consumer Group的一致性,當一個Consumer觸發了Rebalance時,該Consumer Group內的其它所有其它Consumer也應該同時觸發Rebalance。

該方式有如下缺陷:

根據Kafka社區wiki,Kafka作者正在考慮在還未發佈的0.9.x版本中使用中心協調器(Coordinator)。大體思想是爲所有Consumer Group的子集選舉出一個Broker作爲Coordinator,由它Watch Zookeeper,從而判斷是否有Partition或者Consumer的增減,然後生成Rebalance命令,並檢查是否這些Rebalance 在所有相關的Consumer中被執行成功,如果不成功則重試,若成功則認爲此次Rebalance成功(這個過程跟Replication Controller非常類似)。具體方案將在後文中詳細闡述。

Low Level Consumer

使用Low Level Consumer (Simple Consumer)的主要原因是,用戶希望比Consumer Group更好的控制數據的消費。比如:

  • 同一條消息讀多次
  • 只讀取某個Topic的部分Partition
  • 管理事務,從而確保每條消息被處理一次,且僅被處理一次

與Consumer Group相比,Low Level Consumer要求用戶做大量的額外工作。

  • 必須在應用程序中跟蹤offset,從而確定下一條應該消費哪條消息
  • 應用程序需要通過程序獲知每個Partition的Leader是誰
  • 必須處理Leader的變化

使用Low Level Consumer的一般流程如下

  • 查找到一個“活着”的Broker,並且找出每個Partition的Leader
  • 找出每個Partition的Follower
  • 定義好請求,該請求應該能描述應用程序需要哪些數據
  • Fetch數據
  • 識別Leader的變化,並對之作出必要的響應

Consumer重新設計

根據社區社區wiki,Kafka在0.9.*版本中,重新設計Consumer可能是最重要的Feature之一。本節會根據社區wiki介紹Kafka 0.9.*中對Consumer可能的設計方向及思路。

設計方向

簡化消費者客戶端

部分用戶希望開發和使用non-java的客戶端。現階段使用non-java發SimpleConsumer比較方便,但想開發High Level Consumer並不容易。因爲High Level Consumer需要實現一些複雜但必不可少的失敗探測和Rebalance。如果能將消費者客戶端更精簡,使依賴最小化,將會極大的方便non- java用戶實現自己的Consumer。

中心Coordinator

如上文所述,當前版本的High Level Consumer存在Herd Effect和Split Brain的問題。如果將失敗探測和Rebalance的邏輯放到一個高可用的中心Coordinator,那麼這兩個問題即可解決。同時還可大大減少 Zookeeper的負載,有利於Kafka Broker的Scale Out。

允許手工管理offset

一些系統希望以特定的時間間隔在自定義的數據庫中管理Offset。這就要求Consumer能獲取到每條消息的metadata,例如 Topic,Partition,Offset,同時還需要在Consumer啓動時得到每個Partition的Offset。實現這些,需要提供新的 Consumer API。同時有個問題不得不考慮,即是否允許Consumer手工管理部分Topic的Offset,而讓Kafka自動通過Zookeeper管理其它 Topic的Offset。一個可能的選項是讓每個Consumer只能選取1種Offset管理機制,這可極大的簡化Consumer API的設計和實現。

Rebalance後觸發用戶指定的回調

一些應用可能會在內存中爲每個Partition維護一些狀態,Rebalance時,它們可能需要將該狀態持久化。因此該需求希望支持用戶實現並指定一些可插拔的並在Rebalance時觸發的回調。如果用戶使用手動的Offset管理,那該需求可方便得由用戶實現,而如果用戶希望使用Kafka提供的自動Offset管理,則需要Kafka提供該回調機制。

非阻塞式Consumer API

該需求源於那些實現高層流處理操作,如filter by, group by, join等,的系統。現階段的阻塞式Consumer幾乎不可能實現Join操作。

如何通過中心Coordinator實現Rebalance

成功Rebalance的結果是,被訂閱的所有Topic的每一個Partition將會被Consumer Group內的一個(有且僅有一個)Consumer擁有。每一個Broker將被選舉爲某些Consumer Group的Coordinator。某個Cosnumer Group的Coordinator負責在該Consumer Group的成員變化或者所訂閱的Topic的Partititon變化時協調Rebalance操作。

Consumer

1) Consumer啓動時,先向Broker列表中的任意一個Broker發送ConsumerMetadataRequest,並通過 ConsumerMetadataResponse獲取它所在Group的Coordinator信息。ConsumerMetadataRequest 和ConsumerMetadataResponse的結構如下

ConsumerMetadataRequest
{
  GroupId                => String
}

ConsumerMetadataResponse
{
  ErrorCode              => int16
  Coordinator            => Broker
}

2)Consumer連接到Coordinator併發送 HeartbeatRequest,如果返回的HeartbeatResponse沒有任何錯誤碼,Consumer繼續fetch數據。若其中包含 IllegalGeneration錯誤碼,即說明Coordinator已經發起了Rebalance操作,此時Consumer停止fetch數據,commit offset,併發送JoinGroupRequest給它的Coordinator,並在JoinGroupResponse中獲得它應該擁有的所有 Partition列表和它所屬的Group的新的Generation ID。此時Rebalance完成,Consumer開始fetch數據。相應Request和Response結構如下

HeartbeatRequest
{
  GroupId                => String
  GroupGenerationId      => int32
  ConsumerId             => String
}

HeartbeatResponse
{
  ErrorCode              => int16
}

JoinGroupRequest
{
  GroupId                     => String
  SessionTimeout              => int32
  Topics                      => [String]
  ConsumerId                  => String
  PartitionAssignmentStrategy => String
}

JoinGroupResponse
{
  ErrorCode              => int16
  GroupGenerationId      => int32
  ConsumerId             => String
  PartitionsToOwn        => [TopicName [Partition]]
}
TopicName => String
Partition => int32

Consumer狀態機

Down:Consumer停止工作

Start up & discover coordinator:Consumer檢測其所在Group的Coordinator。一旦它檢測到Coordinator,即向其發送JoinGroupRequest。

Part of a group:該狀態下,Consumer已經是該Group的成員,並週期性發送HeartbeatRequest。如 HeartbeatResponse包含IllegalGeneration錯誤碼,則轉換到Stopped Consumption狀態。若連接丟失,HeartbeatResponse包含NotCoordinatorForGroup錯誤碼,則轉換到 Rediscover coordinator狀態。

Rediscover coordinator:該狀態下,Consumer不停止消費而是嘗試通過發送ConsumerMetadataRequest來探測新的Coordinator,並且等待直到獲得無錯誤碼的響應。

Stopped consumption:該狀態下,Consumer停止消費並提交offset,直到它再次加入Group。

故障檢測機制

Consumer成功加入Group後,Consumer和相應的Coordinator同時開始故障探測程序。Consumer向 Coordinator發起週期性的Heartbeat(HeartbeatRequest)並等待響應,該週期爲 session.timeout.ms/heartbeat.frequency。若Consumer在session.timeout.ms內未收到 HeartbeatResponse,或者發現相應的Socket channel斷開,它即認爲Coordinator已宕機並啓動Coordinator探測程序。若Coordinator在 session.timeout.ms內沒有收到一次HeartbeatRequest,則它將該Consumer標記爲宕機狀態併爲其所在Group觸發一次Rebalance操作。

Coordinator Failover過程中,Consumer可能會在新的Coordinator完成Failover過程之前或之後發現新的Coordinator並向其發送HeatbeatRequest。對於後者,新的Cooodinator可能拒絕該請求,致使該Consumer重新探測Coordinator併發起新的連接請求。如果該Consumer向新的Coordinator發送連接請求太晚,新的Coordinator可能已經在此之前將其標記爲宕機狀態而將之視爲新加入的Consumer並觸發一次Rebalance操作。

Coordinator

1)穩定狀態下,Coordinator通過上述故障探測機制跟蹤其所管理的每個Group下的每個Consumer的健康狀態。

2)剛啓動時或選舉完成後,Coordinator從Zookeeper讀取它所管理的Group列表及這些Group的成員列表。如果沒有獲取到Group成員信息,它不會做任何事情直到某個Group中有成員註冊進來。

3)在Coordinator完成加載其管理的Group列表及其相應的成員信息之前,它將爲 HeartbeatRequest,OffsetCommitRequest和JoinGroupRequests返回 CoordinatorStartupNotComplete錯誤碼。此時,Consumer會重新發送請求。

4)Coordinator會跟蹤被其所管理的任何Consumer Group註冊的Topic的Partition的變化,併爲該變化觸發Rebalance操作。創建新的Topic也可能觸發Rebalance,因爲 Consumer可以在Topic被創建之前就已經訂閱它了。

Coordinator發起Rebalance操作流程如下所示。

Coordinator狀態機

Down:Coordinator不再擔任之前負責的Consumer Group的Coordinator

Catch up:該狀態下,Coordinator競選成功,但還未能做好服務相應請求的準備。

Ready:該狀態下,新競選出來的Coordinator已經完成從Zookeeper中加載它所負責管理的所有Group的metadata,並可開始接收相應的請求。

Prepare for rebalance:該狀態下,Coordinator在所有HeartbeatResponse中返回IllegalGeneration錯誤碼,並等待所有Consumer向其發送JoinGroupRequest後轉到Rebalancing狀態。

Rebalancing:該狀態下,Coordinator已經收到了JoinGroupRequest請求,並增加其Group Generation ID,分配Consumer ID,分配Partition。Rebalance成功後,它會等待接收包含新的Consumer Generation ID的HeartbeatRequest,並轉至Ready狀態。

Coordinator Failover

如前文所述,Rebalance操作需要經歷如下幾個階段

1)Topic/Partition的改變或者新Consumer的加入或者已有Consumer停止,觸發Coordinator註冊在Zookeeper上的watch,Coordinator收到通知準備發起Rebalance操作。

2)Coordinator通過在HeartbeatResponse中返回IllegalGeneration錯誤碼發起Rebalance操作。

3)Consumer發送JoinGroupRequest

4)Coordinator在Zookeeper中增加Group的Generation ID並將新的Partition分配情況寫入Zookeeper

5)Coordinator發送JoinGroupResponse

在這個過程中的每個階段,Coordinator都可能出現故障。下面給出Rebalance不同階段中Coordinator的Failover處理方式。

1)如果Coordinator的故障發生在第一階段,即它收到Notification並未來得及作出響應,則新的Coordinator將從 Zookeeper讀取Group的metadata,包含這些Group訂閱的Topic列表和之前的Partition分配。如果某個Group所訂閱的Topic數或者某個Topic的Partition數與之前的Partition分配不一致,亦或者某個Group連接到新的 Coordinator的Consumer數與之前Partition分配中的不一致,新的Coordinator會發起Rebalance操作。

2)如果失敗發生在階段2,它可能對部分而非全部Consumer發出帶錯誤碼的HeartbeatResponse。與第上面第一種情況一樣,新的 Coordinator會檢測到Rebalance的必要性併發起一次Rebalance操作。如果Rebalance是由Consumer的失敗所觸發並且Cosnumer在Coordinator的Failover完成前恢復,新的Coordinator不會爲此發起新的Rebalance操作。

3)如果Failure發生在階段3,新的Coordinator可能只收到部分而非全部Consumer的JoinGroupRequest。 Failover完成後,它可能收到部分Consumer的HeartRequest及另外部分Consumer的JoinGroupRequest。與第1種情況類似,它將發起新一輪的Rebalance操作。

4)如果Failure發生在階段4,即它將新的Group Generation ID和Group成員信息寫入Zookeeper後。新的Generation ID和Group成員信息以一個原子操作一次性寫入Zookeeper。Failover完成後,Consumer會發送 HeartbeatRequest給新的Coordinator,幷包含舊的Generation ID。此時新的Coordinator通過在HeartbeatResponse中返回IllegalGeneration錯誤碼發起新的一輪 Rebalance。這也解釋了爲什麼每次HeartbeatRequest中都需要包含Generation ID和Consumer ID。

5)如果Failure發生在階段5,舊的Coordinator可能只向Group中的部分Consumer發送了 JoinGroupResponse。收到JoinGroupResponse的Consumer在下次向已經失效的Coordinator發送 HeartbeatRequest或者提交Offset時會檢測到它已經失敗。此時,它將檢測新的Coordinator並向其發送帶有新的 Generation ID 的HeartbeatRequest。而未收到JoinGroupResponse的Consumer將檢測新的Coordinator並向其發送 JoinGroupRequest,這將促使新的Coordinator發起新一輪的Rebalance。

Kafka設計解析(五):Kafka Benchmark

性能測試及集羣監控工具

Kafka提供了非常多有用的工具,如Kafka設計解析(三)- Kafka High Availability (下)中提到的運維類工具——Partition Reassign Tool,Preferred Replica Leader Election Tool,Replica Verification Tool,State Change Log Merge Tool。本章將介紹Kafka提供的性能測試工具,Metrics報告工具及Yahoo開源的Kafka Manager。

Kafka性能測試腳本

  • $KAFKA_HOME/bin/kafka-producer-perf-test.sh 該腳本被設計用於測試Kafka Producer的性能,主要輸出4項指標,總共發送消息量(以MB爲單位),每秒發送消息量(MB/second),發送消息總數,每秒發送消息數(records/second)。除了將測試結果輸出到標準輸出外,該腳本還提供CSV Reporter,即將結果以CSV文件的形式存儲,便於在其它分析工具中使用該測試結果
  • $KAFKA_HOME/bin/kafka-consumer-perf-test.sh 該腳本用於測試Kafka Consumer的性能,測試指標與Producer性能測試腳本一樣。

Kafka Metrics

Kafka使用Yammer Metrics來報告服務端和客戶端的Metric信息。Yammer Metrics 3.1.0提供6種形式的Metrics收集——Meters,Gauges,Counters,Histograms,Timers,Health Checks。與此同時,Yammer Metrics將Metric的收集與報告(或者說發佈)分離,可以根據需要自由組合。目前它支持的Reporter有Console Reporter,JMX Reporter,HTTP Reporter,CSV Reporter,SLF4J Reporter,Ganglia Reporter,Graphite Reporter。因此,Kafka也支持通過以上幾種Reporter輸出其Metrics信息。

使用JConsole查看單服務器Metrics

使用JConsole通過JMX,是在不安裝其它工具(既然已經安裝了Kafka,就肯定安裝了Java,而JConsole是Java自帶的工具)的情況下查看Kafka服務器Metrics的最簡單最方便的方法之一。

首先必須通過爲環境變量JMX_PORT設置有效值來啓用Kafka的JMX Reporter。如export JMX_PORT=19797。然後即可使用JConsole通過上面設置的端口來訪問某一臺Kafka服務器來查看其Metrics信息,如下圖所示。

使用JConsole的一個好處是不用安裝額外的工具,缺點很明顯,數據展示不夠直觀,數據組織形式不友好,更重要的是不能同時監控整個集羣的Metrics。在上圖中,在kafka.cluster->Partition->UnderReplicated->topic4下,只有2和5兩個節點,這並非因爲topic4只有這兩個Partition的數據是處於複製狀態的。事實上,topic4在該Broker上只有這2個Partition,其它Partition在其它Broker上,所以通過該服務器的JMX Reporter只看到了這兩個Partition。

通過Kafka Manager查看整個集羣的Metrics

Kafka Manager是Yahoo開源的Kafka管理工具。它支持如下功能:

  • 管理多個集羣
  • 方便查看集羣狀態
  • 執行preferred replica election
  • 批量爲多個Topic生成並執行Partition分配方案
  • 創建Topic
  • 刪除Topic(只支持0.8.2及以上版本,同時要求在Broker中將delete.topic.enable設置爲true)
  • 爲已有Topic添加Partition
  • 更新Topic配置
  • 在Broker JMX Reporter開啓的前提下,輪詢Broker級別和Topic級別的Metrics
  • 監控Consumer Group及其消費狀態
  • 支持添加和查看LogKafka

安裝好Kafka Manager後,添加Cluster非常方便,只需指明該Cluster所使用的Zookeeper列表並指明Kafka版本即可,如下圖所示。


Kafka Benchmark

這裏要注意,此處添加Cluster是指添加一個已有的Kafka集羣進入監控列表,而非通過Kafka Manager部署一個新的Kafka Cluster,這一點與Cloudera Manager不同。

Kafka的一個核心特性是高吞吐率,因此本文的測試重點是Kafka的吞吐率。

本文的測試共使用6臺安裝Red Hat 6.6的虛擬機,3臺作爲Broker,另外3臺作爲Producer或者Consumer。每臺虛擬機配置如下:

  • CPU:8 vCPU, Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz,2 Sockets,4 Cores per socket,1 Thread per core
  • 內存:16 GB
  • 磁盤:500 GB

開啓Kafka JMX Reporter並使用19797端口,利用Kafka-Manager的JMX polling功能監控性能測試過程中的吞吐率。

本文主要測試如下四種場景,測試的指標主要是每秒多少兆字節數據,每秒多少條消息。

Producer Only

這組測試不使用任何Consumer,只啓動Broker和Producer。

Producer Number VS. Throughput

實驗條件:3個Broker,1個Topic,6個Partition,無Replication,異步模式,消息Payload爲100字節。

測試項目:分別測試1,2,3個Producer時的吞吐量。

測試目標:如Kafka設計解析(一)- Kafka背景及架構介紹所介紹,多個Producer可同時向同一個Topic發送數據,在Broker負載飽和前,理論上Producer數量越多,集羣每秒收到的消息量越大,並且呈線性增漲。本實驗主要驗證該特性。同時作爲性能測試,本實驗還將監控測試過程中單個Broker的CPU和內存使用情況

測試結果:使用不同個數Producer時的總吞吐率如下圖所示

由上圖可看出,單個Producer每秒可成功發送約128萬條Payload爲100字節的消息,並且隨着Producer個數的提升,每秒總共發送的消息量線性提升,符合之前的分析。

性能測試過程中,Broker的CPU和內存使用情況如下圖所示。

(點擊放大圖像)

由上圖可知,在每秒接收約117萬條消息(3個Producer總共每秒發送350萬條消息,平均每個Broker每秒接收約117萬條)的情況下,一個Broker的CPU使用量約爲248%,內存使用量爲601 MB。

Message Size VS. Throughput

實驗條件:3個Broker,1個Topic,6個Partition,無Replication,異步模式,3個Producer。

測試項目:分別測試消息長度爲10,20,40,60,80,100,150,200,400,800,1000,2000,5000,10000字節時的集羣總吞吐量。

測試結果:不同消息長度時的集羣總吞吐率如下圖所示:

由上圖可知,消息越長,每秒所能發送的消息數越少,而每秒所能發送的消息的量(MB)越大。另外,每條消息除了Payload外,還包含其它Metadata,所以每秒所發送的消息量比每秒發送的消息數乘以100字節大,而Payload越大,這些Metadata佔比越小,同時發送時的批量發送的消息體積越大,越容易得到更高的每秒消息量(MB/s)。其它測試中使用的Payload爲100字節,之所以使用這種短消息(相對短)只是爲了測試相對比較差的情況下的Kafka吞吐率。

Partition Number VS. Throughput

實驗條件:3個Broker,1個Topic,無Replication,異步模式,3個Producer,消息Payload爲100字節。

測試項目:分別測試1到9個Partition時的吞吐量。

測試結果:不同Partition數量時的集羣總吞吐率如下圖所示:

由上圖可知,當Partition數量小於Broker個數(3個)時,Partition數量越大,吞吐率越高,且呈線性提升。本文所有實驗中,只啓動3個Broker,而一個Partition只能存在於1個Broker上(不考慮Replication。即使有Replication,也只有其Leader接受讀寫請求),故當某個Topic只包含1個Partition時,實際只有1個Broker在爲該Topic工作。如之前文章所講,Kafka會將所有Partition均勻分佈到所有Broker上,所以當只有2個Partition時,會有2個Broker爲該Topic服務。3個Partition時同理會有3個Broker爲該Topic服務。換言之,Partition數量小於等於3個時,越多的Partition代表越多的Broker爲該Topic服務。如前幾篇文章所述,不同Broker上的數據並行插入,這就解釋了當Partition數量小於等於3個時,吞吐率隨Partition數量的增加線性提升。

當Partition數量多於Broker個數時,總吞吐量並未有所提升,甚至還有所下降。可能的原因是,當Partition數量爲4和5時,不同Broker上的Partition數量不同,而Producer會將數據均勻發送到各Partition上,這就造成各Broker的負載不同,不能最大化集羣吞吐量。而上圖中當Partition數量爲Broker數量整數倍時吞吐量明顯比其它情況高,也證實了這一點。

Replica Number VS. Throughput

實驗條件:3個Broker,1個Topic,6個Partition,異步模式,3個Producer,消息Payload爲100字節。

測試項目:分別測試1到3個Replica時的吞吐率。

測試結果:如下圖所示:

由上圖可知,隨着Replica數量的增加,吞吐率隨之下降。但吞吐率的下降並非線性下降,因爲多個Follower的數據複製是並行進行的,而非串行進行。

Consumer Only

實驗條件:3個Broker,1個Topic,6個Partition,無Replication,異步模式,消息Payload爲100字節。

測試項目:分別測試1到3個Consumer時的集羣總吞吐率。
測試結果:在集羣中已有大量消息的情況下,使用1到3個Consumer時的集羣總吞吐量如下圖所示:

由上圖可知,單個Consumer每秒可消費306萬條消息,該數量遠大於單個Producer每秒可消費的消息數量,這保證了在合理的配置下,消息可被及時處理。並且隨着Consumer數量的增加,集羣總吞吐量線性增加。

根據Kafka設計解析(四)- Kafka Consumer設計解析所述,多Consumer消費消息時以Partition爲分配單位,當只有1個Consumer時,該Consumer需要同時從6個Partition拉取消息,該Consumer所在機器的I/O成爲整個消費過程的瓶頸,而當Consumer個數增加至2個至3個時,多個Consumer同時從集羣拉取消息,充分利用了集羣的吞吐率。

Producer Consumer pair

實驗條件:3個Broker,1個Topic,6個Partition,無Replication,異步模式,消息Payload爲100字節。

測試項目:測試1個Producer和1個Consumer同時工作時Consumer所能消費到的消息量。

測試結果:1,215,613 records/second。


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