優步是如何用Kafka構建可靠的重試處理保證資料不丟失

banq發表於2018-03-18
分散式系統中,重試是不可避免的,我們經常使用後臺跑定時進行資料同步,同步不成功就實現重試,重試次數多少取決於你追求一致性還是可用性,如果希望兩個系統之前無論如何都必須一致,那麼你設定重試次數為無限,當然這是理想情況,實際情況是有重試次數限制和重試時間限制,如果超過不成功怎麼辦?丟棄會造成資料丟失進而永久不一致,人工介入又非常複雜,透過引入死信佇列可以優雅處理這種問題。本文是優步Uber工程師夏寧(Ning Xia)釋出的一篇如何使用Kafka的死信佇列實現重試處理的。

從網路錯誤到複製問題甚至下游依賴關係等場景中隨時可能發生的中斷,大規模執行的服務必須儘可能地優雅發現、識別並處理故障。

考慮到優步Uber的運維範圍和效率,我們的系統發生故障時必須智慧化地具有容錯性和不妥協性。為了實現這一目標,我們決定使用開源分散式訊息傳遞平臺Apache Kafka,該平臺已經過業界測試,並能提供大規模的高效能。

利用這些屬性,優步行車保險工程團隊透過擴充套件卡夫卡,在我們現有的事件驅動架構中使用無阻塞請求重新處理和死信佇列(DLQ),實現錯誤處理的解耦,在不中斷實時流量情況下實現可觀察的錯誤處理。這一策略有助於我們遍佈200多個城市的駕駛員能夠可靠地實現每次行程的保費扣除。

在本文中,我們重點介紹了使用實時SLA重新處理大型系統中的請求並分享經驗教訓的方法。


在事件驅動的體系結構中工作
優步的駕駛員損傷保護系統的後端位於Kafka訊息傳遞架構中,該架構貫穿優步大型微服務生態系統內的多個依賴關係的Java服務。本文我們更專注於我們的重試和死信的策略,並透過一個總的應用程式來管理不同產品的預訂,以實現蓬勃發展的線上業務。

在這個模型中,我們希望提供以下功能:
a)能進行支付
b)為每個使用者的每個產品的預購訂單建立單獨的報表記錄,以生成實時產品分析。


每個功能都可以透過其各自服務的API提供。根據功能要求,設計了兩個服務(消費組),一個是支付消費組完成a功能,一個是報表消費組完成b功能,這兩個消費組都預訂了相同的預訂事件頻道(也就是訂閱了Kafka主題PreOrder):


當系統收到預訂請求時,商店服務釋出包含相關請求資料的PreOrder訊息。兩個消費組都會監聽這個PreOrder訊息,從而執行自己的業務邏輯並呼叫其相應的服務。

實施重試的簡單快速的解決方案是在客戶端呼叫呼叫時使用定時迴圈重試。例如,如果支付服務正在發生延遲等待並開始丟擲超時異常,則商店服務將繼續在指定重試次數下進行重試以完成支付),直到它成功或達到另一個停止條件為止。


簡單的重試問題
雖然在客戶端層次進行定時迴圈的重試可能很有用,但大量大規模的系統重試仍可能會受到以下因素影響:

1.阻止批處理。當我們需要實時處理大量訊息時,反覆重試產生的失敗訊息可能會阻塞正常的批處理。最嚴重的情況是超過重試時間限制,這意味會花最長時間,使用的資源會最多。如果沒有成功的回應,卡夫卡消費者將會不斷提交.

2.難以檢索後設資料。在重試上獲取後設資料會很麻煩,比如時間戳和第n次重試。

如果下游支付服務出現重大變化,例如,對於之前是有效的預購訂單卻遭遇收費策略調整導致拒絕接受,那麼這些訊息的所有重試都會無效。接收到該特定訊息的消費者不會提交該訊息的Kafka指標(偏移量),這意味著該訊息將被一次又一次消費,代價是導致到達該通道的大量新訊息被迫處於等待而無法被正常讀取。

如果請求在重試後繼續重試失敗,我們希望在DLQ中收集這些故障以進行可視性檢視和診斷。DLQ應允許以列表方式檢視佇列的內容,清除管理這些內容,併合並重新處理死信訊息,允許全面解決所有受共享問題影響的故障。在優步,我們需要一個可靠並且可擴充套件地為我們提供這些功能的重試策略。



在單獨的佇列中處理
為了解決批處理被重試處理阻塞的問題,我們使用單獨定義的Kafka主題,專門為重試設計單獨佇列。在這種情況下,當消費者處理程式在指定的訊息重試次數之後會返回特定訊息的失敗響應,消費者將該訊息釋出到相應的重試主題中。該處理程式然後將true 返回給原始使用者,該使用者會確認提交了它的Kafka偏移量,從而保證Kafka訊息能夠持續向下讀取。

在這種型別的系統中重試請求非常簡單。與主處理流程一樣,單獨一組消費者將讀取重試佇列。這些消費者的行為與原始架構中的消費者行為類似,只是消費者使用不同的卡夫卡話題。同時,執行多次重試是透過建立多個主題來完成的,其中每一組不同的監聽器(也就是消費組)訂閱每個重試主題。當特定主題的處理程式返回給定訊息的錯誤響應時,它會將該訊息釋出到它下面的下一個重試主題。

最後,在此設計中,DLQ被定義為最終的卡夫卡主題。如果最後一次重試主題的消費者仍然沒有成功,那麼它會將該訊息釋出到死信主題。在那裡,可以使用許多技術來以主題方式進行資料列表,清除和合並,

重要的是不要一個接一個地立即重新嘗試失敗的請求; 這樣做會放大呼叫的數量,實質上是等同於垃圾郵件的惡意請求。相反,每個後續級別的重試使用者都可以執行處理延遲,換句話說,隨著訊息在每個重試主題中逐步下降,超時會增加。此機制遵循漏桶模式。因此,這種重試佇列其實是延遲處理佇列。



我們透過基於佇列的重新處理獲得了什麼
現在,我們討論這種方法的好處,因為它涉及確保可靠和可擴充套件的重新處理:

1.不會都是正常批處理
失敗的訊息輸入他們自己的指定通道,使正在進行批處理能夠成功繼續進行,而不是要求在出現故障時重新處理它們。因此,傳入請求的消耗向前暢通無阻,實現更高的實時吞吐量。

2.解耦
獨立工作流在同一個事件上執行,每個工作流都有自己的消費者流程,重試有單獨的再處理和死信佇列。一個佇列中處理失敗並不需要重試那些已經成功的其他訊息。

3.可配置
建立新主題實際上不會產生開銷,並且這些主題產生的訊息可以遵循相同的架構。原始處理以及每個重試通道都可以分別在易於編寫的較高階別的消費者級別下進行管理,該級別由配置進行管理。

我們還可以區分不同型別錯誤的處理方式,允許重新嘗試網路脆弱等情況,而空指標異常和其他程式碼錯誤應該直接進入DLQ,因為重試不會修復它們。

4.觀測
將訊息處理分割成不同的主題有助於容易地跟蹤錯誤訊息的路徑,重試訊息的時間和次數以及其有效負載的確切屬性。將生產率與再處理主題和DLQ的生產率相比較,可以為自動警報提供閾值並跟蹤實時服務正常執行時間。

5.靈活性
雖然Kafka本身是用Scala和Java編寫的,但Kafka支援多種語言的客戶端庫。例如,優步的許多服務都使用Go作為他們的Kafka客戶端。

使用像Avro這樣的序列化框架的Kafka訊息格式支援可演化的模式。如果我們的資料模型如果需要更新,則只需要最小的調整來反映這一變化。

6.效能和可靠性
Kafka預設提供至少一次的語義。這種耐久性保證在容錯和訊息失敗的情況下非常有價值; 當談到提供關鍵業務資料時(如Uber的情況),訊息無損(訊息不丟失)是最重要的。而且,Kafka的並行模型和基於拉的系統可實現高吞吐量和低延遲。


其他考慮
由於Kafka只能保證分割槽內的順序處理,而跨分割槽接受無法保證順序,因此應用程式必須能夠處理事件發生的確切順序以外的事件。此外,至少一次訊息傳遞需要消費者依賴性冪等性,這是任何分散式系統的共同特徵。

前面闡述了死信佇列提供的顯著優勢,但真正實施可能因用例而異。例如,根據指定的應用程式處理的資料型別的數量,每個主題代表不同的事件型別,這可能導致需要管理大量主題。在這種情況下,基於計數佇列的替代方案可能是比較好的選擇,將事件型別與其他欄位一起打包,從而以更易於管理的方式跟蹤重試次數和時間戳。這種權衡還需要重新考慮如何執行排程,因為這是透過一系列佇列階梯進行管理的。


使用基於計數的卡夫卡主題可實現死信佇列,進行重試的單獨的重新處理執行,使我們能夠在基於事件的系統中重試請求,而不會阻止實時流量。在此框架內,工程師可以根據需要配置,擴充套件,更新和監控訊息傳遞,但不會對開發人員時間或應用程式正常執行時間造成任何損失。


Building Reliable Reprocessing and Dead Letter Que

相關文章