比較服務間通訊的技術 - ardalis

banq發表於2021-08-31

在分散式軟體應用程式中,不同的服務或程式或應用程式經常需要相互通訊。微服務和容器以及雲原生應用程式的現代架構趨勢都增加了應用程式將越來越多地部署為相關服務的集合而不是單個單體的可能性。這些應用程式可以透過多種不同的方式相互通訊,每種選擇都會帶來一定的好處以及後果和權衡。讓我們考慮選項並根據其相對效能、可擴充套件性、應用程式隔離或獨立性以及複雜性來評估每個選項。另外需要CAP定理
 
案例場景:在評估下面顯示的每種技術和模式時,請考慮使用者嘗試從 Web 應用程式 (A) 購買某些產品。Web 應用程式依賴於單獨的系統 (B) 來獲取產品目錄資訊,包括每個產品的最新定價。在結帳過程中,它需要查詢產品目錄以獲取所購買商品的最新價格。暫時忽略這是否是電子商務系統的最佳設計,而只是考慮如何進行這種通訊。
 

共享資料
傳統上,許多公司將擁有一個資料庫,應用程式都將連線到該資料庫。資料庫價格昂貴且任務關鍵,因此只要擁有其中一個,就可以更輕鬆地聘請專家來保護和最佳化它。今天,資料儲存是可以輕鬆部署為任何單個應用程式或服務的一部分的商品,並且眾所周知,使用資料庫作為程式間通訊的主要機制會對服務/應用程式的獨立性產生很多負面影響。
在電子商務示例中,訂單處理服務 (A) 和產品目錄 (B) 將它們的資料儲存在同一個資料庫中。這意味著服務 A 可以簡單地查詢適當的表來獲取完成客戶訂單所需的價格資料。

  • 效能

單個資料庫可以為大量請求提供足夠的效能,尤其是讀取。但是,當表變大且未正確索引時,或者應用大量更新時,關聯式資料庫的效能會受到影響。
  • 可擴充套件性

雲提供允許單個資料庫擴充套件到大規模,儘管這並非沒有大量成本。
  • 應用隔離

使用共享的可變全域性狀態作為應用程式之間的整合方式的最大問題是它們都與共享狀態提供程式(資料庫)緊密耦合。任何時候資料庫關閉,所有應用程式都會關閉,並且對資料庫的任何更改都可能導致依賴它的任意數量的應用程式關閉。
  • 複雜

由於大多數 Web 應用程式至少需要在外部資料儲存中儲存一些狀態,因此利用共享資料儲存通常不會顯著增加應用程式的複雜性。事實上,每個依賴於共享資料庫的應用程式都可以像它是該資料庫的唯一使用者一樣構建,但需要注意的是,它不能在不破壞其他應用程式的情況下對資料庫進行任何破壞性更改。
 

直接 API 呼叫
當您需要其他服務提供的東西時,有時最簡單的方法就是詢問。在這種情況下,訂單處理服務 (A) 可以對產品目錄服務 (B) 進行同步 API 呼叫。這要求服務 A 知道服務 B,並且這兩個服務同時可用。然而,這是一種相當簡單的方法,不需要任何額外的服務或複雜性,如訊息佇列或匯流排。

  • 效能

直接 API 呼叫的效能在很大程度上取決於處理和完成呼叫的速度。對於冗長的請求,呼叫的同步特性會損害上游服務的效能(即服務 A 的響應時間至少與服務 B 的響應時間一樣長)。
  • 可擴充套件性

由於服務 A 和 B 都可以橫向擴充套件,因此該技術可以根據需要很好地擴充套件。
  • 應用隔離

在此方法的基本版本中,服務 A 依賴於服務 B。服務 B 簽名的任何重大更改都需要更新服務 A。此外,如果服務 B 的網路位置發生變化,則必須更新服務 A。這些問題可以透過使用集中配置和/或閘道器模式來緩解。不能輕易緩解的是兩個服務之間的時間依賴性。如果 B 不可用,A 也將不可用。
  • 複雜

這種解決方案往往比共享資料庫更復雜,因為涉及多個應用程式程式。除錯和測試可能會更加困難,並且應用程式失敗的方式也更多。但是,總的來說,這是一種相對簡單的方法。
 

具有非同步輪詢的直接 API 呼叫
對於較長的請求,初始 API 呼叫可以快速完成,但會提供一個位置標頭,指示在哪裡檢查請求的狀態。對狀態端點的後續呼叫(如果成功)最終將導致資源或結果被請求。
在這個例子中,對於任何不能快速完成的請求,服務 B 可以返回一個 202 和狀態端點的位置。服務 A 可以輪詢狀態端點(額外的標頭可能指示在再次檢查狀態之前等待多長時間),最終取回它期望的結果(或超時或任何數量的其他錯誤狀態)。請注意,如果需要,此模式可以批次應用於所有 API 呼叫,從而產生一致的後端方法。但是,更多情況下,它會根據需要新增到效能緩慢的端點。該模式也可以短路,如果例如請求的資源已經準備好(例如在快取中可用),則直接返回資源,而不是返回 202 並強制呼叫者點選狀態端點。

  • 效能

涉及輪詢和多個請求的解決方案几乎總是比直接呼叫的解決方案效能更差。因此,由於需要更多 Web 請求,以及在資源準備就緒和呼叫者輪詢獲取資源之間發生的額外浪費時間,此解決方案將導致效能損失。較短的輪詢間隔將減少這種浪費時間的程度,但會導致網路和系統的整體負載增加。
  • 可擴充套件性

使用這種方法可以提高服務 A 和 B 的可擴充套件性。服務 A 可能能夠處理更多請求,因為它將有更少的傳出請求被阻止等待響應(儘管這對於 HTTP 客戶端程式碼中的現代非同步模式來說不是一個因素)。如果服務 B 能夠將處理解除安裝到其他資源,並且只負責快速返回 202 響應,然後處理狀態檢查,那麼它可以擴充套件以處理更多請求。涉及的實際工作可能由根本沒有耦合到服務 B 的其他程式完成。
  • 應用隔離

應用程式隔離評估與直接 API 呼叫相比並沒有真正改變。如果有的話,情況更糟,因為除了需要發出初始請求的服務 B 上的特定端點之外,服務 A 現在還依賴於狀態端點和服務 B 使用的輪詢模式(標頭等)。
  • 複雜

這種模式透過新增額外的端點和輪詢行為增加了同步 API 呼叫的現有複雜性。
 

非同步訊息
一些應用程式採用消除服務之間所有同步呼叫的方法,而是選擇對所有內容使用訊息。雖然非同步訊息適用於釋出狀態事件和發出命令,但它們更難用於查詢。許多利用 CQRS 的架構將在模式的命令部分使用訊息傳遞系統,同時將查詢作為同步呼叫。然而,這種方法將查詢作為非同步命令發出,然後等待直到指示查詢處理的相應事件發生或立即返回並在響應到達時透過其他方式通知客戶端查詢的狀態。

  • 效能

與直接同步模式相比,基於訊息傳遞的系統對於單個呼叫的效能通常更差。此外,如果在佇列中建立了大量請求,單個請求的效能可能會發生巨大變化,因為每個請求現在都必須透過佇列才能開始處理。
  • 可擴充套件性

基於非同步訊息傳遞的系統往往具有極高的可擴充套件性,因為任意數量的服務都可以獨立於彼此和請求系統來獲取訊息並處理它們。此外,使用訊息匯流排返回響應進一步使應用程式彼此分離,從而更容易擴充套件系統。
  • 應用隔離

這種方法的一個好處是服務 A 和 B 之間沒有直接的依賴關係。它們不依賴於彼此的 API 定義或資料庫模式(然而,它們都依賴於一種通用的訊息模式)。它們不需要同時可用。服務 B 可以短時間停機,而服務 A 可以繼續不受阻礙地工作。
  • 複雜

非同步處理本質上比同步處理更復雜,並且要求除了每個命令之外的每個查詢都非同步處理會導致更加複雜。在執行時診斷問題和除錯都比使用更簡單的系統更困難,並且通常需要更高階的日誌記錄和監控功能來實現這些系統的更好可維護性。
 

來自真實來源的直接 API 更新的本地快取
最快呼叫另一項服務是您不必撥打的電話一樣的呼叫。無需在每次需要一條資訊時呼叫另一個服務,尤其是服務經常需要的資訊,資料的本地副本可以儲存在快取中。這可以是記憶體快取,也可以是像 Redis 這樣的永續性儲存,甚至是服務用於其自身永續性需求的相同資料庫。
任何時候在快取中找不到所需的資料時,都可以使用Cache-Aside 模式從“真實來源”服務請求它。快取條目通常會給出一個過期日期,但為了更好地提高執行時效能(並避免讓客戶端請求支付更新快取的成本),下游服務可以對消費服務進行 API 呼叫以更新其快取版本資料隨時更改。透過這種方式,快取可以與其源資料保持同步,而不必需要短暫的到期或頻繁的更新,至少對於“主要讀取”型別的資料而言。

  • 效能

透過消除在絕大多數情況下在執行時呼叫服務 B 的需要,這種方法的效能超過了上面列出的 API 和基於訊息的通訊方法(並且可能與共享資料庫方法大致相同,但可能更好,因為服務 A 的資料庫不太可能受到來自其他應用程式的負載的影響)。
  • 可擴充套件性

對上游服務消費者的直接 API 呼叫只能擴充套件到目前為止。如果更新頻繁且訂閱應用程式的數量很大,服務 B 很快就會花費大量時間進行服務間呼叫來更新其他應用程式(這些應用程式可能需要也可能不需要此類持續更新)。但是,對於資料相對穩定的相對較小的系統,這種方法非常有效。
  • 應用隔離

這種設計使用了一種依賴倒置的形式。儘管服務 A 的資料依賴於服務 B,但服務 A 對服務 B 沒有編譯時或執行時依賴性(除非它使用快取輔助模式,這是可選的)。相反,服務 B 需要了解服務 A 及其用於處理資料更新的 API。它還要求服務 A 在對其資料進行更新時隨時可用,否則它將需要重試邏輯以確保最終的一致性,或者系統將需要求助於快取超時以確保丟失的更新最終得到糾正。
  • 複雜

服務 A 的複雜性低於所示的大多數其他方法。如果沒有 Cache-Aside 模式,服務 A 的執行就好像它需要的所有資料都在它自己的資料儲存中是本地的(儘管它應該注意只讀取而不是更新這些資料)。但是,它確實需要構建和公開 API 來更新其他只讀資料,當更新發生時,服務 B 將使用哪些資料。
然而,服務 B 更復雜,因為它需要執行其職責範圍內的任何操作,然後在發生會影響這些服務的快取資料的更改時處理對所有訂閱服務的更新。這可能比起初看起來更困難,因為它通常不像直接的 1:1 表對映那麼簡單。
 

來自真實來源的更新事件的本地快取
與必須去獲取資料相比,已經擁有資料仍然會更快。使用這種方式,設計和之前的一樣,只是下游服務不再呼叫 API 來更新上游服務,而是簡單地釋出事件。這種方法具有前一種方法的所有優點,但大大簡化了整體架構。不必構建、定義和使用 API,服務 A 只需要處理它感興趣的某些型別的事件,而服務 B 只需要在發生某些更改時釋出事件。

  • 效能

高,和上一個一樣。
  • 可擴充套件性

像所有基於訊息的系統一樣,這個系統的擴充套件性非常好。
  • 應用隔離

服務 A 可以在沒有服務 B 的情況下執行。服務 B 可以在沒有服務 A 的情況下執行。在執行時兩者都不依賴於另一個;兩者都只取決於傳遞的訊息的格式和所使用的訊息匯流排的實現細節。
  • 複雜

這種方法比僅使用共享資料庫更復雜,但服務 A 能夠像在那種情況下一樣輕鬆地讀取資料(可能更容易,因為它可以完全控制資料結構並且不共享資料)與任何其他服務或應用程式的架構)。但是,服務 A 需要支援訊息處理程式來檢測更新並將它們應用於其本地資料儲存。服務 B 只需要在進行更新時釋出事件,這會增加一些複雜性,但通常比處理對多個應用程式上的多個 API 端點的呼叫更簡單,正如前面的模式所要求的那樣。
 

混合方法
一致性很有價值,可以幫助降低系統的複雜性。但是,您應該為了複雜性而犧牲所需的使用者體驗。如果您的大部分系統都可以使用一種技術,但少數服務會從使用另一種方法中受益,那麼請務必使用正確的工具來完成這項工作。您可以隨意混合搭配技術,但通常最好確定給定技術最適合的一類服務或行為。也許報告需要一段時間才能從 API 和輪詢技術中獲益。也許命令受益於純粹的非同步訊息傳遞方法,而查詢使用更具決定性的流程。如果存在可以遵循的既定規則,則可以減少不一致,因此編寫新服務的新開發人員可以輕鬆確定要採用的適當模式。架構決策記錄 (ADR)有助於記錄和傳達此類決策和政策。

 

相關文章