Apache kafka是如何實現消息的精確一次(Exactly-once-semantics)語義的?

       原文鏈接:https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/

       精確一次消息語義(Exactly-once semantics)是可以實現的:讓我們看看Kafka是怎麼實現的。

       我很興奮,我們到達了Kafka社區一直以來期待的令人激動的里程碑:我們在Apache Kafka 0.11 release版本和Confluent Platform 3.3中引入了精確一次消息語義。在這篇文章中,我會告訴你Apache Kafka中的精確一次語義是什麼意思,爲什麼這是一個難以實現的問題,還有Kafka中的冪等(idempotence)和事務(transactions)新特性是如何保證使用Kafka Stream API來正確地進行精確一次流處理的(exactly-once stream processing)。

精確一次確實很難實現(Exactly-once is a really hard problem)

       我知道你們中的一些人在想什麼。你們可能會認爲精確一次投遞(exactly-once delivery)是不可能的, 它的代價太高而無法在實際中使用,或者認爲我完全錯了! 不僅僅只有你這麼想。 我的一些業內同事承認,精確一次性交付是分佈式系統中最難解決的問題之一。
       Mathias Verraes說,分佈式系統中最難解決的兩個問題是:1.消息順序保證(Guaranteed order of messages)。2.消息的精確一次投遞(Exactly-once delivery)。
       還有一些人直接坦白地說,精確一次投遞根本不可能實現。
       我並不否認引入一次性交付語義,並且只支持一次流處理,是一個真正難以解決的問題。 但我也見證了Confluent公司的機智的分佈式系統工程師在開源社區努力工作了一年多,以便在Apache Kafka中解決這個問題。 因此,讓我們直奔主題,先來了解消息傳遞語義的概述。

消息系統語義概述(Overview of messaging system semantics)

       在一個分佈式發佈訂閱消息系統中,組成系統的計算機總會由於各自的故障而不能工作。在Kafka中,一個單獨的broker,可能會在生產者發送消息到一個topic的時候宕機,或者出現網絡故障,從而導致生產者發送消息失敗。根據生產者如何處理這樣的失敗,產生了不通的語義:

  • 至少一次語義(At least once semantics):如果生產者收到了Kafka broker的確認(acknowledgement,ack),並且生產者的acks配置項設置爲all(或-1),這就意味着消息已經被精確一次寫入Kafka topic了。然而,如果生產者接收ack超時或者收到了錯誤,它就會認爲消息沒有寫入Kafka topic而嘗試重新發送消息。如果broker恰好在消息已經成功寫入Kafka topic後,發送ack前,出了故障,生產者的重試機制就會導致這條消息被寫入Kafka兩次,從而導致同樣的消息會被消費者消費不止一次。每個人都喜歡一個興高采烈的給予者,但是這種方式會導致重複的工作和錯誤的結果。
  • 至多一次語義(At most once semantics):如果生產者在ack超時或者返回錯誤的時候不重試發送消息,那麼消息有可能最終並沒有寫入Kafka topic中,因此也就不會被消費者消費到。但是爲了避免重複處理的可能性,我們接受有些消息可能被遺漏處理。
  • 精確一次語義(Exactly once semantics):即使生產者重試發送消息,也只會讓消息被髮送給消費者一次。精確一次語義是最令人滿意的保證,但也是最難理解的。因爲它需要消息系統本身和生產消息的應用程序還有消費消息的應用程序一起合作。比如,在成功消費一條消息後,你又把消費的offset重置到之前的某個offset位置,那麼你將收到從那個offset到最新的offset之間的所有消息。這解釋了爲什麼消息系統和客戶端程序必須合作來保證精確一次語義。

必須被處理的故障(Failures that must be handled)

       爲了描述爲了支持精確一次消息投遞語義而引入的挑戰,讓我們從一個簡單的例子開始。
       假設有一個單進程生產者程序,發送了消息“Hello Kafka“給一個叫做“EoS“的單分區Kafka topic。然後有一個單實例的消費者程序在另一端從topic中拉取消息,然後打印。在沒有故障的理想情況下,這能很好的工作,“Hello Kafka“只被寫入到EoS topic一次。消費者拉取消息,處理消息,提交偏移量來說明它完成了處理。然後,即使消費者程序出故障重啓也不會再收到“Hello Kafka“這條消息了。
       然而,我們知道,我們不能總認爲一切都是順利的。在上規模的集羣中,即使最不可能發生的故障場景都可能最終發生。比如:

  1. broker可能故障:Kafka是一個高可用、持久化的系統,每一條寫入一個分區的消息都會被持久化並且多副本備份(假設有n個副本)。所以,Kafka可以容忍n-1哥broker故障,意味着一個分區只要至少有一個broker可用,分區就可用。Kafka的副本協議保證了只要消息被成功寫入了主副本,它就會被複制到其他所有的可用副本(ISR)。
  2. producer到broker的RPC調用可能失敗:Kafka的持久性依賴於生產者接收broker的ack。沒有接收成功ack不代表生產請求本身失敗了。broker可能在寫入消息後,發送ack給生產者的時候掛了。甚至broker也可能在寫入消息前就掛了。由於生產者沒有辦法知道錯誤是什麼造成的,所以它就只能認爲消息沒寫入成功,並且會重試發送。在一些情況下,這會造成同樣的消息在Kafka分區日誌中重複,進而造成消費端多次收到這條消息。
  3. 客戶端可能會故障:精確一次交付也必須考慮客戶端故障。但是我們如何知道一個客戶端已經故障而不是暫時和brokers斷開,或者經歷一個程序短暫的暫停?區分永久性故障和臨時故障是很重要的,爲了正確性,broker應該丟棄僵住的生產這發送來的消息,同樣,也應該不向已經僵住的消費者發送消息。一旦一個新的客戶端實例啓動,它應該能夠從失敗的實例留下的任何狀態中恢復,從一個安全點開始處理。這意味着,消費的偏移量必須始終與生產的輸出保持同步。

Apache Kafka中的精確一次語義(Exactly-once semantics in Apache Kafka, explained)

       在0.11版本之前,Apache Kafka支持最少一次交付語義,和分區內有序交付。從上面的例子可以知道,生產者重試可能會造成重複消息。在新的精確一次語義特性中,我們以三個不同且相互關聯的方式加強了Kafka軟件的處理語義。

冪等性:每個分區中精確一次且有序(Idempotence: Exactly-once in order semantics per partition)

       一個冪等性的操作就是一種被執行多次造成的影響和只執行一次造成的影響一樣的操作。現在生產者發送的操作是冪等的了。如果出現導致生產者重試的錯誤,同樣的消息,仍由同樣的生產者發送多次,將只被寫到kafka broker的日誌中一次。對於單個分區,冪等生產者不會因爲生產者或broker故障而發送多條重複消息。想要開啓這個特性,獲得每個分區內的精確一次語義,也就是說沒有重複,沒有丟失,並且有序的語義,只需要設置producer配置中的”enable.idempotence=true”。
       這個特性是怎麼實現的呢?在底層,它和TCP的工作原理有點像,每一批發送到Kafka的消息都將包含一個序列號,broker將使用這個序列號來刪除重複的發送。和只能在瞬態內存中的連接中保證不重複的TCP不同,這個序列號被持久化到副本日誌,所以,即使分區的leader掛了,其他的broker接管了leader,新leader仍可以判斷重新發送的是否重複了。這種機制的開銷非常低:每批消息只有幾個額外的字段。你將在這篇文章的後面看到,這種特性比非冪等的生產者只增加了可忽略的性能開銷。

事務:跨分區原子寫入(Transactions: Atomic writes across multiple partitions)

       Kafka現在通過新的事務API支持跨分區原子寫入。這將允許一個生產者發送一批到不同分區的消息,這些消息要麼全部對任何一個消費者可見,要麼對任何一個消費者都不可見。這個特性也允許你在一個事務中處理消費數據和提交消費偏移量,從而實現端到端的精確一次語義。下面是的代碼片段演示了事務API的使用:

producer.initTransactions();
try {
  producer.beginTransaction();
  producer.send(record1);
  producer.send(record2);
  producer.commitTransaction();
} catch(ProducerFencedException e) {
  producer.close();
} catch(KafkaException e) {
  producer.abortTransaction();
}

       上面的代碼片段演示了你可以如何使用新生產者API來原子性地發送消息到topic的多個partition。值得注意的是,一個Kafka topic的分區中的消息,可以有些是在事務中,有些不在事務中。
       因此在消費者方面,你有兩種選擇來讀取事務性消息,通過隔離等級“isolation.level”消費者配置表示:

  1. read_commited:除了讀取不屬於事務的消息之外,還可以讀取事務提交後的消息。
  2. read_uncommited:按照偏移位置讀取所有消息,而不用等事務提交。這個選項類似Kafka消費者的當前語義。

       爲了使用事務,需要配置消費者使用正確的隔離等級,使用新版生產者,並且將生產者的“transactional.id”配置項設置爲某個唯一ID。 需要此唯一ID來提供跨越應用程序重新啓動的事務狀態的連續性。

真實案例:Apache Kafka中的精確一次流處理(The real deal: Exactly-once stream processing in Apache Kafka)

       構建於冪等性和原子性之上,精確一次流處理現在可以通過Apache Kafka的流處理API實現了。使Streams應用程序使用精確一次語義所需要的就是設置配置“processing.guarantee = exact_once”。 這可以保證所有處理恰好發生一次; 包括處理和由寫回Kafka的處理作業創建的所有具體狀態的精確一次。
       這就是爲什麼Kafka的Streams API提供的精確一次性保證是迄今爲止任何流處理系統提供的最強保證。 它爲流處理應用程序提供端到端的一次性保證,從Kafka讀取的數據,Streams應用程序物化到Kafka的任何狀態,到寫回Kafka的最終輸出。 僅依靠外部數據系統來實現狀態支持的流處理系統對於精確一次的流處理提供了較少的保證。 即使他們使用Kafka作爲流處理的源並需要從失敗中恢復,他們也只能倒回他們的Kafka偏移量來重建和重新處理消息,但是不能回滾外部系統中的關聯狀態,導致狀態不正確,更新不是冪等的。
       讓我再詳細解釋一下。 流處理系統的關鍵問題是“我的流處理應用程序是否得到了正確的答案,即使其中一個實例在處理過程中崩潰了?”。在恢復失敗的實例時,恢復到崩潰前相同的狀態進行處理是很關鍵的。
       現在,流處理只不過是對Kafka topic的讀取-處理-寫入操作; 消費者從Kafka topic讀取消息,一些處理邏輯轉換這些消息或修改由處理程序維護的狀態,然後生產者將結果消息寫入另一個Kafka topic。精確一次的流處理只是一種保證僅執行一次讀-處理-寫操作的能力。在這種情況下,“獲得正確答案”意味着不會丟失任何輸入消息或產生任何重複輸出。 這是用戶期望從精確一次性流處理器中獲得的行爲。
除了我們到目前爲止討論的簡單場景之外,還有許多其他失敗場景需要考慮:

  • 流處理器可能從多個源topic獲取輸入,並且跨多個源topic的消息順序在多次運行中是不確定的。 因此,如果重新運行從多個源topic獲取輸入的流處理器,可能會產生不同的結果。
  • 同樣,流處理器可以生成到多個目標topic的輸出。 如果生產者無法跨多個topic進行原子寫入,那麼如果對某些(但不是所有)分區的寫入失敗,則生產者輸出可能不正確。
  • 流處理器可以使用Streams API提供的狀態管理工具在多個輸入之間聚合或連接數據。 如果流處理器的其中一個實例失敗,那麼你需要能夠回滾由流處理器的該實例保存的狀態。 在重新啓動實例時,你還需要能夠恢復處理並重新創建其狀態。
  • 流處理器可能會查找外部數據庫中濃縮的信息,或者通過調用外面的服務來查找信息。 依賴外部服務使流處理器從根本上不確定; 如果外部服務在兩次運行流處理器之間更改其內部狀態,則會導致下游的結果不正確。 但是,如果處理得當,這不應導致完全錯誤的結果。 它應該只導致流處理器輸出屬於一組合法輸出。 稍後將在博客中詳細介紹。

       應用失敗和重新啓動,尤其是與非確定性操作相結合時,並且應用程序計算的持久狀態的更改時,可能不僅會導致重複,還可能會導致錯誤的結果。 例如,如果一個處理階段正在計算所看到的事件數量,那麼上游處理階段中的重複可能導致下游的錯誤計數。 因此,我們必須限定短語“精確一次流處理。”它指的是從topic消費,生成中間狀態到Kafka topic中,並把結果寫到另一個Kafka topic中,而不是使用Streams API對消息進行的所有可能的計算。某些計算(例如,取決於外部服務或從多個源topic消費)從根本上是不確定的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章