1. 程式人生 > >Kafka實踐:到底該不該把不同類型的消息放在同一個主題中?

Kafka實踐:到底該不該把不同類型的消息放在同一個主題中?

所有 是我 可能 兩個 follow 形式 解釋 特定 示例

Kafka 主題最重要的一個功能是可以讓消費者指定它們想要消費的消息子集。在極端情況下,將所有數據放在同一個主題中可能不是一個好主意,因為這樣消費者就無法選擇它們感興趣的事件——它們需要消費所有的消息。另一種極端情況,擁有數百萬個不同的主題也不是一個好主意,因為 Kafka 的每個主題都是有成本的,擁有大量主題會損害性能。

實際上,從性能的角度來看,分區數量才是關鍵因素。在 Kafka 中,每個主題至少對應一個分區,如果你有 n 個主題,至少會有 n 個分區。不久之前,Jun Rao 寫了一篇博文,解釋了擁有多個分區的成本(端到端延遲、文件描述符、內存開銷、發生故障後的恢復時間)。根據經驗,如果你關心延遲,那麽每個節點分配幾百個分區就可以了。如果每個節點的分區數量超過成千上萬個,就會造成較大的延遲。

關於性能的討論為設計主題結構提供了一些指導:如果你發現自己有數千個主題,那麽將一些細粒度、低吞吐量的主題合並到粗粒度主題中可能是個明智之舉,這樣可以避免分區數量蔓延。

然而,性能並不是我們唯一關心的問題。在我看來,更重要的是主題結構的數據完整性和數據模型。我們將在本文的其余部分討論這些內容。

主題等於相同類型事件的集合?

人們普遍認為應該將相同類型的事件放在同一主題中,不同的事件類型應該使用不同的主題。這種思路讓我們聯想到關系型數據庫,其中表是相同類型記錄的集合,於是我們就有了數據庫表和 Kafka 主題之間的類比。

Confluent Avro Schema Registry 進一步強化了這種概念,因為它鼓勵你對主題的所有消息使用相同的 Avro 模式(schema)。模式可以在保持兼容性的同時進行演化(例如通過添加可選字段),但所有消息都必須符合某種記錄類型。稍後我會再回過頭來討論這個問題。

對於某些類型的流式數據,例如活動事件,要求同一主題中所有消息都符合相同的模式,這是合理的。但是,有些人把 Kafka 當成了數據庫來用,例如事件溯源,或者在微服務之間交換數據。對於這種情況,我認為是否將主題定義為具有相同模式的消息集合就不那麽重要了。這個時候,更重要的是主題分區中的消息必須是有序的。

想象一下這樣的場景:你有一個實體(比如客戶),這個實體可能會發生許多不同的事情,比如創建客戶、客戶更改地址、客戶向帳戶中添加新的信用卡、客戶發起客服請求,客戶支付賬單、客戶關閉帳戶。

這些事件之間的順序很重要。例如,我們希望其他事件必須在創建客戶之後才能發生,並且在客戶關閉帳戶之後不能再發生其他事件。在使用 Kafka 時,你可以將它們全部放在同一個主題分區中來保持它們的順序。在這個示例中,你可以使用客戶 ID 作為分區的鍵,然後將所有事件放在同一個主題中。它們必須位於同一主題中,因為不同的主題對應不同的分區,而 Kafka 是不保證分區之間的順序的。

順序問題

如果你為 customerCreated、customerAddressChanged 和 customerInvoicePaid 事件使用了不同的主題,那麽這些主題的消費者可能就看不到這些事件之間的順序。例如,消費者可能會看到一個不存在的客戶做出的地址變更(這個客戶尚未創建,因為相應的 customerCreated 事件可能發生了延遲)。

如果消費者暫停一段時間(比如進行維護或部署新版本),那麽事件出現亂序的可能性就更高了。在消費者停止期間,事件繼續發布,並且這些事件被存儲在特定定的主題分區中。當消費者再次啟動時,它會消費所有積壓在分區中的事件。如果消費者只消費一個分區,那就沒問題:積壓的事件會按照它們存儲的順序依次被處理。但是,如果消費者同時消費幾個主題,就會按任意順序讀取主題中數據。它可以先讀取積壓在一個主題上的所有數據,然後再讀取另一個主題上積壓的數據,或者交錯地讀取多個主題的數據。

因此,如果你將 customerCreated、customerAddressChanged 和 customerInvoicePaid 事件放在三個單獨的主題中,那麽消費者可能會在看到 customerCreated 事件之前先看到 customerAddressChanged 事件。因此,消費者很可能會看到一個客戶的 customerAddressChanged 事件,但這個客戶卻未被創建。

你可能會想到為每條消息附加時間戳,並用它來對事件進行排序。如果你將事件導入數據倉庫,再對事件進行排序,或許是沒有問題的。但在流數據中只使用時間戳是不夠的:在你收到一個具有特定時間戳的事件時,你不知道是否需要等待具有較早時間戳的事件,或者所有之前的事件是否已經在當前事情之前到達。依靠時鐘進行同步通常會導致噩夢,有關時鐘問題的更多詳細信息,請參閱“Designing Data-Intensive Applications”的第 8 章。

何時拆分主題,何時合並主題?

基於這個背景,我將給出一些經驗之談,幫你確定哪些數據應該放在同一主題中,以及哪些數據應該放在不同的主題中。

首先,需要保持固定順序的事件必須放在同一主題中(並且需要使用相同的分區鍵)。如果事件屬於同一實體,那麽事件的順序就很重要。因此,我們可以說,同一實體的所有事件都應該保存在同一主題中。
如果你使用事件溯源進行數據建模,事件的排序尤為重要。聚合對象的狀態是通過以特定的順序重放事件日誌而得出的。因此,即使可能存在不同的事件類型,聚合所需要的所有事件也必須在同一主題中。
對於不同實體的事件,它們應該保存在相同的主題中還是不同的主題中?我想說,如果一個實體依賴於另一個實體(例如一個地址屬於一個客戶),或者經常需要同時用到它們,那麽它們也應該保存在同一主題中。另一方面,如果它們不相關,並且屬於不同的團隊,那麽最好將它們放在不同的主題中。
另外,這也取決於事件的吞吐量:如果一個實體類型的事件吞吐量比其他實體要高很多,那麽最好將它分成幾個主題,以免讓只想消費低吞吐量實體的消費者不堪重負(參見第 4 點)。不過,可以將多個具有低吞吐量的實體合並起來。
如果一個事件涉及多個實體該怎麽辦?例如,訂單涉及到產品和客戶,轉賬至少涉及到兩個賬戶。
我建議在一開始將這些事件記錄為單個原子消息,而不是將其分成幾個屬於不同主題的消息。在記錄事件時,最好可以保持原封不動,即盡可能保持數據的原始形式。你可以隨後使用流式處理器來拆分復合事件,但如果過早進行拆分,想要重建原始事件會難得多。如果能夠為初始事件分配一個唯一 ID(例如 UUID)就更好了,之後如果你要拆分原始事件,可以帶上這個 ID,從而可以追溯到每個事件的起源。
看看消費者需要訂閱的主題數量。如果幾個消費者都訂閱了一組特定的主題,這表明可能需要將這些主題合並在一起。
如果將細粒度的主題合並成粗粒度的主題,一些消費者可能會收到他們不需要的事件,需要將其忽略。這不是什麽大問題:消費消息的成本非常低,即使最終忽略了一大半的事件,總的成本可能也不會很大。只有當消費者需要忽略絕大多數消息(例如 99.9%是不需要的)時,我才建議將大容量事件流拆分成小容量事件流。
用作 Kafka Streams 狀態存儲(KTable)的變更日誌主題應該與其他主題分開。在這種情況下,這些主題由 Kafka Streams 流程來管理,所以不應該包含其他類型的事件。
最後,如果基於上述的規則依然無法做出正確的判斷,該怎麽辦?那麽就按照類型對事件進行分組,把相同類型的事件放在同一個主題中。不過,我認為這條規則是最不重要的。
模式管理

如果你的數據是普通文本(如 JSON),而且沒有使用靜態的模式,那麽就可以輕松地將不同類型的事件放在同一個主題中。但是,如果你使用了模式編碼(如 Avro),那麽在單個主題中保存多種類型的事件則需要考慮更多的事情。

如上所述,基於 Avro 的 Kafka Confluent Schema Registry 假設了一個前提,即每個主題都有一個模式(更確切地說,一個模式用於消息的鍵,一個模式用於消息的值)。你可以註冊新版本的模式,註冊表會檢查模式是否向前和向後兼容。這樣設計的一個好處是,你可以讓不同的生產者和消費者同時使用不同版本的模式,並且仍然保持彼此的兼容性。

Confluent 的 Avro 序列化器通過 subject 名稱在註冊表中註冊模式。默認情況下,消息鍵的 subject 為-key,消息值的 subject 為-value。模式註冊表會檢查在特定 subject 下註冊的所有模式的相互兼容性。

最近,我為 Avro 序列化器提供了一個補丁(https://github.com/confluentinc/schema-registry/pull/680 ),讓兼容性檢查變得更加靈活。這個補丁添加了兩個新的配置選項:key.subject.name.strategy(用於定義如何構造消息鍵的 subject 名稱)和 value.subject.name.strategy(用於定義如何構造消息值的 subject 名稱)。它們的值可以是如下幾個:

io.confluent.kafka.serializers.subject.TopicNameStrategy(默認):消息鍵的 subject 名稱為-key,消息值為-value。這意味著主題中所有消息的模式必須相互兼容。
io.confluent.kafka.serializers.subject.RecordNameStrategy:subject 名稱是 Avro 記錄類型的完全限定名。因此,模式註冊表會檢查特定記錄類型的兼容性,而不管是哪個主題。這個設置允許同一主題包含不同類型的事件。
io.confluent.kafka.serializers.subject.TopicRecordNameStrategy:subject 名稱是-,其中是 Kafka 主題名,是 Avro 記錄類型的完全限定名。這個設置允許同一主題包含不同類型的事件,並進一步對當前主題進行兼容性檢查。
有了這個新特性,你就可以輕松地將屬於特定實體的所有不同類型的事件放在同一個主題中。現在,你可以自由選擇主題的粒度,而不僅限於一個類型對應一個主題。

喜歡小編輕輕點關註哦!

Kafka實踐:到底該不該把不同類型的消息放在同一個主題中?