史上最全、最詳細的 kafka 學習筆記!

weixin_33866037發表於2018-11-27

一、為什麼需要訊息系統

1.解耦:

允許你獨立的擴充套件或修改兩邊的處理過程,只要確保它們遵守同樣的介面約束。

2.冗餘:

訊息佇列把資料進行持久化直到它們已經被完全處理,通過這一方式規避了資料丟失風險。許多訊息佇列所採用的"插入-獲取-刪除"正規化中,在把一個訊息從佇列中刪除之前,需要你的處理系統明確的指出該訊息已經被處理完畢,從而確保你的資料被安全的儲存直到你使用完畢。

3.擴充套件性:

因為訊息佇列解耦了你的處理過程,所以增大訊息入隊和處理的頻率是很容易的,只要另外增加處理過程即可。

4.靈活性 & 峰值處理能力:

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

5.可恢復性:

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

6.順序保證:

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

7.緩衝:

有助於控制和優化資料流經過系統的速度,解決生產訊息和消費訊息的處理速度不一致的情況。

8.非同步通訊:

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

二、kafka 架構

2.1 拓撲結構

如下圖:

13894260-907935b2cdb9a262

圖.1

2.2 相關概念

如圖.1中,kafka 相關名詞解釋如下:

1.producer:

訊息生產者,釋出訊息到kafka叢集的終端或服務。

2.broker:

kafka叢集中包含的伺服器。

3.topic:

每條釋出到kafka叢集的訊息屬於的類別,即kafka是面向topic的。

4.partition:

partition是物理上的概念,每個topic包含一個或多個partition。kafka分配的單位是partition。

5.consumer:

從kafka叢集中消費訊息的終端或服務。

6.Consumergroup:

high-levelconsumerAPI中,每個consumer都屬於一個consumergroup,每條訊息只能被consumergroup中的一個Consumer消費,但可以被多個consumergroup消費。

7.replica:

partition的副本,保障partition的高可用。

8.leader:

replica中的一個角色,producer和consumer只跟leader互動。

9.follower:

replica中的一個角色,從leader中複製資料。

10.controller:

kafka叢集中的其中一個伺服器,用來進行leaderelection以及 各種failover。

11.zookeeper:

kafka通過zookeeper來儲存叢集的meta資訊。

2.3 zookeeper 節點

kafka 在 zookeeper 中的儲存結構如下圖所示:

13894260-7989ca490bbf90ee

圖.2


三、producer 釋出訊息

3.1 寫入方式

producer 採用 push 模式將訊息釋出到 broker,每條訊息都被 append 到 patition 中,屬於順序寫磁碟(順序寫磁碟效率比隨機寫記憶體要高,保障 kafka 吞吐率)。

3.2 訊息路由

producer 傳送訊息到 broker 時,會根據分割槽演算法選擇將其儲存到哪一個 partition。其路由機制為:

指定了 patition,則直接使用;

未指定 patition 但指定 key,通過對 key 的 value 進行hash 選出一個 patition

 patition 和 key 都未指定,使用輪詢選出一個 patition。


附上 java 客戶端分割槽原始碼,一目瞭然:

//建立訊息例項

publicProducerRecord(String topic, Integer partition, Long timestamp, K key, Vvalue){

if(topic ==null)

thrownewIllegalArgumentException("Topic cannot be null");

if(timestamp !=null&& timestamp <0)

thrownewIllegalArgumentException("Invalid timestamp "+ timestamp);

this.topic = topic;

this.partition = partition;

this.key = key;

this.value=value;

this.timestamp = timestamp;

}

//計算 patition,如果指定了 patition 則直接使用,否則使用 key 計算

privateintpartition(ProducerRecord record,byte[] serializedKey ,byte[] serializedValue, Cluster cluster){

Integer partition = record.partition();

if(partition !=null) {

List partitions = cluster.partitionsForTopic(record.topic());

intlastPartition = partitions.size() -1;

if(partition <0|| partition > lastPartition) {

thrownewIllegalArgumentException(String.format("Invalid partition given with record: %d is not in the range [0...%d].", partition, lastPartition));

}

returnpartition;

}

returnthis.partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);

}

// 使用 key 選取 patition

publicintpartition(String topic, Object key,byte[] keyBytes, Objectvalue,byte[] valueBytes, Cluster cluster){

List partitions = cluster.partitionsForTopic(topic);

intnumPartitions = partitions.size();

if(keyBytes ==null) {

intnextValue = counter.getAndIncrement();

List availablePartitions = cluster.availablePartitionsForTopic(topic);

if(availablePartitions.size() >0) {

intpart = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();

returnavailablePartitions.get(part).partition();

}else{

returnDefaultPartitioner.toPositive(nextValue) % numPartitions;

}

}else{

//對 keyBytes 進行 hash 選出一個 patition

returnDefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;

}

}

3.3 寫入流程

 producer 寫入訊息序列圖如下所示:

13894260-f5aa072b7ab3d27a

圖.3

流程說明:

producer 先從 zookeeper 的 "/brokers/.../state" 節點找到該 partition 的 leader

 producer 將訊息傳送給該 leader

 leader 將訊息寫入本地 log

 followers 從 leader pull 訊息,寫入本地 log 後 leader 傳送 ACK

leader 收到所有 ISR 中的 replica 的 ACK 後,增加 HW(high watermark,最後 commit 的 offset) 並向 producer 傳送 ACK

3.4 producer delivery guarantee

 一般情況下存在三種情況:

At most once 訊息可能會丟,但絕不會重複傳輸

 At least one 訊息絕不會丟,但可能會重複傳輸

 Exactly once 每條訊息肯定會被傳輸一次且僅傳輸一次

當 producer 向 broker 傳送訊息時,一旦這條訊息被 commit,由於 replication 的存在,它就不會丟。但是如果 producer 傳送資料給 broker 後,遇到網路問題而造成通訊中斷,那 Producer 就無法判斷該條訊息是否已經 commit。雖然 Kafka 無法確定網路故障期間發生了什麼,但是 producer 可以生成一種類似於主鍵的東西,發生故障時冪等性的重試多次,這樣就做到了 Exactly once,但目前還並未實現。所以目前預設情況下一條訊息從 producer 到 broker 是確保了 At least once,可通過設定 producer 非同步傳送實現At most once。


四、broker 儲存訊息

4.1 儲存方式

物理上把 topic 分成一個或多個 patition(對應 server.properties 中的 num.partitions=3 配置),每個 patition 物理上對應一個資料夾(該資料夾儲存該 patition 的所有訊息和索引檔案),如下:

13894260-089fa15a0e60a741

圖.4

4.2 儲存策略

無論訊息是否被消費,kafka 都會保留所有訊息。有兩種策略可以刪除舊資料:

基於時間:log.retention.hours=168

基於大小:log.retention.bytes=1073741824

需要注意的是,因為Kafka讀取特定訊息的時間複雜度為O(1),即與檔案大小無關,所以這裡刪除過期檔案與提高 Kafka 效能無關。

4.3 topic 建立與刪除

4.3.1 建立 topic

建立 topic 的序列圖如下所示:

13894260-6a036b167bd57897

圖.5

流程說明:

 controller 在 ZooKeeper 的 /brokers/topics 節點上註冊 watcher,當 topic 被建立,則 controller 會通過 watch 得到該 topic 的 partition/replica 分配。

controller從 /brokers/ids 讀取當前所有可用的 broker 列表,對於 set_p 中的每一個 partition:

2.1 、從分配給該 partition 的所有 replica(稱為AR)中任選一個可用的 broker 作為新的 leader,並將AR設定為新的 ISR

2.2 、將新的 leader 和 ISR 寫入 /brokers/topics/[topic]/partitions/[partition]/state

 controller 通過 RPC 向相關的 broker 傳送 LeaderAndISRRequest。

4.3.2 刪除 topic

刪除 topic 的序列圖如下所示:

13894260-d976774aaefebb38

圖.6

流程說明:

 controller 在 zooKeeper 的 /brokers/topics 節點上註冊 watcher,當 topic 被刪除,則 controller 會通過 watch 得到該 topic 的 partition/replica 分配。

若 delete.topic.enable=false,結束;否則 controller 註冊在 /admin/delete_topics 上的 watch 被 fire,controller 通過回撥向對應的 broker 傳送 StopReplicaRequest。

五、kafka HA

5.1 replication

如圖.1所示,同一個 partition 可能會有多個 replica(對應 server.properties 配置中的 default.replication.factor=N)。沒有 replica 的情況下,一旦 broker 當機,其上所有 patition 的資料都不可被消費,同時 producer 也不能再將資料存於其上的 patition。引入replication 之後,同一個 partition 可能會有多個 replica,而這時需要在這些 replica 之間選出一個 leader,producer 和 consumer 只與這個 leader 互動,其它 replica 作為 follower 從 leader 中複製資料。

Kafka 分配 Replica 的演算法如下:

1. 將所有 broker(假設共 n 個 broker)和待分配的 partition 排序

2. 將第 i 個 partition 分配到第(i mod n)個 broker 上

3. 將第 i 個 partition 的第 j 個 replica 分配到第((i + j) mode n)個 broker上

5.2 leader failover

當 partition 對應的 leader 當機時,需要從 follower 中選舉出新 leader。在選舉新leader時,一個基本的原則是,新的 leader 必須擁有舊 leader commit 過的所有訊息。

kafka 在 zookeeper 中(/brokers/.../state)動態維護了一個 ISR(in-sync replicas),由3.3節的寫入流程可知 ISR 裡面的所有 replica 都跟上了 leader,只有 ISR 裡面的成員才能選為 leader。對於 f+1 個 replica,一個 partition 可以在容忍 f 個 replica 失效的情況下保證訊息不丟失。

當所有 replica 都不工作時,有兩種可行的方案:

1. 等待 ISR 中的任一個 replica 活過來,並選它作為 leader。可保障資料不丟失,但時間可能相對較長。

2. 選擇第一個活過來的 replica(不一定是 ISR 成員)作為 leader。無法保障資料不丟失,但相對不可用時間較短。

kafka 0.8.* 使用第二種方式。

kafka 通過 Controller 來選舉 leader,流程請參考5.3節。

5.3 broker failover

kafka broker failover 序列圖如下所示:

13894260-67f07abb78ea68a4

圖.7

流程說明: 

1. controller 在 zookeeper 的 /brokers/ids/[brokerId] 節點註冊 Watcher,當 broker 當機時 zookeeper 會 fire watch

2. controller 從 /brokers/ids 節點讀取可用broker

3. controller決定set_p,該集合包含當機 broker 上的所有 partition

4. 對 set_p 中的每一個 partition

    4.1 從/brokers/topics/[topic]/partitions/[partition]/state 節點讀取 ISR

    4.2 決定新 leader(如4.3節所描述)

    4.3 將新 leader、ISR、controller_epoch 和 leader_epoch 等資訊寫入 state 節點

5. 通過 RPC 向相關 broker 傳送 leaderAndISRRequest 命令

5.4 controller failover

 當 controller 當機時會觸發 controller failover。每個 broker 都會在 zookeeper 的 "/controller" 節點註冊 watcher,當 controller 當機時 zookeeper 中的臨時節點消失,所有存活的 broker 收到 fire 的通知,每個 broker 都嘗試建立新的 controller path,只有一個競選成功並當選為 controller。

當新的 controller 當選時,會觸發 KafkaController.onControllerFailover 方法,在該方法中完成如下操作:

1. 讀取並增加 Controller Epoch。

2. 在 reassignedPartitions Patch(/admin/reassign_partitions) 上註冊 watcher。

3. 在 preferredReplicaElection Path(/admin/preferred_replica_election) 上註冊 watcher。

4. 通過 partitionStateMachine 在 broker Topics Patch(/brokers/topics) 上註冊 watcher。

5. 若 delete.topic.enable=true(預設值是false),則 partitionStateMachine 在DeleteTopicPatch(/admin/delete_topics) 上註冊 watcher。

6.通過 replicaStateMachine在 Broker IdsPatch(/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且DeleteTopicPatch(/admin/delete_topics)中有值,則刪除相應的Topic。

6. consumer 消費訊息

6.1 consumer API

kafka 提供了兩套 consumer API:

1. The high-level Consumer API

2. The SimpleConsumer API

 其中 high-level consumer API 提供了一個從 kafka 消費資料的高層抽象,而 SimpleConsumer API 則需要開發人員更多地關注細節。

6.1.1 The high-level consumer API

high-level consumer API 提供了 consumer group 的語義,一個訊息只能被 group 內的一個 consumer 所消費,且 consumer 消費訊息時不關注 offset,最後一個 offset 由 zookeeper 儲存。

使用 high-level consumer API 可以是多執行緒的應用,應當注意:

1. 如果消費執行緒大於 patition 數量,則有些執行緒將收不到訊息

2. 如果 patition 數量大於執行緒數,則有些執行緒多收到多個 patition 的訊息

3. 如果一個執行緒消費多個 patition,則無法保證你收到的訊息的順序,而一個 patition 內的訊息是有序的

6.1.2 The SimpleConsumer API

如果你想要對 patition 有更多的控制權,那就應該使用 SimpleConsumer API,比如:

1. 多次讀取一個訊息

2. 只消費一個 patition 中的部分訊息

3. 使用事務來保證一個訊息僅被消費一次

 但是使用此 API 時,partition、offset、broker、leader 等對你不再透明,需要自己去管理。你需要做大量的額外工作:

1. 必須在應用程式中跟蹤 offset,從而確定下一條應該消費哪條訊息

2. 應用程式需要通過程式獲知每個 Partition 的 leader 是誰

3. 需要處理 leader 的變更

使用 SimpleConsumer API 的一般流程如下:

1. 查詢到一個“活著”的 broker,並且找出每個 partition 的 leader

2. 找出每個 partition 的 follower

3. 定義好請求,該請求應該能描述應用程式需要哪些資料

4. fetch 資料

5. 識別 leader 的變化,並對之作出必要的響應

以下針對 high-level Consumer API 進行說明。

6.2 consumer group

如 2.2 節所說, kafka 的分配單位是 patition。每個 consumer 都屬於一個 group,一個 partition 只能被同一個 group 內的一個 consumer 所消費(也就保障了一個訊息只能被 group 內的一個 consuemr 所消費),但是多個 group 可以同時消費這個 partition。

kafka 的設計目標之一就是同時實現離線處理和實時處理,根據這一特性,可以使用 spark/Storm 這些實時處理系統對訊息線上處理,同時使用 Hadoop 批處理系統進行離線處理,還可以將資料備份到另一個資料中心,只需要保證這三者屬於不同的 consumer group。如下圖所示:

13894260-33a867e5615bb372

圖.8

6.3 消費方式

consumer 採用 pull 模式從 broker 中讀取資料。

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

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

6.4 consumer delivery guarantee

如果將 consumer 設定為 autocommit,consumer 一旦讀到資料立即自動 commit。如果只討論這一讀取訊息的過程,那 Kafka 確保了 Exactly once。

但實際使用中應用程式並非在 consumer 讀取完資料就結束了,而是要進行進一步處理,而資料處理與 commit 的順序在很大程度上決定了consumer delivery guarantee:

1.讀完訊息先 commit 再處理訊息。

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

2.讀完訊息先處理再 commit。

  這種模式下,如果在處理完訊息之後 commit 之前 consumer crash 了,下次重新開始工作時還會處理剛剛未 commit 的訊息,實際上該訊息已經被處理過了。這就對應於 At least once。

3.如果一定要做到 Exactly once,就需要協調 offset 和實際操作的輸出。

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

總之,Kafka 預設保證 At least once,並且允許通過設定 producer 非同步提交來實現 At most once(見文章《kafka consumer防止資料丟失》)。而 Exactly once 要求與外部儲存系統協作,幸運的是 kafka 提供的 offset 可以非常直接非常容易得使用這種方式。

6.5 consumer rebalance

當有 consumer 加入或退出、以及 partition 的改變(如 broker 加入或退出)時會觸發 rebalance。consumer rebalance演算法如下:

1. 將目標 topic 下的所有 partirtion 排序,存於PT

2. 對某 consumer group 下所有 consumer 排序,存於 CG,第 i 個consumer 記為 Ci

3. N=size(PT)/size(CG),向上取整

4. 解除 Ci 對原來分配的 partition 的消費權(i從0開始)

5. 將第i*N到(i+1)*N-1個 partition 分配給 Ci

在 0.8.*版本,每個 consumer 都只負責調整自己所消費的 partition,為了保證整個consumer group 的一致性,當一個 consumer 觸發了 rebalance 時,該 consumer group 內的其它所有其它 consumer 也應該同時觸發 rebalance。這會導致以下幾個問題:

1.Herd effect

 任何 broker 或者 consumer 的增減都會觸發所有的 consumer 的 rebalance

2.Split Brain

 每個 consumer 分別單獨通過 zookeeper 判斷哪些 broker 和 consumer 當機了,那麼不同 consumer 在同一時刻從 zookeeper 看到的 view 就可能不一樣,這是由 zookeeper 的特性決定的,這就會造成不正確的 reblance 嘗試。

3. 調整結果不可控

 所有的 consumer 都並不知道其它 consumer 的 rebalance 是否成功,這可能會導致 kafka 工作在一個不正確的狀態。

基於以上問題,kafka 設計者考慮在0.9.*版本開始使用中心 coordinator 來控制 consumer rebalance,然後又從簡便性和驗證要求兩方面考慮,計劃在 consumer 客戶端實現分配方案。(見文章《Kafka Detailed Consumer Coordinator Design》和《Kafka Client-side Assignment Proposal》),此處不再贅述。

七、注意事項

7.1 producer 無法傳送訊息的問題

最開始在本機搭建了kafka偽叢集,本地 producer 客戶端成功釋出訊息至 broker。隨後在伺服器上搭建了 kafka 叢集,在本機連線該叢集,producer 卻無法釋出訊息到 broker(奇怪也沒有拋錯)。最開始懷疑是 iptables 沒開放,於是開放埠,結果還不行(又開始是程式碼問題、版本問題等等,倒騰了很久)。最後沒辦法,一項一項檢視 server.properties 配置,發現以下兩個配置:

# The address the socket server listens on. It will get the value returned from 

# java.net.InetAddress.getCanonicalHostName() if not configured.

#   FORMAT:

#     listeners = security_protocol://host_name:port

#   EXAMPLE:

#     listeners = PLAINTEXT://your.host.name:9092

listeners=PLAINTEXT://:9092

# Hostname and port the broker will advertise to producers and consumers. If not set, 

# it uses the value for "listeners" if configured. Otherwise, it will use the value

# returned from java.net.InetAddress.getCanonicalHostName().

#advertised.listeners=PLAINTEXT://your.host.name:9092

以上說的就是 advertised.listeners 是 broker 給 producer 和 consumer 連線使用的,如果沒有設定,就使用 listeners,而如果 host_name 沒有設定的話,就使用 java.net.InetAddress.getCanonicalHostName() 方法返回的主機名。

相關文章