Categories
程式開發

系統解讀Kafka的流和表(二):主題、分區和存儲


這是探索Kafka存儲層和處理層核心基礎系列文章的第二篇。在這篇文章中,我們將深入了解Kafka的存儲層。我們將探索Kafka的主題,以及我認為Kafka最重要的概念:分區。

我們先從最基本的問題開始:Kafka是如何存儲數據的?

Kafka的主題是什麼東西?

主題屬於Kafka的存儲層,或許是Kafka最為人所熟知的一個概念。事件就保存在主題裡,主題類似於分佈式文件系統中的文件。保存和提供數據服務的機器叫作Kafka代理,也就是Kafka的服務器組件。

從概念上講,主題是沒有邊界的事件序列,事件就是鍵值對或“消息”。在實際當中,事件將包含時間戳,不過為了簡單起見,我們將忽略這些細節。主題有自己的名字,比如payments、truck-geolocations、cloud-metrics或customer-registrations。

主題有各種配置參數,比如壓縮(compaction)和數據保留策略。很多人把Kafka主題看成是臨時性的,你可以強製配置存儲限制(例如,配置一個主題最多存儲3TB事件,達到這個限制之後,舊事件會被刪除)或時間限制(例如,配置一個主題保留事件最多5年時間)。不過你也可以無限制地存儲數據,就像傳統的數據庫一樣,只需要把保留策略配置為無限制,事件就會被永久保存下來。紐約時報就是這麼做的,他們的大部分關鍵業務數據就保存在Kafka中,並將其作為單一的事實來源。

存儲格式:事件的序列化和反序列化

事件被寫入主題時被序列化,從主題讀取時被反序列化。反序列化是指將二進制數據轉成人類可理解的形式,序列化則反過來。需要注意的是,這些操作是由Kafka客戶端完成的,也就是那些應用程序,比如ksqlDB、Kafka Streams或者使用了Kafka Go語言客戶端的微服務。 Kafka有多種存儲格式,常見的有Avro、Protobuf和JSON。

Kafka代理不關心序列化格式或事件“類型”,它們只看到事件鍵值對的原始字節碼(在Java里為),所以代理不知道數據裡包含了什麼東西,數據對代理來說就像黑盒一樣。這種設計看起來很“笨”,但實際上是很聰明的,因為相比傳統的消息系統,Kafka代理具備了更好的伸縮性。

在事件流和類似的分佈式數據處理系統中,很多CPU時間被用在數據的序列化和反序列化上。可以想像一下,如果你要粉刷一個房間,花在計劃上的時間比花在粉刷上的時間還要多。所幸的是,Kafka代理不需要做這些事情!

分區存儲

Kafka的主題由分區組成,也就是說,一個主題包含了分佈在多個不同代理上的“桶”。這種分佈式數據存儲方式對伸縮性來說至關重要,因為客戶端可以同時從不同的代理讀取數據。

在創建主題時必須指定分區數量,每個分區將包含主題的部分數據。為了實現數據容錯,每個分區可以有多個副本,副本可以跨區域或跨數據中心,當發生故障或執行維護任務時,總會有幾個代理上的數據是可用的。常見的主題副本數量一般為3。

在我看來,分區是Kafka最基本的一個概念,它是Kafka伸縮性、彈性和容錯能力的基礎,我們將多次提到分區這個概念。

系統解讀Kafka的流和表(二):主題、分區和存儲 1

分區是最基本的構建塊,它為Kafka帶來了分佈式能力、伸縮性、彈性和容錯能力

事件生產者決定了事件的分區

Kafka將事件生產者和事件消費者解耦開,這也是Kafka比其他消息系統更具伸縮性的原因之一。生產者並不知道哪個消費者讀取了事件,讀取的頻率是怎樣的,或者是否讀取了事件。消費者可以是零個、幾十個、幾百個,甚至是幾千個。

生產者決定了事件的分區,即事件是如何分佈在同一個主題的不同分區裡的。確切地說,生產者使用了分區函數f(event.key, event.value)來決定一個事件應該被發送給主題的哪個分區。默認的分區函數是f(event.key, event.value) = hash(event.key) % numTopicPartitions,在大多數情況下,事件會被均勻地分佈在可用的分區上。分區函數實際上提供了除事件鍵以外的信息,比如主題的名字和集群元數據,不過這些東西不在本文的討論範圍之內。

系統解讀Kafka的流和表(二):主題、分區和存儲 2

在這個例子中,主題有4個分區,從P1到P4。兩個不同的生產者客戶端各自向主題發布事件,具有相關性的事件被寫到同一個分區。請注意,如果有必要,兩個生產者可以向同一個分區寫入數據。

如何給事件分區:具有相同鍵的事件放在同一個分區

之前已經有篇文章介紹瞭如何選擇正確的分區數量,所以現在我們將把注意力放在如何對事件進行分區上。分區的主要目標是保證事件的順序:生產者應該把“相關”的事件發送給相同的分區,因為Kafka只保證單個分區內的事件是有序的。

為了說明如何分區,我們以更新物流公司卡車地理位置信息為例。對於這種場景,同一輛卡車的事件應該被發送給同一個分區。我們可以為每一輛卡車選擇唯一的標識符作為事件的鍵(例如車牌或車架號),並使用默認的分區函數。

不過,除此之外,分區還有另外一個好處。流式處理應用程序通常會使用消費者群組,這些消費者同時讀取同一個主題。對於這種情況,我們需要控制不同的分區分配給了同一群組裡的不同消費者。

那麼,在哪些情況下具有相同鍵的事件會被分配給不同的分區?

  1. 主題的配置發生變化:有人增加了主題的分區數量。在這種情況下,默認的分區函數f(event.key, event.value)會為一小部分事件分配不同的分區,因為分區函數里的模數發生了變化。
  2. 生產者的配置發生變化:生產者使用了自定義的分區函數。

對於這類情況要格外小心,因為解決這些問題需要做額外的工作。為此,我們建議使用較大的分區數量,避免發生重新分區。

我個人建議一個主題使用30個分區,這個數字足以滿足一些高吞吐量場景的需求,同時又不超過一個代理可以處理的分區數量。另外,這個數字可以被1、2、3、5、6、10、15、30整除,可以均勻地分佈工作負載。 Kafka集群可以支持20萬個分區,所以這種使用大分區數量的做法對於大多數人來說是安全的。

總結

這篇文章介紹了Kafka的存儲層:主題、分區和代理,以及存儲格式和分區機制。在後續的文章中,我們將深入了解Kafka的數據處理層。我們將從事件的存儲跳到事件的處理,探索流和表以及數據契約和消費者群組,以及如何用這些東西實現分佈式大規模並行處理應用程序。

原文鏈接:

https://www.confluent.io/blog/kafka-streams-tables-part-2-topics-partitions-and-storage-fundamentals/

系列文章:

《系統解讀Kafka的流和表(一):開篇》