微服務(wù)架構(gòu)中的挑戰(zhàn)及應(yīng)對(duì)方式:Outbox 模式
使用 Outbox 模式保持微服務(wù)數(shù)據(jù)一致性
在一個(gè)由許多小型服務(wù)組成的系統(tǒng)中保持?jǐn)?shù)據(jù)一致性是困難的,因?yàn)樗鼈兎稚⒃诟魈帯R韵率且恍┏R妴栴}以及如何處理它們的方法:當(dāng)服務(wù)發(fā)送消息時(shí),同時(shí)更新數(shù)據(jù)庫和發(fā)送消息是棘手的問題。
在微服務(wù)中發(fā)出事件時(shí),我們必須解決如何以事務(wù)方式更新數(shù)據(jù)庫并發(fā)出事件的問題。
Outbox 模式
處理這個(gè)問題的簡單方法是使用事務(wù)性 Outbox 模式。
問題:雙寫問題
當(dāng)我們必須同時(shí)更新兩個(gè)不同的系統(tǒng)時(shí),就會(huì)出現(xiàn)雙寫問題。例如,如果我們需要在 Apache Kafka 和數(shù)據(jù)庫中記錄事件。由于這些系統(tǒng)沒有連接,我們無法一次性更新它們。我們必須找到一種方法來確保兩者同時(shí)更新,或者兩者都不更新。這就是事務(wù)性 Outbox 模式發(fā)揮作用的地方。
如果我們的數(shù)據(jù)庫支持事務(wù)性更新,我們可以使用它來解決雙寫問題。我們將事務(wù)邏輯移到數(shù)據(jù)庫中,而不是嘗試同時(shí)更新數(shù)據(jù)庫和 Kafka。每當(dāng)我們更新數(shù)據(jù)庫時(shí),我們也在同一個(gè)事務(wù)中更新一個(gè) Outbox 表。可以將 Outbox 想象成一個(gè)郵箱,我們將需要發(fā)送的信件放在其中。然后,我們等待郵遞員收集這些信件并將其送到郵局。在我們的情況下,這些信件代表我們想要發(fā)送到 Kafka 的事件,而 Kafka 則充當(dāng)郵局。但是,我們?nèi)匀恍枰撤N東西扮演郵遞員的角色。
要從 Outbox 表中發(fā)出事件到 Apache Kafka,我們可以使用一個(gè)單獨(dú)的進(jìn)程來異步監(jiān)視該表。每當(dāng)它檢測(cè)到事件時(shí),就可以將其發(fā)送到 Kafka。
雙寫問題
一旦事件成功傳遞,它就可以從 Outbox 表中刪除。該進(jìn)程通常是在原始微服務(wù)中的另一個(gè)線程中編寫的;但是,它也可以作為完全獨(dú)立的應(yīng)用程序運(yùn)行。根據(jù)您使用的數(shù)據(jù)庫,您可能可以使用 Kafka 連接器(例如 Postgres-Kafka 連接器)或更改數(shù)據(jù)捕獲(CDC)系統(tǒng)(例如 Debezium)來監(jiān)視表并發(fā)送事件。
Kafka 連接器或更改數(shù)據(jù)捕獲(CDC)
使用 CDC 解決雙寫問題的優(yōu)勢(shì)
Outbox 事務(wù)模式避免了雙寫問題。原因是狀態(tài)和 Outbox 表將始終以事務(wù)方式更新。如果由于某種原因狀態(tài)未能更新,則事件不會(huì)寫入 Outbox;這意味著我們可以保證 Outbox 中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)完全同步。然后,負(fù)責(zé)將事件傳遞到 Kafka 的獨(dú)立進(jìn)程確保 Outbox 表和 Kafka 保持同步。這使我們能夠保證每個(gè)數(shù)據(jù)庫操作都會(huì)在 Kafka 中有一個(gè)相應(yīng)的事件,盡管會(huì)有一點(diǎn)延遲。
缺點(diǎn):當(dāng)傳遞過程向 Kafka 發(fā)出事件時(shí),可能會(huì)出現(xiàn)失敗或超時(shí)。在這種情況下,為了確保 Kafka 收到數(shù)據(jù),我們必須重試。這些重試可能導(dǎo)致重復(fù)的消息;因此,我們向 Kafka 的傳遞保證是至少一次的。我們保證 Outbox 中的每條消息最終都會(huì)到達(dá) Kafka,但可能會(huì)重復(fù)到達(dá)。因此,我們需要確保下游系統(tǒng)準(zhǔn)備好處理任何重復(fù)消息。
在分布式系統(tǒng)中,至少一次的保證是常見的,因此,即使不涉及雙寫問題,實(shí)現(xiàn)去重邏輯也是一個(gè)良好的做法。例如,接收方在處理 Kafka 消息時(shí)可能會(huì)失敗,并且當(dāng)它重新啟動(dòng)時(shí)可能會(huì)再次收到相同的消息。
Outbox 模式中的挑戰(zhàn)
我們必須準(zhǔn)備好處理這些情況。這可能會(huì)導(dǎo)致大量的流量。頻繁的更新意味著數(shù)據(jù)庫可能會(huì)始終將表保存在內(nèi)存中,占用大量資源。與此同時(shí),一些數(shù)據(jù)庫在處理刪除時(shí)效率不高。它們可能在幕后使用墓碑,并且隨著頻繁的插入和刪除發(fā)生,這些墓碑可能會(huì)累積,這會(huì)導(dǎo)致資源使用量大增,并在我們的表中引起爭(zhēng)用。如果數(shù)據(jù)庫無法處理此類流量,可能會(huì)減慢我們的應(yīng)用程序,因?yàn)檎?qǐng)記住,每個(gè)寫入都將觸及該 Outbox 表。為了解決這些問題,我們可能需要進(jìn)行調(diào)整,例如將記錄而不是刪除它們,或者調(diào)整數(shù)據(jù)庫管理墓碑的方式。保留事件可能會(huì)帶來長期的好處,因此刪除可能并非絕對(duì)必要。一些數(shù)據(jù)庫專門設(shè)計(jì)用于處理這種類型的流量。
結(jié)論
如果您的系統(tǒng)滿足事務(wù)性 Outbox 模式的要求,那么它可以是解決雙寫問題的一種簡單有效的方法。與其他選項(xiàng)(例如事件溯源或監(jiān)聽自己模式)相比,這種方法采用事件優(yōu)先的方法,使用 Kafka 實(shí)時(shí)通知微服務(wù)變更,保持系統(tǒng)一致性。但是,諸如訂單履行之類的組件可能需要編排,無法運(yùn)行。