1. Kafka簡介
Kafka是一種分布式的,基于發布/訂閱的消息系統。主要設計目標如下:
- 以時間復雜度為O(1)的方式提供消息持久化能力,即使對TB級以上數據也能保證常數時間復雜度的訪問性能
- 高吞吐率。即使在非常廉價的商用機器上也能做到單機支持每秒100K條以上消息的傳輸
- 支持Kafka Server間的消息分區,及分布式消費,同時保證每個Partition內的消息順序傳輸
- 同時支持離線數據處理和實時數據處理
- Scale out:支持在線水平擴展
2. Kafka架構
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)。
3. 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訂閱并消費消息。
4. Delivery Guarantee
有這么幾種可能的delivery guarantee:
-
At most once
消息可能會丟,但絕不會重復傳輸 -
At least one
消息絕不會丟,但可能會重復傳輸 -
Exactly once
每條消息肯定會被傳輸一次且僅傳輸一次,很多時候這是用戶所想要的。
5. Kafka HA設計解析
5.1 Replica復制算法
? 為了更好的做負載均衡,Kafka盡量將所有的Partition均勻分配到整個集群上。一個典型的部署方式是一個Topic的Partition數量大于Broker的數量。同時為了提高Kafka的容錯能力,也需要將同一個Partition的Replica盡量分散到不同的機器。實際上,如果所有的Replica都在同一個Broker上,那一旦該Broker宕機,該Partition的所有Replica都無法工作,也就達不到HA的效果。同時,如果某個Broker宕機了,需要保證它上面的負載可以被均勻的分配到其它幸存的所有Broker上。
- 將所有Broker(假設共n個Broker)和待分配的Partition排序
- 將第i個Partition分配到第(i mod n)個Broker上
- 將第i個Partition的第j個Replica分配到第((i + j) mod n)個Broker上
5.2 Broker活著的判定
kafka判定broker是否活著,通過以下2個方式:
- 和zk的session沒有斷(通過心跳來維系)
- follower能及時將leader消息復制過來,不能落后太多(例如默認lag超過4000就會踢出ISR)
5.3 所有Replica都不工作的情況
如果所有副本都出問題,一般有兩種選擇:
- 等待ISR中的任一個Replica“活”過來,并且選它作為Leader(一致性好,但是可用性差)
- 選擇第一個“活”過來的Replica(不一定是ISR中的)作為Leader(一致性差,但是可用性相比第一種方式好)
5.4 Propagate消息
? Producer在發布消息到某個Partition時,先通過 Metadata (通過 Broker 獲取并且緩存在 Producer 內) 找到該 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消費。但考慮到這種場景非常少見,可以認為這種方式在性能和數據持久化上做了一個比較好的平衡。
5.5 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.time.max.ms
來配置,其默認值是10000)未向Leader發送fetch請求。。
Kafka的復制機制既不是完全的同步復制,也不是單純的異步復制。事實上,同步復制要求所有能工作的Follower都復制完,這條消息才會被認為commit,這種復制方式極大的影響了吞吐率(高吞吐率是Kafka非常重要的一個特性)。而異步復制方式下,Follower異步的從Leader復制數據,數據只要被Leader寫入log就被認為已經commit,這種情況下如果Follower都復制完都落后于Leader,而如果Leader突然宕機,則會丟失數據。而Kafka的這種使用ISR的方式則很好的均衡了確保數據不丟失以及吞吐率。Follower可以批量的從Leader復制數據,這樣極大的提高復制性能(批量寫磁盤),極大減少了Follower與Leader的差距。
6. 分區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負載會過重。
改進的方法——所有broker中選出一個controller,所有Partition的Leader選舉都由controller決定。controller會將Leader的改變直接通過RPC的方式(比Zookeeper Queue的方式更高效)通知需為此作出響應的Broker。同時controller也負責增刪Topic以及Replica的重新分配。
7. 各組件Failover過程
Broker failover過程
- Controller在Zookeeper注冊Watch,一旦有Broker宕機(這是用宕機代表任何讓系統認為其die的情景,包括但不限于機器斷電,網絡不可用,GC導致的Stop The World,進程crash等),其在Zookeeper對應的znode會自動被刪除,Zookeeper會fire Controller注冊的watch,Controller讀取最新的幸存的Broker
- Controller決定set_p,該集合包含了宕機的所有Broker上的所有Partition
- 對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_epoch及controller_epoch寫入/brokers/topics/[topic]/partitions/[partition]/state。注意,該操作只有其version在3.1至3.3的過程中無變化時才會執行,否則跳轉到3.1
- 直接通過RPC向set_p相關的Broker發送LeaderAndISRRequest命令。Controller可以在一個RPC操作中發送多個命令從而提高效率。
Broker failover順序圖如下所示。
Controller failure過程
- Controller在Zookeeper的
/brokers/ids
節點上注冊Watch。一旦有Broker宕機(本文用宕機代表任何讓Kafka認為其Broker die的情景,包括但不限于機器斷電,網絡不可用,GC導致的Stop The World,進程crash等),其在Zookeeper對應的Znode會自動被刪除,Zookeeper會fire Controller注冊的Watch,Controller即可獲取最新的幸存的Broker列表。 - Controller決定set_p,該集合包含了宕機的所有Broker上的所有Partition。
- 對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_epoch
及controller_epoch
寫入/brokers/topics/[topic]/partitions/[partition]/state
。注意,該操作只有Controller版本在3.1至3.3的過程中無變化時才會執行,否則跳轉到3.1。 - 直接通過RPC向set_p相關的Broker發送LeaderAndISRRequest命令。Controller可以在一個RPC操作中發送多個命令從而提高效率。
Broker failover順序圖如下所示。
Partition重新分配
管理工具發出重新分配Partition請求后,會將相應信息寫到/admin/reassign_partitions
上,而該操作會觸發ReassignedPartitionsIsrChangeListener,從而通過執行回調函數KafkaController.onPartitionReassignment來完成以下操作:
- 將Zookeeper中的AR(Current Assigned Replicas)更新為OAR(Original list of replicas for partition) + RAR(Reassigned replicas)。
- 強制更新Zookeeper中的leader epoch,向AR中的每個Replica發送LeaderAndIsrRequest。
- 將RAR - OAR中的Replica設置為NewReplica狀態。
- 等待直到RAR中所有的Replica都與其Leader同步。
- 將RAR中所有的Replica都設置為OnlineReplica狀態。
- 將Cache中的AR設置為RAR。
- 若Leader不在RAR中,則從RAR中重新選舉出一個新的Leader并發送LeaderAndIsrRequest。若新的Leader不是從RAR中選舉而出,則還要增加Zookeeper中的leader epoch。
- 將OAR - RAR中的所有Replica設置為OfflineReplica狀態,該過程包含兩部分。第一,將Zookeeper上ISR中的OAR - RAR移除并向Leader發送LeaderAndIsrRequest從而通知這些Replica已經從ISR中移除;第二,向OAR - RAR中的Replica發送StopReplicaRequest從而停止不再分配給該Partition的Replica。
- 將OAR - RAR中的所有Replica設置為NonExistentReplica狀態從而將其從磁盤上刪除。
- 將Zookeeper中的AR設置為RAR。
- 刪除
/admin/reassign_partition
。
參考文章
如果大家喜歡我的文章,可以關注個人訂閱號。歡迎隨時留言、交流。