微服務下分佈式事務模式的詳細對比

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文最初發表於 "},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/articles\/2021\/09\/21\/distributed-transaction-patterns-microservices-compared","title":"","type":null},"content":[{"type":"text","text":"RedHat 博客網站"}]},{"type":"text","text":",經原作者 Bilgin Ibryam 授權由 InfoQ 中文站翻譯分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲 Red Hat 諮詢架構師,我有幸參與了大量客戶項目。雖然每個客戶都面臨自己特有的挑戰,但是我發現其中有一些共同點。大多數項目都想知道如何協調對多個記錄系統的寫入。要回答這個問題,一般會涉及長篇累牘的解釋,包括雙重寫入(dual write)、分佈式事務、現代化的替代方案以及每種方式可能出現的故障情況和缺點。這樣做通常會讓客戶意識到,"},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/articles\/2021\/06\/14\/application-modernization-patterns-apache-kafka-debezium-and-kubernetes#","title":"","type":null},"content":[{"type":"text","text":"將單體應用拆分爲微服務架構"}]},{"type":"text","text":"是一個漫長和複雜的過程,而且通常都需要權衡。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文不會深入介紹事務的細節,而是總結了向多個數據源協調寫入操作的主要方式和模式。我知道,你可能對這些方法有過美好或糟糕的經驗。但是實踐中,在正確的環境和正確的限制條件下,這些方法都能很好地工作。技術領導者要爲自己的環境選擇最好的方式。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"雙重寫入的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於你是否會面臨雙重寫入的問題有一個簡單的指標,那就是預期要不要向多個記錄系統進行寫入操作。這樣的需求可能並不明顯,在分佈式系統設計的過程中,它可能會以不同的方式進行表述。比如說:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你已經爲每項工作選擇了最佳工具,現在在一個業務事務中,你必須要更新一個NoSQL數據庫、一個搜索索引和一個緩存。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你所設計的服務必須要更新自己的數據庫,同時還要把變更相關的信息以通知的形式發送給另一個服務。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你的業務事務跨越了多個服務的邊界。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能需要以冪等的方式實現服務操作,因爲服務的消費者必須要重試失敗的調用。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在本文中,我們將會使用一個很簡單的示例場景來評估在分佈式事務中處理雙重寫入的各種方法。我們的場景是一個客戶端應用,它會在發生變更操作的時候,調用一個微服務。服務A要更新自己的數據庫,但是它還要調用服務B進行寫入操作,如圖1所示。至於數據庫的實際類型以及服務與服務之間進行交互的協議,這些對於我們的討論都無關緊要,因爲問題都是一樣的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/46\/46ede9bf20e2d74aa18bc6677d7603f2.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"微服務中的雙重寫入問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們簡要解釋一下爲什麼這個問題沒有簡單的解決方案。如果服務A寫入到了自己的數據庫,然後發送一個通知到隊列中供服務B使用(我們將這種方式稱爲"},{"type":"text","marks":[{"type":"italic"}],"text":"local-commit-then-publish"},{"type":"text","text":"),這樣應用依然有可能無法可靠地運行。當服務A寫入到自己的數據庫,然後發送消息到隊列時,依然有很小的概率發生這樣的事情,即應用在提交到數據庫後,且在第二個操作之前,發生了崩潰,這樣的話,就會使系統處於一個不一致的狀態。如果消息在寫入到數據庫之前發送的話(我們將這種方式稱爲"},{"type":"text","marks":[{"type":"italic"}],"text":"publish-then-local-commit"},{"type":"text","text":"),有可能出現數據庫寫入失敗,或者服務B接收到事件的時候,服務A還沒有提交到數據庫,這會出現時效性問題。不管是出現哪種情況,這種場景都會涉及到對數據庫和隊列的雙重寫入問題,這就是我們要探討的核心問題。在下面的章節中,我們將會討論針對這一長期存在的挑戰目前已有的各種解決方案。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"模塊化單體"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將應用程序開發爲模塊化單體看起來像一種權宜之計(hack),或是架構演化的一種倒退。但是,我發現它在實踐中能夠很好地運行。它不是一種微服務的模式,而是微服務規則的一個例外情況,能夠非常嚴謹地與微服務相結合。如果強寫入一致性是驅動性的需求,甚至要比獨立部署和擴展微服務的能力更重要時,那麼我們就可以採用模塊化單體的架構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"採用單體架構並不意味着系統設計得很差或者是件壞事。它並不說明任何質量相關的問題。顧名思義,這是一個按照模塊化方式設計的系統,它只有一個部署單元。需要注意,這是一個精心設計和實現的"},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/blog\/2016\/10\/27\/the-fast-moving-monolith-how-we-sped-up-delivery-from-every-three-months-to-every-week","title":"","type":null},"content":[{"type":"text","text":"模塊化單體"}]},{"type":"text","text":",這與隨意創建並隨時間而不斷增長的單體是不同的。在精心設計的模塊化單體架構中,每個模塊都遵循微服務的原則。每個模塊會封裝對其數據的所有訪問,但是操作是以內存方法調用的方式進行暴露和消費的。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"模塊化單體的架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果採用這種方式的話,我們必須要將兩個微服務(服務A和服務B)轉換成可以部署到共享運行時的庫模塊(library module)。然後,讓這兩個微服務共享同一個數據庫實例。因爲服務是在一個通用的運行中編寫和部署的,所以它們可以參與相同的事務。鑑於這些模塊共享同一個數據庫實例,所以我們可以使用本地事務一次性地提交或回滾所有的變更。在部署方法方面也有差異,因爲我們希望模塊以庫的方式部署到一個更大的部署單元中,並參與現有的事務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即便是在單體架構中,也有一些方式來隔離代碼和數據。例如,我們可以將模塊隔離成單獨的包、構建模塊和源碼倉庫,這些模塊可以由不同的團隊所擁有。通過將表按照命名規則、模式、數據庫實例,甚至數據庫服務器的方式進行分組,我們可以實現數據的部分隔離。圖2的靈感來源於Axel Fontaine關於"},{"type":"link","attrs":{"href":"https:\/\/www.youtube.com\/watch?v=BOvxJaklcr0","title":"","type":null},"content":[{"type":"text","text":"偉大的模塊化單體"}]},{"type":"text","text":"的演講,它闡述了應用中不同的代碼和數據隔離級別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7d\/7d17bb92cac31b31d320a0e4d9db41c1.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"應用程序的代碼和數據隔離級別"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拼圖的最後一塊是使用一個運行時和一個包裝器服務(wrapper service),該服務能夠消費其他的模塊並將其納入到現有事務的上下文中。所有的這些限制使模塊比典型的微服務耦合更緊密,但是好處在於包裝器服務能夠啓動一個事務、調用庫模塊來更新它們的數據庫,並且以一個操作的形式提交或回滾事務,而不必擔心部分失敗或最終一致性的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們的樣例中,如圖3所示,我們將服務A和服務B轉換爲庫,並將它們部署到一個共享的運行時中,或者也可以將其中的某個服務作爲共享運行時。數據庫的表也共享同一個數據庫實例,但是它會被拆分爲一組由各自的庫服務管理的表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/09\/099d7f62269ebf37e4fcbc7add32c16e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"具有共享數據庫的模塊化單體"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"模塊化單體的優點和缺點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在有些行業中,這種架構的收益遠比其他地方所看重的更快的交付以及更快的變更節奏重要得多。表1總結了模塊化單體架構的優點和缺點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e6\/e605a168e67452f11d99f1dc48c042e9.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"表1:模塊化單體架構的優點和缺點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分佈式事務通常是最後的方案,通常會在如下的情況下使用:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當對不同資源的寫入操作不允許最終一致性時;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們必須要寫入到不同種類的數據源時;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們需要確保對消息的處理有且僅有一次,而且無法重構系統以實現操作的冪等性時;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當與第三方黑盒系統或實現了兩階段提交規範的遺留系統進行集成時。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這些情況下,如果可擴展性不是重要的關注點的話,我們可以考慮將分佈式事務作爲一種可選方案。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"實現兩階段提交架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兩階段提交技術要求我們有一個分佈式事務管理器(如"},{"type":"link","attrs":{"href":"https:\/\/narayana.io\/","title":"","type":null},"content":[{"type":"text","text":"Narayana"}]},{"type":"text","text":")和一個可靠的事務日誌存儲層。我們還需要能夠兼容"},{"type":"link","attrs":{"href":"https:\/\/publications.opengroup.org\/standards\/dist-computing\/c193","title":"","type":null},"content":[{"type":"text","text":"DTP XA"}]},{"type":"text","text":"的數據源,以及能夠參與分佈式事務的相關的XA驅動,比如RDBMS、消息代理和緩存。如果你足夠幸運有合適的數據源,但是運行在一個動態環境中,比如"},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/topics\/kubernetes","title":"","type":null},"content":[{"type":"text","text":"Kubernetes"}]},{"type":"text","text":",那麼你還需要有一個像operator這樣的機制,以確保分佈式事務管理器只有一個實例。事務管理器必須是高可用的,並且必須能夠訪問事務日誌。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就實現而言,你可以嘗試使用"},{"type":"link","attrs":{"href":"https:\/\/github.com\/snowdrop\/narayana-spring-boot\/tree\/master\/openshift\/recovery-controller","title":"","type":null},"content":[{"type":"text","text":"Snowdrop Recovery Controller"}]},{"type":"text","text":",它使用"},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/blog\/2020\/05\/11\/top-10-must-know-kubernetes-design-patterns","title":"","type":null},"content":[{"type":"text","text":"Kubernetes StatefulSet模式"}]},{"type":"text","text":"來實現單例,並使用持久化捲來存儲事務日誌。在這個類別中,我還包含了適用於SOAP Web服務的"},{"type":"link","attrs":{"href":"http:\/\/docs.oasis-open.org\/ws-tx\/wstx-wsat-1.2-spec.html","title":"","type":null},"content":[{"type":"text","text":"Web Services Atomic Transaction"}]},{"type":"text","text":"(WS-AtomicTransaction)等規範。所有這些技術的共同點在於它們實現了XA規範,並且有一箇中心化的事務協調器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們的樣例中,如圖4所示,服務A使用分佈式事務提交所有的變更到自己的數據庫中,並且會提交一條消息到隊列中,這個過程中不會出現消息的重複和丟失。類似的,服務B可以使用分佈式服務來消費消息,並在同一個事務中提交至數據庫B,這個過程中也不會出現任何的重複數據。或者,服務B也可以選擇不使用分佈式事務,而是使用本地事務並實現冪等的消費者模式。在本節中,一個更合適的例子是使用WS-AtomicTransaction在一個事務中協調對數據庫A和數據庫B的寫入,並完全避免最終一致性。但是,現在這種方式已經不太常見了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5b\/5b20199a9eb5349502f36d5fb8dbba29.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"跨數據庫和消息代理的二階段提交"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"兩階段提交架構優點和缺點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兩階段提交協議所提供的保障與模塊化單體中的本地事務類似,但有些例外情況。因爲這裏有兩個或更多的獨立數據源參與到原子更新之中,所以它們可能會以不同的方式失敗並阻塞整個事務。但是,由於存在一箇中心化的協調者,相對於我下面將要討論的其他方式,我們還是能夠很容易地發現分佈式系統的狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/8d\/8d96b6c89b3f408d3b59d81e6080e307.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"表2:兩階段提交的優點和缺點"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"編排式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於模塊化單體來講,我們會使用本地事務,這樣我們始終能夠知道系統的狀態。對基於兩階段提交的分佈式事務,我們也能保證狀態的一致性。唯一的例外情況是事務協調者出現了不可恢復的故障。但是,如果我們想要減弱一致性的需求,而希望能夠了解整個分佈式系統的狀態,並且能從一個地方對其進行協調,那麼我們該怎麼處理呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種情況下,我們可以考慮採取一種編排(orchestration)的方式,在這裏,某個服務會擔任整個分佈式狀態變更的協調者和編排者。編排者服務有責任調用其他的服務,直至它們達到所需的狀態,或者在它們出現故障的時候執行糾正措施。編排者使用它的本地數據庫來跟蹤狀態變更,並且要負責恢復與狀態變更的所有故障。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"實現編排式架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編排式技術最流行的實現是BPMN規範的各種具體實現,比如"},{"type":"link","attrs":{"href":"https:\/\/www.jbpm.org\/","title":"","type":null},"content":[{"type":"text","text":"jBPM"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/github.com\/camunda\/camunda-bpm-platform","title":"","type":null},"content":[{"type":"text","text":"Camunda"}]},{"type":"text","text":"。對這種系統的需求並不會因爲微服務或Serverless這樣的極度分佈式架構的出現而消失,相反,這種需求還會增加。爲了證明這一點,我們可以看一下較新的有狀態編排引擎,它們沒有遵循什麼規範,但是卻提供了類似的有狀態行爲,比如Netflix的"},{"type":"link","attrs":{"href":"https:\/\/netflix.github.io\/conductor\/","title":"","type":null},"content":[{"type":"text","text":"Conductor"}]},{"type":"text","text":"、Uber的"},{"type":"link","attrs":{"href":"https:\/\/github.com\/uber\/cadence","title":"","type":null},"content":[{"type":"text","text":"Cadence"}]},{"type":"text","text":"和Apache的"},{"type":"link","attrs":{"href":"https:\/\/airflow.apache.org\/","title":"","type":null},"content":[{"type":"text","text":"Airflow"}]},{"type":"text","text":"。像Amazon StepFunctions、Azure Durable Functions和Azure Logic Apps這樣的Serverless有狀態函數也屬於這個類別。還有一些開源庫允許我們實現有狀態的協調和回滾行爲,如Apache Camel的"},{"type":"link","attrs":{"href":"https:\/\/camel.apache.org\/components\/latest\/eips\/saga-eip.html","title":"","type":null},"content":[{"type":"text","text":"Saga"}]},{"type":"text","text":"模式實現和NServiceBus的"},{"type":"link","attrs":{"href":"https:\/\/docs.particular.net\/nservicebus\/sagas\/","title":"","type":null},"content":[{"type":"text","text":"Saga"}]},{"type":"text","text":"功能。許多實現Saga模式的自定義系統也屬於這一類。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/49\/49b4cb842daba77edf8d707ebca43a72.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"編排兩個服務的分佈式事務"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們的示例圖中,我們讓服務A作爲有狀態的編排者,負責調用服務B並在需要的時候通過補償操作從故障中恢復。這種方式的關鍵特徵是,服務A和服務B有本地事務的邊界,但是服務A有協調整個交互流程的知識和責任。這也是爲什麼它的事務邊界會接觸到服務B的端點。在實現方面,我們可以使用同步的交互,就像上圖所示,也可以在服務之間使用消息隊列(在這種情況下我們也可以使用兩階段提交)。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"編排式的優點和缺點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編排式是一種最終一致的方法,它可能會涉及到重試和回滾才能使分佈式系統達到一致的狀態。雖然避免了對分佈式事務的需求,但是編排的方式要求參與的服務提供冪等的操作,以防協調者必須進行重試操作。參與的服務還必須要提供恢復端點,以防協調者決定執行回滾並修復全局狀態。這種方式的最大優點是,能夠僅通過本地事務就能驅動那些可能不支持分佈式事務的異構服務達到一致的狀態。協調者和參與的服務只需要本地事務即可,而且始終能夠通過協調者查詢系統的狀態,即便它目前可能處於部分一致的狀態。在下面我所描述的其他方式中,是不可能實現這一點的。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bb\/bb00e723d834c2a6b7c262b21ebf13b1.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"表3:編排式的優點和缺點"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"協同式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從迄今爲止的討論中,我們可以看到,一個業務操作可能會導致服務間的多次調用,並且一個業務事務完成端到端的處理所需的時間是不確定的。爲了管理這一點,編排式(orchestration)模式會使用一箇中心化的控制器服務,它會告訴參與者該做什麼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編排式的一種替代方案就是"},{"type":"text","marks":[{"type":"italic"}],"text":"協同式(choreography)"},{"type":"text","text":",在這種風格的服務協調中,參與者在交換事件時沒有一箇中心化的控制點。在這種模式下,每個服務會執行一個本地事務併發布事件,從而觸發其他服務中的本地事務。系統中的每個組件都要參與業務事務工作流的決策,而不是依賴一箇中心化的控制點。在歷史上,協同式方式最常見的實現就是使用異步消息層來進行服務的交互。圖6說明了協同式模式的基本架構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/8b\/8b9007618207d1682a17d6bd969a0331.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"通過消息層進行服務協同化"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"具有雙重寫入的協同式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了實現基於消息的服務協同,我們需要每個參與的服務執行一個本地事務,並通過向消息基礎設施發佈一個命令或事件,以觸發下一個服務。同樣的,其他參與的服務必須消費一個消息並執行本地事務。從本質上來講,這就是在一個較高層級的雙重寫入問題中又出現了另一個雙重寫入的問題。當我們開發一個具有雙重寫入的消息層來實現協同式模式的時候,我們可以把它設計成跨本地數據庫和消息代理的一個兩階段提交。在前面,我們曾經介紹過這種方式。另外,我們也可以採用publish-then-local-commit或local-commit-then-publish模式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Publish-then-local-commit"},{"type":"text","text":":我們可以先嚐試發佈一條消息,然後再提交本地事務。雖然這種方案聽起來不錯,但是它有一些切實的挑戰。舉例來說,在很多時候,我們需要發佈一個由本地事務所生成的ID,而這個ID此時還沒有生成,因此無法發佈。另外,本地事務有可能會失敗,但是我們無法回滾已經發布的消息。這種方式缺乏“讀取自己的寫入”的語義,因此對於大多數場景來說,這並不是合適的方案。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Local-commit-then-publish"},{"type":"text","text":":一個稍好一點的辦法是先提交本地事務,然後再發布消息。在本地事務提交之後和消息發佈之前這裏有很小的概率會出現故障。但即便是出現這樣的情況,你也可以把服務設計成冪等的並對操作進行重試。這意味着會再次提交本地事務併發布消息。如果你能控制下游的消費者並且確保它們是冪等的,那麼這種方式就是行之有效的。總體而言,這是一個很好的實現方案。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"無雙重寫入的協同式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現協同式架構的各種實現方式都限制每個服務都要通過本地事務寫入到單一的數據源中,而不能寫入到其他的地方中。我們看一下,如何在避免雙重寫入的情況下實現這一點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設服務A接收到一個請求並要對數據庫A進行寫入操作,除此之外不再操作其他的數據源。服務B週期性地輪詢服務A並探測新的變更。當它讀取到變更時,服務B會基於變更更新自己的數據庫,並且會更新索引或時間戳來標記獲取到了變更。這裏的關鍵在於,這兩個服務只對自己的數據庫進行寫入操作,並以本地事務的形式進行提交。如圖7所示,這種方式可以描述爲"},{"type":"text","marks":[{"type":"italic"}],"text":"服務協同(service choreography)"},{"type":"text","text":",或者我們也可以用非常古老的數據管道的術語來對其進行描述。至於可供選用的實現方案就更有趣了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3d\/3d7ec00a914479a6248b1581f95fa99b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"通過輪詢實現的服務協同"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於服務B來說,最簡單的場景就是連接到服務A的數據庫並讀取服務A的表。但是,業界會盡量避免共享數據表這種級別的耦合,原因在於:服務A的實現和數據模型的任意變更都可能干擾到服務B。我們可以對這種場景做一些改進,例如使用"},{"type":"link","attrs":{"href":"https:\/\/microservices.io\/patterns\/data\/transactional-outbox.html","title":"","type":null},"content":[{"type":"text","text":"發件箱(Outbox)模式"}]},{"type":"text","text":",爲服務A提供一個表作爲公開接口。這個表可以只包含服務B所需的內容,它可以設計得易於查詢和跟蹤變更。如果你覺得這還不夠好的話,進一步的改進方案是讓服務B通過"},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/products\/red-hat-openshift-api-management\/overview","title":"","type":null},"content":[{"type":"text","text":"API管理層"}]},{"type":"text","text":"查詢服務A的所有變化,而不是直接連接數據庫A。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從根本上來講,所有的這些變種形式都有一個相同的缺點:服務B需要不斷地輪詢服務A。這種方式會給系統帶來不必要的持續負載,或者在接收變更時存在不必要的延遲。輪詢微服務的變更並不是常見的做法,那麼我們看一下如何進一步改善這個架構。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用Debezium的協同式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在改進協同式架構時,有一種方式很有吸引力,那就是引入像"},{"type":"link","attrs":{"href":"https:\/\/debezium.io\/blog\/2019\/02\/19\/reliable-microservices-data-exchange-with-the-outbox-pattern\/","title":"","type":null},"content":[{"type":"text","text":"Debezium"}]},{"type":"text","text":"這樣的工具,它使用數據庫A的事務日誌執行變更數據捕獲(change data capture,CDC)。這種方式如圖8所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2f\/2ffa492dd59245ea385e8ef81663c7f2.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"通過變更數據捕獲實現的服務協同"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Debezium可以監控數據庫的事務日誌,執行必要的過濾和轉換,並將相關的變更投遞到Apache Kafka的主題中。這樣的話,服務B就可以監聽主題中的通用事件,而不是輪詢服務A的數據庫或API。我們通過這種方式,將數據庫輪詢轉換成了流式變更,並且在服務間引入了一個隊列,這樣會使得分佈式系統更加可靠、可擴展,而且爲新的使用場景會引入其他消費者提供了可能性。Debezium提供了一種優雅的方式來實現"},{"type":"link","attrs":{"href":"https:\/\/debezium.io\/blog\/2019\/02\/19\/reliable-microservices-data-exchange-with-the-outbox-pattern\/","title":"","type":null},"content":[{"type":"text","text":"發件箱模式"}]},{"type":"text","text":",能夠用於基於編排式和協同式的"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/articles\/saga-orchestration-outbox\/","title":"","type":null},"content":[{"type":"text","text":"Saga模式實現"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方式的一個副作用在於,服務B有接收到重複消息的可能性。這可以通過實現冪等的服務來解決,可以在業務邏輯層面來解決,也可以使用技術化的去重器(deduplicator,比如Apache ActiveMQ Artemis的"},{"type":"link","attrs":{"href":"https:\/\/activemq.apache.org\/components\/artemis\/documentation\/1.1.0\/duplicate-detection.html","title":"","type":null},"content":[{"type":"text","text":"重複消息探測"}]},{"type":"text","text":"或者Apache Camel的冪等消費者模式)。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用事件溯源的協同式模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事件溯源(event sourcing)是另外一種服務協同的實現模式。在這種模式下,實體的狀態會被存儲爲一系列的狀態變更事件。當有新的更新時,不是更新實體的狀態,而是往事件的列表中追加一個新的事件。往事件存儲中追加新的事件是一個原子性的操作,會在一個本地事務中完成。如圖9所示,這種方式的好處在於,對於消費數據更新的其他服務來講,事件存儲的行爲也是一個消息隊列。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b6\/b6e90470862cbc0f706e48c562484a4e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"通過事件溯源實現的服務協同"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們樣例中,如果要轉換成使用事件溯源的話,要把客戶端的請求存儲在一個只能進行追加操作的事件存儲中。服務A可以通過重放(replay)事件重新構建當前的狀態。事件存儲需要讓服務B也訂閱相同的更新事件。通過這種機制,服務A使用其存儲層作爲與其他服務的通信層。儘管這種機制非常整潔,解決了當有狀態變更時可靠地發佈事件的問題,但是它引入了一種很多開發人員所不熟悉的編程風格,並且圍繞狀態重建和消息壓縮,會引入額外的複雜性,這需要專門的存儲。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"協同式的優點和缺點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不管使用哪種方式來檢索數據變更,協同式的模式都解耦了寫入,能夠實現獨立的服務可擴展性,並提升系統整體的彈性。這種方式的缺點在於,決策流是分散的,很難發現全局的分佈式狀態。要查看一個請求的狀態需要查詢多個數據源,這對於服務數量衆多的場景來說是一個挑戰。表4總結了這種方式的優點和缺點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7c\/7cafa970755357bb1cd147b4359da600.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"表4:協同式的優點和缺點"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"並行管道"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在協同式模式中,沒有一箇中心化的地方可以查詢系統的狀態,但是會有一個服務的序列,以便於在分佈式系統中傳播狀態。協同式模式創建了一個處理服務的序列化管道,所以我們能夠知道當一個消息到達整個過程的特定步驟時,它肯定已經通過了前面的所有步驟。如果我們能夠放鬆這個限制,允許獨立地處理這些步驟的話,情況又會怎樣呢?在這種場景下,服務B在處理一個請求的時候,根本不用關心服務A是否已經處理過它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在並行管道的方式中,我們會添加一個路由服務,該服務接收請求,並在一個本地事務中通過消息代理將請求轉發至服務A和服務B。如圖10所示,從這個步驟開始,兩個服務可以獨立、並行地處理請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/fe\/fe1207af5c5302df5d47b518b17ebd93.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"通過並行管道進行處理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管這種模式很容易實現,但是它只適用於服務之間沒有時間約束的場景。例如,服務B不管服務A是否已經處理過該請求,它都能夠對請求進行處理。同時,這種方式需要一個額外的路由服務,或者客戶端知道服務A和服務B,從而能夠給它們發送消息。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"監聽自身"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方式有一種輕量級的替代方案,被稱爲“"},{"type":"link","attrs":{"href":"https:\/\/medium.com\/@odedia\/listen-to-yourself-design-pattern-for-event-driven-microservices-16f97e3ed066","title":"","type":null},"content":[{"type":"text","text":"監聽自身(listen to yourself)"}]},{"type":"text","text":"”模式,在這裏,其中有個服務會同時擔任路由。在這種替代方式下,當服務A接收到一個請求時,它不會寫入到自己的數據庫中,而是將請求發送至消息系統中,而消息的目標是服務B以及服務A本身。圖11闡述了這種模式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/4e\/4e7e55048eecad4beffd33169c9d1315.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"監聽自身模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這裏,不寫入數據庫的原因在於避免雙重寫入。當進入消息系統之後,消息會在完全獨立的事務上下文中進入服務B,也會重新返回服務A。通過這樣一個曲折的處理流程,服務A和服務B就可以獨立地處理請求,並寫入到各自的數據庫中了。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"並行管道的優點和缺點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e7\/e715275ccbc4e9ffeb645ba257d1f322.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"表5:並行管道的優點和缺點"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如何選擇分佈式事務策略"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從本文的論述中,你可能已經猜到,在微服務架構中,處理分佈式事務並沒有正確或錯誤的模式。每種模式都有其優點和缺點。每種模式都能解決一些問題,但是反過來又會產生其他的問題。圖12中的圖表簡單總結了我所闡述的各種雙重寫入模式的主要特徵。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ea\/ea9f0ca4afee6a5fefec553e6b549f3e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"雙重寫入模式的特徵"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不管你採用哪種方式,都要闡述和記錄決策背後的動機,以及該選擇在架構上所帶來的長期影響。你還需要得到從長期實現和維護該系統的團隊那裏獲取支持。在這裏,我根據數據一致性和可擴展性特徵來組織和評估本文所描述的各種方法,如圖13所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/83\/83a0948a3d6204abdab0539228dfa93a.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"各個雙重寫入模式的數據一致性和可擴展性特徵"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們從可擴展性最強、可用性最高的方法到可擴展性最差、可用性最低的順序來評估各種方法。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"高:並行管道和協同式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你的步驟在時間上是解耦的,那麼採用並行管道的方法來運行是很合適的。有可能你只能在系統的某些部分使用這種模式,而不是在整個系統中。接下來,假設步驟間存在時間方面的耦合性,特定的操作和服務必須要在其他的服務前執行,那麼你可以考慮採用協同式的方式。藉助協同式的服務,我們可以創建一個可擴展的、"},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/topics\/event-driven","title":"","type":null},"content":[{"type":"text","text":"事件驅動的架構"}]},{"type":"text","text":",在這裏消息會通過一個去中心化的協同化過程在服務和服務之間流動。在這種情況下,使用Debezium和Apache Kafka的發件箱模式實現(如"},{"type":"link","attrs":{"href":"https:\/\/developers.redhat.com\/products\/red-hat-openshift-streams-for-apache-kafka\/getting-started","title":"","type":null},"content":[{"type":"text","text":"Red Hat OpenShift Streams for Apache Kafka"}]},{"type":"text","text":")特別有趣,而且越來越受歡迎"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"中等:編排式和兩階段提交"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果協同式模式不是很合適,你需要一個負責協調和決策的中心點,那麼可以考慮採用編排式模式。這是一個流行的架構,有基於標準的和自定義的開源實現。基於標準的實現可能會強迫你使用某些事務語義,而自定義的編排式實現則允許你在所需的數據一致性和可擴展性之間進行權衡。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"低:模塊化單體"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你沿着圖示再往左走的話,那麼很可能你對數據一致性有非常強烈的需求,而且對它所需的重大權衡有充分的思想準備。在這種情況下,針對特定數據源,通過兩階段提交的分佈式事務是可行的,但是在專門爲可擴展性和高度可用性設計的動態雲環境中,它很難可靠地實現。如果是這樣的話,那麼你可以直接採用比較老式的模塊化單體方式,同時伴以從微服務運動中學到的實踐。這種方式可以確保最高的數據一致性,但代價是運行時和數據源的耦合。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"結論"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在具有數十個服務的大型分佈式系統中,並不會有一個適用於所有場景的方式,我們需要將其中的幾個方法結合起來,應用於不同的環境中。我們可能會將幾個服務部署在一個共享的運行時上,以滿足對數據一致性的特殊需求。我們可能會選擇兩階段的提交來與支持JTA的遺留系統進行集成。我們可能會編排複雜的業務流程,並讓其餘的服務使用協同式模式和並行處理。總而言之,你選擇什麼策略並不重要,重要的是基於正確的原因,精心選擇一個策略,並執行它。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章