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

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

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 記錄型別的完全限定名。這個設定允許同一主題包含不同型別的事件,並進一步對當前主題進行相容性檢查。 有了這個新特性,你就可以輕鬆地將屬於特定實體的所有不同型別的事件放在同一個主題中。現在,你可以自由選擇主題的粒度,而不僅限於一個型別對應一個主題。

喜歡小編輕輕點關注哦!