Kafka实践:到底该不该把不同类型的消息放在同一个主题中
如果你使用了像Kafka這樣的流式處理平臺,就要搞清楚一件事情:你需要用到哪些主題?特別是如果你要將一堆不同的事件作為消息發(fā)布到Kafka,是將它們放在同一個主題中,還是將它們拆分到不同的主題中?
Kafka主題最重要的一個功能是可以讓消費(fèi)者指定它們想要消費(fèi)的消息子集。在極端情況下,將所有數(shù)據(jù)放在同一個主題中可能不是一個好主意,因?yàn)檫@樣消費(fèi)者就無法選擇它們感興趣的事件——它們需要消費(fèi)所有的消息。另一種極端情況,擁有數(shù)百萬個不同的主題也不是一個好主意,因?yàn)镵afka的每個主題都是有成本的,擁有大量主題會損害性能。
實(shí)際上,從性能的角度來看,分區(qū)數(shù)量才是關(guān)鍵因素。在Kafka中,每個主題至少對應(yīng)一個分區(qū),如果你有n個主題,至少會有n個分區(qū)。不久之前,Jun Rao寫了一篇博文,解釋了擁有多個分區(qū)的成本(端到端延遲、文件描述符、內(nèi)存開銷、發(fā)生故障后的恢復(fù)時間)。根據(jù)經(jīng)驗(yàn),如果你關(guān)心延遲,那么每個節(jié)點(diǎn)分配幾百個分區(qū)就可以了。如果每個節(jié)點(diǎn)的分區(qū)數(shù)量超過成千上萬個,就會造成較大的延遲。
相關(guān)廠商內(nèi)容
實(shí)時監(jiān)控業(yè)務(wù)質(zhì)量現(xiàn)狀
機(jī)器學(xué)習(xí)在大規(guī)模服務(wù)器治理復(fù)雜場景的實(shí)踐
如何構(gòu)建微服務(wù)下的性能監(jiān)控
基于NEO區(qū)塊鏈的專家網(wǎng)絡(luò)應(yīng)用實(shí)踐
2018年,你應(yīng)該關(guān)注這些運(yùn)維技術(shù)熱點(diǎn)
相關(guān)贊助商
CNUTCon全球運(yùn)維技術(shù)大會,11月16日-17日,上海,|https://cnutcon2018.geekbang.org/?utm_source=infoq&utm_medium=vcrbox]]
關(guān)于性能的討論為設(shè)計主題結(jié)構(gòu)提供了一些指導(dǎo):如果你發(fā)現(xiàn)自己有數(shù)千個主題,那么將一些細(xì)粒度、低吞吐量的主題合并到粗粒度主題中可能是個明智之舉,這樣可以避免分區(qū)數(shù)量蔓延。
然而,性能并不是我們唯一關(guān)心的問題。在我看來,更重要的是主題結(jié)構(gòu)的數(shù)據(jù)完整性和數(shù)據(jù)模型。我們將在本文的其余部分討論這些內(nèi)容。
主題等于相同類型事件的集合?
人們普遍認(rèn)為應(yīng)該將相同類型的事件放在同一主題中,不同的事件類型應(yīng)該使用不同的主題。這種思路讓我們聯(lián)想到關(guān)系型數(shù)據(jù)庫,其中表是相同類型記錄的集合,于是我們就有了數(shù)據(jù)庫表和Kafka主題之間的類比。
Confluent Avro Schema Registry進(jìn)一步強(qiáng)化了這種概念,因?yàn)樗膭钅銓χ黝}的所有消息使用相同的Avro模式(schema)。模式可以在保持兼容性的同時進(jìn)行演化(例如通過添加可選字段),但所有消息都必須符合某種記錄類型。稍后我會再回過頭來討論這個問題。
對于某些類型的流式數(shù)據(jù),例如活動事件,要求同一主題中所有消息都符合相同的模式,這是合理的。但是,有些人把Kafka當(dāng)成了數(shù)據(jù)庫來用,例如事件溯源,或者在微服務(wù)之間交換數(shù)據(jù)。對于這種情況,我認(rèn)為是否將主題定義為具有相同模式的消息集合就不那么重要了。這個時候,更重要的是主題分區(qū)中的消息必須是有序的。
想象一下這樣的場景:你有一個實(shí)體(比如客戶),這個實(shí)體可能會發(fā)生許多不同的事情,比如創(chuàng)建客戶、客戶更改地址、客戶向帳戶中添加新的信用卡、客戶發(fā)起客服請求,客戶支付賬單、客戶關(guān)閉帳戶。
這些事件之間的順序很重要。例如,我們希望其他事件必須在創(chuàng)建客戶之后才能發(fā)生,并且在客戶關(guān)閉帳戶之后不能再發(fā)生其他事件。在使用Kafka時,你可以將它們?nèi)糠旁谕粋€主題分區(qū)中來保持它們的順序。在這個示例中,你可以使用客戶ID作為分區(qū)的鍵,然后將所有事件放在同一個主題中。它們必須位于同一主題中,因?yàn)椴煌闹黝}對應(yīng)不同的分區(qū),而Kafka是不保證分區(qū)之間的順序的。
順序問題
如果你為customerCreated、customerAddressChanged和customerInvoicePaid事件使用了不同的主題,那么這些主題的消費(fèi)者可能就看不到這些事件之間的順序。例如,消費(fèi)者可能會看到一個不存在的客戶做出的地址變更(這個客戶尚未創(chuàng)建,因?yàn)橄鄳?yīng)的customerCreated事件可能發(fā)生了延遲)。
如果消費(fèi)者暫停一段時間(比如進(jìn)行維護(hù)或部署新版本),那么事件出現(xiàn)亂序的可能性就更高了。在消費(fèi)者停止期間,事件繼續(xù)發(fā)布,并且這些事件被存儲在特定定的主題分區(qū)中。當(dāng)消費(fèi)者再次啟動時,它會消費(fèi)所有積壓在分區(qū)中的事件。如果消費(fèi)者只消費(fèi)一個分區(qū),那就沒問題:積壓的事件會按照它們存儲的順序依次被處理。但是,如果消費(fèi)者同時消費(fèi)幾個主題,就會按任意順序讀取主題中數(shù)據(jù)。它可以先讀取積壓在一個主題上的所有數(shù)據(jù),然后再讀取另一個主題上積壓的數(shù)據(jù),或者交錯地讀取多個主題的數(shù)據(jù)。
因此,如果你將customerCreated、customerAddressChanged和customerInvoicePaid事件放在三個單獨(dú)的主題中,那么消費(fèi)者可能會在看到customerCreated事件之前先看到customerAddressChanged事件。因此,消費(fèi)者很可能會看到一個客戶的customerAddressChanged事件,但這個客戶卻未被創(chuàng)建。
你可能會想到為每條消息附加時間戳,并用它來對事件進(jìn)行排序。如果你將事件導(dǎo)入數(shù)據(jù)倉庫,再對事件進(jìn)行排序,或許是沒有問題的。但在流數(shù)據(jù)中只使用時間戳是不夠的:在你收到一個具有特定時間戳的事件時,你不知道是否需要等待具有較早時間戳的事件,或者所有之前的事件是否已經(jīng)在當(dāng)前事情之前到達(dá)。依靠時鐘進(jìn)行同步通常會導(dǎo)致噩夢,有關(guān)時鐘問題的更多詳細(xì)信息,請參閱“Designing Data-Intensive Applications”的第8章。
何時拆分主題,何時合并主題?
基于這個背景,我將給出一些經(jīng)驗(yàn)之談,幫你確定哪些數(shù)據(jù)應(yīng)該放在同一主題中,以及哪些數(shù)據(jù)應(yīng)該放在不同的主題中。
如果你使用事件溯源進(jìn)行數(shù)據(jù)建模,事件的排序尤為重要。聚合對象的狀態(tài)是通過以特定的順序重放事件日志而得出的。因此,即使可能存在不同的事件類型,聚合所需要的所有事件也必須在同一主題中。
另外,這也取決于事件的吞吐量:如果一個實(shí)體類型的事件吞吐量比其他實(shí)體要高很多,那么最好將它分成幾個主題,以免讓只想消費(fèi)低吞吐量實(shí)體的消費(fèi)者不堪重負(fù)(參見第4點(diǎn))。不過,可以將多個具有低吞吐量的實(shí)體合并起來。
我建議在一開始將這些事件記錄為單個原子消息,而不是將其分成幾個屬于不同主題的消息。在記錄事件時,最好可以保持原封不動,即盡可能保持?jǐn)?shù)據(jù)的原始形式。你可以隨后使用流式處理器來拆分復(fù)合事件,但如果過早進(jìn)行拆分,想要重建原始事件會難得多。如果能夠?yàn)槌跏际录峙湟粋€唯一ID(例如UUID)就更好了,之后如果你要拆分原始事件,可以帶上這個ID,從而可以追溯到每個事件的起源。
如果將細(xì)粒度的主題合并成粗粒度的主題,一些消費(fèi)者可能會收到他們不需要的事件,需要將其忽略。這不是什么大問題:消費(fèi)消息的成本非常低,即使最終忽略了一大半的事件,總的成本可能也不會很大。只有當(dāng)消費(fèi)者需要忽略絕大多數(shù)消息(例如99.9%是不需要的)時,我才建議將大容量事件流拆分成小容量事件流。
最后,如果基于上述的規(guī)則依然無法做出正確的判斷,該怎么辦?那么就按照類型對事件進(jìn)行分組,把相同類型的事件放在同一個主題中。不過,我認(rèn)為這條規(guī)則是最不重要的。
模式管理
如果你的數(shù)據(jù)是普通文本(如JSON),而且沒有使用靜態(tài)的模式,那么就可以輕松地將不同類型的事件放在同一個主題中。但是,如果你使用了模式編碼(如Avro),那么在單個主題中保存多種類型的事件則需要考慮更多的事情。
如上所述,基于Avro的Kafka Confluent Schema Registry假設(shè)了一個前提,即每個主題都有一個模式(更確切地說,一個模式用于消息的鍵,一個模式用于消息的值)。你可以注冊新版本的模式,注冊表會檢查模式是否向前和向后兼容。這樣設(shè)計的一個好處是,你可以讓不同的生產(chǎn)者和消費(fèi)者同時使用不同版本的模式,并且仍然保持彼此的兼容性。
Confluent的Avro序列化器通過subject名稱在注冊表中注冊模式。默認(rèn)情況下,消息鍵的subject為<topic>-key,消息值的subject為<topic>-value。模式注冊表會檢查在特定subject下注冊的所有模式的相互兼容性。
最近,我為Avro序列化器提供了一個補(bǔ)丁(https://github.com/confluentinc/schema-registry/pull/680),讓兼容性檢查變得更加靈活。這個補(bǔ)丁添加了兩個新的配置選項(xiàng):key.subject.name.strategy(用于定義如何構(gòu)造消息鍵的subject名稱)和value.subject.name.strategy(用于定義如何構(gòu)造消息值的subject名稱)。它們的值可以是如下幾個:
- io.confluent.kafka.serializers.subject.TopicNameStrategy(默認(rèn)):消息鍵的subject名稱為<topic>-key,消息值為<topic>-value。這意味著主題中所有消息的模式必須相互兼容。
- io.confluent.kafka.serializers.subject.RecordNameStrategy:subject名稱是Avro記錄類型的完全限定名。因此,模式注冊表會檢查特定記錄類型的兼容性,而不管是哪個主題。這個設(shè)置允許同一主題包含不同類型的事件。
- io.confluent.kafka.serializers.subject.TopicRecordNameStrategy:subject名稱是<topic>-<type>,其中<topic>是Kafka主題名,<type>是Avro記錄類型的完全限定名。這個設(shè)置允許同一主題包含不同類型的事件,并進(jìn)一步對當(dāng)前主題進(jìn)行兼容性檢查。
有了這個新特性,你就可以輕松地將屬于特定實(shí)體的所有不同類型的事件放在同一個主題中。現(xiàn)在,你可以自由選擇主題的粒度,而不僅限于一個類型對應(yīng)一個主題。
英文原文:http://martin.kleppmann.com/2018/01/18/event-types-in-kafka-topic.html
總結(jié)
以上是生活随笔為你收集整理的Kafka实践:到底该不该把不同类型的消息放在同一个主题中的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 机器学习算法Python实现:gensi
- 下一篇: ElasticSearch学习资料