微服務通信設計模式

猿來猿往 發佈 2022-02-09T09:37:39+00:00

微服務很有意思,可以幫助我們創建可伸縮的、高效的架構,因此當前幾乎所有主流平台都基於微服務架構系統。如果沒有微服務,就不可能有現在的Netflix、Facebook或Instagram。然而,將業務邏輯分解為更小的單元並以分布式方式部署它們只是第一步。

微服務很有意思,可以幫助我們創建可伸縮的、高效的架構,因此當前幾乎所有主流平台都基於微服務架構系統。如果沒有微服務,就不可能有現在的Netflix、Facebook或Instagram。

然而,將業務邏輯分解為更小的單元並以分布式方式部署它們只是第一步。我們還必須理解怎樣才能讓服務之間更好的通信。沒錯,微服務不僅僅是面向外部的——或者換句話說,為外部客戶服務——很多時候它們也是同一系統中其他服務的客戶。

那麼,如何使兩個服務相互通信呢?簡單的方案是繼續使用呈現給外部客戶的API。例如,如果我們面向外部客戶的API是REST HTTP API,那麼內部服務也可以通過這些API進行交互。

這是一個很合理的設計,但讓我們看看有沒有其他改進方案。

註:通信是基於商定的協議,微服務之間以及服務和客戶之間的通信都是如此,始終確保協議一致的一種方法是在這些解耦的代碼庫之間共享描述這些協議的代碼,可以是類、類型、模擬數據對象等,Bit[2]就是有助於實現這一目標的工具。

Bit從源頭獨立的控制TS/JS模塊,即使它們被部署到獨立的遠程主機上,也能維護它們之間的依賴關係,從而使得對某一模塊的更新能夠觸發其所有依賴模塊的持續集成。


HTTP API

HTTP API畢竟是非常有效的設計,就讓我們從這兒開始。HTTP API本質上意味著讓服務就像響應瀏覽器或者Postman[3]這樣的桌面客戶端那樣發送信息。

HTTP API基於CS模式,意味著通信只能由客戶端發起。這也是一種同步通信,意味著一旦通信由客戶端發起,要一直等到服務端返迴響應才會結束。


經典的CS微服務通信


因為這和我們訪問網際網路的方式一致,因此這種方法非常流行。HTTP是網際網路的支柱,因此所有程式語言都通過某種方式支持HTTP功能,從而使其成為一種非常流行的方法。

但這種方式並不完美,我們來分析一下。

優點

  • 容易實現。HTTP協議並不難實現,而且所有主要的程式語言都已經對它提供了原生支持,開發人員幾乎不需要擔心其內部是如何工作的,複雜性被類庫所隱藏和抽象。
  • 可以被標準化。如果在HTTP之上添加了REST之類的東西(正確實現的),其實就是創建了一個標準API,允許任何客戶端可以快速學習如何與我們的業務邏輯進行通信。
  • 技術中立。由於HTTP充當了客戶端和伺服器之間的數據傳輸通道,因此和兩端的具體實現技術無關。可以用Node.js實現服務端,用JAVA或C#實現客戶端(或其他服務),只要遵循相同的HTTP協議,就能夠彼此通信。

缺點

  • 額外的時延。作為HTTP協議的一部分,有若干個步驟確保了數據被正確發送,因此HTTP非常可靠。然而,該這樣也給通信增加了延遲(額外的步驟意味著額外的時間)。因此,考慮這樣一個場景:在最後一個微服務完成之前,3個或更多的微服務需要在彼此之間交換數據。換句話說,需要讓A向B發送數據,這樣B才可以向C發送數據,然後C才能夠發送響應。除了每個服務的處理時間外,還必須考慮在它們之間建立3個HTTP通道所增加的延遲。
  • 超時。雖然可以在大多數場景中配置超時時間,但默認情況下,如果伺服器占用的時間太長,將導致客戶端關閉連接。多長時間是「太長」?這取決於配置和當前的服務,但是總會有這麼個時間。這為業務邏輯增加了額外的約束:需要快速執行,否則將失敗。
  • 失敗難以解決。解決伺服器故障並不是不可能的,但是需要有額外的基礎設施。默認情況下,如果伺服器關閉,將不會通知客戶端。客戶端只有在試圖訪問伺服器時才會意識到這一點,但已經為時已晚。有一些方法可以緩解這種情況,例如使用負載平衡器或API網關,但需要在CS通信之上進行額外的工作,以使其更可靠。

因此,如果我們的業務邏輯快速可靠,並且需要被許多不同的客戶端訪問,HTTP API是一個很好的解決方案。多個團隊在不同的客戶端上工作時,可以基於一個標準、一致的接口通信,這會非常有用。

如果多個服務需要互相交互,或者其中一些服務中的業務邏輯需要大量時間才能完成,那麼就不要使用HTTP API。


異步消息(Asynchronous Messaging)

這種模式還包括了一個在消息生產者和接收端之間的消息代理。

這絕對是我最喜歡的多服務之間通信的方式之一,尤其是當我們需要橫向擴展平台的處理能力時。

微服務之間的異步通信


這種模式通常需要引入消息代理,因此會增加額外的複雜性。然而,這樣做的好處遠不止於抽象。

優點

  • 容易擴展。客戶端和伺服器之間直接通信的一個主要問題是,為了讓客戶端能夠發送消息,伺服器需要有空閒的處理能力,但這受到單個服務可以執行的並行處理量的限制。如果客戶端需要發送更多的數據,那麼服務就需要擴容並擁有更多的處理能力。這有時可以通過擴展服務部署的基礎設施來解決,使用更好的處理器或更多的內存,但總會有上限。相反,我們可以繼續使用較低規格的基礎設施,並讓多個副本並行工作。消息代理可以將接收到的消息分發到多個目標服務,可以根據需求,讓副本接收相同的數據或不同的消息。
  • 易於添加新服務。創建新服務、訂閱希望接收的消息類型、將新服務連接到工作流,都會很簡單。生產者不需要知道新服務,只需要知道需要發送什麼樣的消息。
  • 簡單的重試機制。如果消息的傳遞由於伺服器宕機而失敗,只要消息代理願意,可以自動繼續嘗試,不需要編寫特殊的邏輯。
  • 事件驅動。異步消息可以幫助我們創建事件驅動體系架構,這是微服務交互的最有效方式之一。與其讓單個服務因為等待同步響應而被阻塞,或者更糟的是,讓它不斷輪詢某個存儲介質來等待響應,還不如編寫服務代碼,以便在數據準備就緒時通知它們。當需要等待響應時,服務可以處理其他事情(比如處理下一個傳入的請求)。這種架構可以更快的數據處理、更有效的使用資源和提供更好的整體通信體驗。

缺點

  • 調試困難。由於沒有明確的數據流,只是承諾消息會被儘快處理,因此調試數據流和數據處理路徑可能會成為一場噩夢。這就是為什麼通常需要在接收到消息時生成一個唯一ID,這樣就可以通過日誌跟蹤消息在內部系統中的路徑。
  • 沒有明確的直接響應。考慮到此模式的異步特性,一旦從客戶端接收到請求,唯一可能的響應是「OK,收到了,一旦準備好,我會讓您知道」。對於無效請求,還可以發送400錯誤。問題是,客戶端不能直接訪問服務端的執行邏輯返回的輸出,而是需要單獨請求。作為一種替代方法,客戶端可以訂閱響應消息類型。通過這種方式,一旦響應消息到達,客戶端將立即得到通知。
  • 代理成為單點故障。如果沒有正確配置消息代理,它可能會成為架構的問題。雖然不必忍受自己編寫的不穩定的服務,但卻被迫維護一個幾乎不知道如何使用的消息代理。

這絕對是一個有趣的模式,並且提供了很大的靈活性。如果生產者端需要產生大量消息,那麼在生產者和消費者之間有一個類似緩衝區的結構將增加系統的穩定性。

雖然處理過程可能會很慢,但有了緩衝區後,擴展將變得容易得多。


socket連結(Direct socket connection)

有時候我們不必依賴古老的HTTP來發送和接收消息,而是可以採用一些完全不同的路徑,使用一些更快的技術,比方說socket。

為微服務通信打開socket通道


乍一看,基於socket的通信很像在HTTP中實現的客戶端-伺服器模式,然而,如果仔細看,還是有一些區別:

  • 對於初學者來說,協議要簡單得多,這意味著也要快得多。當然,如果希望提供可靠的通信,需要編寫更多代碼來實現,不過HTTP所增加的額外延遲在這裡已經消失了。
  • 通信可以由任何一方參與者啟動,而不僅僅是客戶端。一旦打開socket通道,它會一直保持這種狀態,直到被關閉。可以把它想像成一個進行中的電話,任何人都可以開始對話,而不僅僅是打電話的人。

話雖如此,還是來看看這種方法的利弊:

缺點

  • 沒有真正的標準。與HTTP相比,基於socket的通信似乎有點混亂,沒有任何結構化的標準(比如SOAP和REST)。因此,需要實現方來定義通信結構。反過來又使得創建和實現新客戶端有點困難。但是,如果只是為了自己的服務可以相互交互,那麼實際上是在實現自定義協議。
  • 容易使接收端過載。如果一個服務產生太多的消息讓另一個服務無法處理,那麼可能會導致第二個服務無法承受並崩潰。這就是上一個模式解決的問題。在這裡,發送和接收消息之間的延遲非常小,這意味著吞吐量可以更高,但也意味著接收服務必須足夠快的處理所有事情。

優點

  • 輕量級。實現基本的socket通信只需要很少的工作和設置。當然,這取決於使用的程式語言,但其中一些,例如帶有Socket.io[4]的Node.js,可以通過幾行代碼就實現兩個服務的通信。
  • 非常優化的通信流程。由於在兩個服務之間有一個長時間打開的通道,因此雙方都能夠在消息到達時作出反應。和拉取資料庫來獲取新消息的方式不一樣,這是一個反射性的方法(reactive approach),沒有比這個更快的方式了。

基於socket的通信是讓服務彼此通信的非常有效的方式。例如,當部署為集群時,Redis使用這個方法來自動檢測失敗的節點,並將它們從集群中移除。由於通信速度快且成本低(意味著幾乎沒有額外的延遲,並且只需要很少的網絡資源),才可以做到這一點。

如果能夠控制服務之間交換的信息量,並且不介意定義自己的標準協議,那麼就可以使用這種方法。


輕量級事件(Lightweight events)

此模式混合了前兩種模式。一方面,它提供了一種讓多個服務通過消息總線相互通信的方式,從而允許異步通信。另一方面,由於它只通過該通道發送非常輕量級的載荷,並要求調用相應服務的REST API將額外信息與載荷結合起來。


微服務通信中的輕量級事件和API的混合作用


當我們希望儘可能控制網絡流量,或者當消息隊列有包大小限制時,這種通信模式非常方便。在這種情況下,最好讓事情儘可能簡單,然後只在需要的時候要求額外的信息。

優點

  • 兩全其美。因為有80-90%的數據通過類似緩衝區的結構發送,因此這種方法提供了異步通信模式的優點,並且只需要通過效率較低但標準的、基於API的方法來完成一小部分網絡流量。
  • 重點優化最常見的場景。如果我們知道在大多數情況下不需要使用額外的信息來填充事件,那麼將其保持在最低限度將有助於優化網絡流量,並將消息代理的需求保持在非常低的水平。
  • 基本的緩衝區。通過這種方法,每個事件的額外細節都是保密的,並且遠離緩衝區。這反過來又消除了在需要為這些消息定義schema的情況下可能有的耦合。保持緩衝區的「啞(dumb)」使它更容易與其他系統交互,特別是在需要遷移或擴展的情況下(例如從RabbitMQ遷移到AWS SQS)。

缺點

  • 可能會有太多API請求。如果不小心為不適合的用例實現此模式,那麼最終將面臨API請求的開銷,而這會增加響應服務的額外延遲,更不用說服務之間發送的所有HTTP請求所增加的額外網絡流量了。如果面臨這樣的場景,請考慮切換到完全基於異步的通信模型。
  • 兩倍的通信接口。服務必須提供兩種不同的通信方式。一方面,需要實現消息隊列所需的異步模型,但另一方面,還必須具有類似於API的接口。考慮到兩種方法使用的不同,這可能會變得難以維護。

這是一種非常有趣的混合模式,考慮到需要將兩種方法混合在一起,需要花費一些精力編寫代碼。

這可以是一種非常好的網絡優化技術,確保對於對應用例的載荷混合請求只發生大約10 - 20%的比例,否則帶來的好處將不值得為其編寫額外的代碼。


微服務之間通信的最佳方式是提供了我們想要的東西的方式,可以是性能、可靠性或者安全性,我們必須知道想要什麼,然後基於這些信息來選擇最佳模式。

沒有通信模式的銀彈,即使像我一樣更喜歡其中一種模式,現實的說,還是必須找到適應當前用例的模式。

關鍵字: