微服務之間如何調用最佳?


在微服務架構中,需要調用很多服務才能完成一項功能。服務之間如何互相調用就變成微服務架構中的一個關鍵問題。

服務調用有兩種方式,一種是 RPC 方式,另一種是事件驅動(Event-driven)方式,也就是發消息方式。

消息方式是松耦合方式,比緊耦合的 RPC 方式要優越,但 RPC 方式如果用在適合的場景也有它的一席之地。

我們總在談耦合,那麼耦合到底意味着什麼呢?

耦合的種類:

時間耦合: 客戶端和服務端必須同時上線才能工作。發消息時,接受消息隊列必須運行,但後臺處理程序暫時不工作也不影響。

容量耦合: 客戶端和服務端的處理容量必須匹配。發消息時,如果後臺處理能力不足也不要緊,消息隊列會起到緩衝的作用。

接口耦合: RPC 調用有函數標籤,而消息隊列只是一個消息。例如買了商品之後要調用發貨服務,如果是發消息,那麼就只需發送一個商品被買消息。

發送方式耦合: RPC 是點對點方式,需要知道對方是誰,它的好處是能夠傳回返回值。消息既可以點對點,也可以用廣播的方式,這樣減少了耦合,但也使返回值比較困難。

下面我們來逐一分析這些耦合的影響。第一,時間耦合,對於多數應用來講,你希望能馬上得到回答,因此即使使用消息隊列,後臺也需要一直工作。

第二,容量耦合,如果你對回覆有時間要求,那麼消息隊列的緩衝功能作用不大,因爲你希望及時響應。

真正需要的是自動伸縮(Auto-scaling),它能自動調整服務端處理能力去匹配請求數量。第三和第四,接口耦合和發送方式耦合,這兩個確實是 RPC 方式的軟肋。

事件驅動(Event-Driven)方式

Martin Fowler 把事件驅動分成四種方式 (What do you mean by “Event-Driven”),簡化之後本質上只有兩種方式。一種就是我們熟悉的的事件通知(Event Notification),另一種是事件溯源(Event Sourcing)。

事件通知就是微服務之間不直接調用,而是通過發消息來進行合作。事件溯源有點像記賬,它把所有的事件都記錄下來,作爲永久存儲層,再在它的基礎之上構建應用程序。

實際上從應用的角度來講,它們並不應該分屬一類,它們的用途完全不同。事件通知是微服務的調用(或集成)方式,應該和 RPC 分在一起。事件溯源是一種存儲數據的方式,應該和數據庫分在一起。

事件通知(Event Notification)方式

讓我們用具體的例子來看一下。在下面的例子中,有三個微服務,“Order Service”, “Customer Service” 和 “Product Service”。

先說讀數據,假設要創建一個 “Order”,在這個過程中需要讀取“Customer” 的數據和 “Product” 數據。

如果用事件通知的方式就只能在 “Order Service” 本地也創建只讀 “Customer” 和“Product”表,並把數據用消息的方式同步過來。

再說寫數據,如果在創建一個 “Order” 時需要創建一個新的 “Customer” 或要修改 “Customer” 的信息,那麼可以在界面上跳轉到用戶創建頁面,然後在 “Customer Service” 創建用戶之後再發”用戶已創建 “的消息,“Order Service” 接到消息,更新本地 “Customer” 表。

這並不是一個很好的使用事件驅動的例子,因爲事件驅動的優點就是不同的程序之間可以獨立運行,沒有綁定關係。但現在 “Order Service” 需要等待 “Customer Service” 創建完了之後才能繼續運行,來完成整個創建 “Order” 的工作。主要是因爲 “Order” 和“Customer”本身從邏輯上來講就是緊耦合關係,沒有 “Customer” 你是不能創建 “Order” 的。

在這種緊耦合的情況下,也可以使用 RPC。你可以建立一個更高層級的管理程序來管理這些微服務之間的調用,這樣 “Order Service” 就不必直接調用 “Customer Service” 了。

當然它從本質上來講並沒有解除耦合,只是把耦合轉移到了上一層,但至少現在 “order Service” 和“Customer Service”可以互不影響了。之所以不能根除這種緊耦合關係是因爲它們在業務上是緊耦合的。

再舉一個購物的例子。用戶選好商品之後進行 “Checkout”,生成“Order”,然後需要“payment”,再從“Inventory” 取貨,最後由 “Shipment” 發貨,它們每一個都是微服務。這個例子用 RPC 方式和事件通知方式都可以完成。

當用 RPC 方式時,由 “Order” 服務調用其他幾個服務來完成整個功能。用事件通知方式時,“Checkout”服務完成之後發送 “Order Placed” 消息,“Payment”服務收到消息,接收用戶付款,發送 “Payment received” 消息。

“Inventory”服務收到消息,從倉庫裏取貨,併發送 “Goods fetched” 消息。“Shipment”服務得到消息,發送貨物,併發送 “Goods shipped” 消息。

對這個例子來講,使用事件驅動是一個不錯的選擇,因爲每個服務發消息之後它不需要任何反饋,這個消息由下一個模塊接收來完成下一步動作,時間上的要求也比上一個要寬鬆。用事件驅動的好處是降低了耦合度,壞處是你現在不能在程序裏找到整個購物過程的步驟。

如果一個業務邏輯有它自己相對固定的流程和步驟,那麼使用 RPC 或業務流程管理(BPM)能夠更方便地管理這些流程。在這種情況下選哪種方案呢?在我看來好處和壞處是大致相當的。從技術上來講要選事件驅動,從業務上來講要選 RPC。不過現在越來越多的人採用事件通知作爲微服務的集成方式,它似乎已經成了微服務之間的標椎調用方式。

事件溯源 (Event Sourcing)

這是一種具有顛覆性質的的設計,它把系統中所有的數據都以事件(Event)的方式記錄下來,它的持久存儲叫 Event Store, 一般是建立在數據庫或消息隊列(例如 Kafka)基礎之上,並提供了對事件進行操作的接口,例如事件的讀寫和查詢。事件溯源是由領域驅動設計 (Domain-Driven Design) 提出來的。

DDD 中有一個很重要的概念,有界上下文(Bounded Context),可以用有界上下文來劃分微服務,每個有界上下文都可以是一個微服務。下面是有界上下文的示例。下圖中有兩個服務 “Sales” 和“Support”。

有界上下文的一個關鍵是如何處理共享成員, 在圖中是 “Customer” 和“Product”。在不同的有界上下文中,共享成員的含義、用法以及他們的對象屬性都會有些不同,DDD 建議這些共享成員在各自的有界上下文中都分別建自己的類(包括數據庫表),而不是共享。可以通過數據同步的手段來保持數據的一致性。下面還會詳細講解。

事件溯源是微服務的一種存儲方式,它是微服務的內部實現細節。因此你可以決定哪些微服務採用事件溯源方式,哪些不採用,而不必所有的服務都變成事件溯源的。通常整個應用程序只有一個 Event Store, 不同的微服務都通過向 Event Store 發送和接受消息而互相通信。

Event Store 內部可以分成不同的 stream(相當於消息隊列中的 Topic), 供不同的微服務中的領域實體(Domain Entity)使用。

事件溯源的一個短板是數據查詢,它有兩種方式來解決。第一種是直接對 stream 進行查詢,這隻適合 stream 比較小並且查詢比較簡單的情況。

查詢複雜的話,就要採用第二種方式,那就是建立一個只讀數據庫,把需要的數據放在庫中進行查詢。數據庫中的數據通過監聽 Event Store 中相關的事件來更新。

數據庫存儲方式只能保存當前狀態,而事件溯源則存儲了所有的歷史狀態,因而能根據需要回放到歷史上任何一點的狀態,具有很大優勢。但它也不是一點問題都沒有。

第一,它的程序比較複雜,因爲事件是一等公民,你必須把業務邏輯按照事件的方式整理出來,然後用事件來驅動程序。第二,如果你要想修改事件或事件的格式就比較麻煩,因爲舊的事件已經存儲在 Event Store 裏了(事件就像日誌,是隻讀的),沒有辦法再改。

由於事件溯源和事件通知表面上看起來很像,不少人都搞不清楚它們的區別。事件通知只是微服務的集成方式,程序內部是不使用事件溯源的,內部實現仍然是傳統的數據庫方式。

只有當要與其他微服務集成時纔會發消息。而在事件溯源中,事件是一等公民,可以不要數據庫,全部數據都是按照事件的方式存儲的。

雖然事件溯源的踐行者有不同的意見,但有不少人都認爲事件溯源不是微服務的集成方式,而是微服務的一種內部實現方式。因此,在一個系統中,可以某些微服務用事件溯源,另外一些微服務用數據庫。

當你要集成這些微服務時,你可以用事件通知的方式。注意現在有兩種不同的事件需要區分開,一種是微服務的內部事件,是顆粒度比較細的,這種事件只發送到這個微服務的 stream 中,只被事件溯源使用。

另一種是其他微服務也關心的,是顆粒度比較粗的,這種事件會放到另外一個或幾個 stream 中,被多個微服務使用,是用來做服務之間集成的。這樣做的好處是限制了事件的作用範圍,減少了不相關事件對程序的干擾。詳見 "Domain Events vs. Event Sourcing"。

事件溯源出現已經很長時間了,雖然熱度一直在上升(尤其是這兩年),但總的來說非常緩慢,談論的人不少,但生產環境使用的不多。究其原因就是應爲它對現在的體系結構顛覆太大,需要更改數據存儲結構和程序的工作方式,還是有一定風險的。

另外,微服務已經形成了一整套體系,從程序部署,服務發現與註冊,到監控,服務韌性(Service Resilience),它們基本上都是針對 RPC 的,雖然也支持消息,但成熟度就差多了,因此有不少工作還是要自己來做。

有意思的是 Kafka 一直在推動它作爲事件驅動的工具,也取得了很大的成功。但它卻沒有得到事件溯源圈內的認可。

多數事件溯源都使用一個叫 evenstore 的開源 Event Store,或是基於某個數據庫的 Event Store,只有比較少的人用 Kafka 做 Event Store。

但如果用 Kafka 實現事件通知就一點問題都沒有。總的來說,對大多數公司來講事件溯源是有一定挑戰的,應用時需要找到合適的場景。如果你要嘗試的話,可以先拿一個微服務試水。

雖然現在事件驅動還有些生澀,但從長遠來講,還是很看好它的。像其他全新的技術一樣,事件溯源需要大規模的適用場景來推動。例如容器技術就是因爲微服務的流行和推動,才走向主流。

事件溯源以前的適用場景只限於記賬和源代碼庫,侷限性較大。區塊鏈可能會成爲它的下一個機遇,因爲它用的也是事件溯源技術。

另外 AI 今後會滲入到具體程序中,使程序具有學習功能。而 RPC 模式註定沒有自適應功能。事件驅動本身就具有對事件進行反應的能力,這是自我學習的基礎。因此,這項技術長遠來講定會大放異彩,但短期內(3-5 年)大概不會成爲主流。

RPC 方式

RPC 的方式就是遠程函數調用,像 RESTFul,gRPC, DUBBO 都是這種方式。它一般是同步的,可以馬上得到結果。在實際中,大多數應用都要求立刻得到結果,這時同步方式更有優勢,代碼也更簡單。

服務網關(API Gateway)

熟悉微服務的人可能都知道服務網關(API Gateway)。當 UI 需要調用很多微服務時,它需要了解每個服務的接口,這個工作量很大。

於是就用服務網關創建了一個 Facade,把幾個微服務封裝起來,這樣 UI 就只調用服務網關就可以了,不需要去對付每一個微服務。下面是 API Gateway 示例圖:

服務網關(API Gateway)不是爲了解決微服務之間調用的緊耦合問題,它主要是爲了簡化客戶端的工作。其實它還可以用來降低函數之間的耦合度。

有了 API Gateway 之後,一旦服務接口修改,你可能只需要修改 API Gateway, 而不必修改每個調用這個函數的客戶端,這樣就減少了程序的耦合性。

服務調用

可以借鑑 API Gateway 的思路來減少 RPC 調用的耦合度,例如把多個微服務組織起來形成一個完整功能的服務組合,並對外提供統一的服務接口。這種想法跟上面的 API Gateway 有些相似,都是把服務集中起來提供粗顆粒(Coarse Granular)服務,而不是細顆粒的服務(Fine Granular)。

但這樣建立的服務組合可能只適合一個程序使用,沒有多少共享價值。因此如果有合適的場景就採用,否側也不必強求。雖然我們不能降低 RPC 服務之間的耦合度,卻可以減少這種緊耦合帶來的影響。

降低緊耦合的影響

什麼是緊耦合的主要問題呢?就是客戶端和服務端的升級不同步。服務端總是先升級,客戶端可能有很多,如果要求它們同時升級是不現實的。它們有各自的部署時間表,一般都會選擇在下一次部署時順帶升級。

一般有兩個辦法可以解決這個問題:

同時支持多個版本:這個工作量比較大,因此大多數公司都不會採用這種方式。

服務端向後兼容:這是更通用的方式。例如你要加一個新功能或有些客戶要求給原來的函數增加一個新的參數,但別的客戶不需要這個參數。這時你只好新建一個函數,跟原來的功能差不多,只是多了一個參數。這樣新舊客戶的需求都能滿足。它的好處是向後兼容(當然這取決於你使用的協議)。

它的壞處是當以後新的客戶來了,看到兩個差不多的函數就糊塗了,不知道該用那個。而且時間越長越嚴重,你的服務端可能功能增加的不多,但相似的函數卻越來越多,無法選擇。

它的解決辦法就是使用一個支持向後兼容的 RPC 協議,現在最好的就是 Protobuf gRPC,尤其是在向後兼容上。

它給每個服務定義了一個接口,這個接口是與編程語言無關的中性接口,然後你可以用工具生成各個語言的實現代碼,供不同語言使用。函數定義的變量都有編號,變量可以是可選類型的,這樣就比較好地解決了函數兼容的問題。

就用上面的例子,當你要增加一個可選參數時,你就定義一個新的可選變量。由於它是可選的,原來的客戶端不需要提供這個參數,因此不需要修改程序。

而新的客戶端可以提供這個參數。你只要在服務端能同時處理這兩種情況就行了。這樣服務端並沒有增加新的函數,但用戶的新需求滿足了,而且還是向後兼容的。

微服務的數量有沒有上限?

總的來說微服務的數量不要太多,不然會有比較重的運維負擔。有一點需要明確的是微服務的流行不是因爲技術上的創新,而是爲了滿足管理上的需要。單體程序大了之後,各個模塊的部署時間要求不同,對服務器的優化要求也不同,而且團隊人數衆多,很難協調管理。

把程序拆分成微服務之後,每個團隊負責幾個服務,就容易管理了,而且每個團隊也可以按照自己的節奏進行創新,但它給運維帶來了巨大的麻煩。所以在微服務剛出來時,我一直覺得它是一個退步,弊大於利。但由於管理上的問題沒有其他解決方案,只有硬着頭皮上了。

值得慶幸的是微服務帶來的麻煩都是可解的。直到後來,微服務建立了全套的自動化體系,從程序集成到部署,從全鏈路跟蹤到日誌,以及服務檢測,服務發現和註冊,這樣才把微服務的工作量降了下來。

雖然微服務在技術上一無是處,但它的流行還是大大推動了容器技術,服務網格(Service Mesh)和全鏈路跟蹤等新技術的發展。不過它本身在技術上還是沒有發現任何優勢。

直到有一天,我意識到單體程序其實性能調試是很困難的(很難分離出瓶頸點),而微服務配置了全鏈路跟蹤之後,能很快找到癥結所在。看來微服務從技術來講也不全是缺點,總算也有好的地方。但微服務的顆粒度不宜過細,否則工作量還是太大。

一般規模的公司十幾個或幾十個微服務都是可以承受的,但如果有幾百個甚至上千個,那麼絕不是一般公司可以管理的。儘管現有的工具已經很齊全了,而且與微服務有關的整個流程也已經基本上全部自動化了,但它還是會增加很多工作。

Martin Fowler 幾年以前建議先從單體程序開始(詳見 MonolithFirst),然後再逐步把功能拆分出去,變成一個個的微服務。但是後來有人反對這個建議,他也有些鬆口了。

如果單體程序不是太大,這是個好主意。可以用數據額庫表的數量來衡量程序的大小,我見過大的單體程序有幾百張表,這就太多了,很難管理。正常情況下,一個微服務可以有兩、三張表到五、六張表,一般不超過十張表。但如果要減少微服務數量的話,可以把這個標準放寬到不要超過二十張表。

用這個做爲大致的指標來創建微程序,如果使用一段時間之後還是覺得太大了,那麼再逐漸拆分。當然,按照這個標準建立的服務更像是服務組合,而不是單個的微服務。不過它會爲你減少工作量。只要不影響業務部門的創新進度,這是一個不錯的方案。

到底應不應該選擇微服務呢?如果單體程序已經沒法管理了,那麼你別無選擇。如果沒有管理上的問題,那麼微服務帶給你的只有問題和麻煩。其實,一般公司都沒有太多選擇,只能採用微服務,不過你可以選擇建立比較少的微服務。如果還是沒法決定,有一個折中的方案,“內部微服務設計”。

內部微服務設計

這種設計表面上看起來是一個單體程序,它只有一個源代碼存儲倉庫,一個數據庫,一個部署,但在程序內部可以按照微服務的思想來進行設計。它可以分成多個模塊,每個模塊是一個微服務,可以由不同的團隊管理。

用這張圖做例子。這個圖裏的每個圓角方塊大致是一個微服務,但我們可以把它作爲一個單體程序來設計,內部有五個微服務。

每個模塊都有自己的數據庫表,它們都在一個數據庫中,但模塊之間不能跨數據庫訪問(不要建立模塊之間數據庫表的外鍵)。

“User”(在 Conference Management 模塊中)是一個共享的類,但在不同的模塊中的名字不同,含義和用法也不同,成員也不一樣(例如,在 “Customer Service” 裏叫“Customer”)。

DDD(Domain-Driven Design)建議不要共享這個類,而是在每一個有界上下文(模塊)中都建一個新類,並擁有新的名字。

雖然它們的數據庫中的數據應該大致相同,但 DDD 建議每一個有界上下文中都建一個新表,它們之間再進行數據同步。

這個所謂的 “內部微服務設計” 其實就是 DDD,但當時還沒有微服務,因此外表看起來是單體程序,但內部已經是微服務的設計了。

它的書在 2003 就出版了,當時就很有名。但它更偏重於業務邏輯的設計,踐行起來也比較困難,因此大家談論得很多,真正用的較少。

直到十年之後,微服務出來之後,人們發現它其實內部就是微服務,而且微服務的設計需要用它的思想來指導,於是就又重新煥發了青春,而且這次更猛,已經到了每個談論微服務的人都不得不談論 DDD 的地步。不過一本軟件書籍,在十年之後還能指導新技術的設計,非常令人欽佩。

這樣設計的好處是它是一個單體程序,省去了多個微服務帶來的部署、運維的麻煩。但它內部是按微服務設計的,如果以後要拆分成微服務會比較容易。至於什麼時候拆分不是一個技術問題。

如果負責這個單體程序的各個團隊之間不能在部署時間表,服務器優化等方面達成一致,那麼就需要拆分了。

當然你也要應對隨之而來的各種運維麻煩。內部微服務設計是一個折中的方案,如果你想試水微服務,但又不願意冒太大風險時,這是一個不錯的選擇。

結論

微服務之間的調用有兩種方式,RPC 和事件驅動。事件驅動是更好的方式,因爲它是松耦合的。但如果業務邏輯是緊耦合的,RPC 方式也是可行的(它的好處是代碼更簡單),而且你還可以通過選取合適的協議(Protobuf gRPC)來降低這種緊耦合帶來的危害。

由於事件溯源和事件通知的相似性,很多人把兩者弄混了,但它們實際上是完全不同的東西。微服務的數量不宜太多,可以先創建比較大的微服務(更像是服務組合)。

如果你還是不能確定是否採用微服務架構,可以先從 “內部微服務設計” 開始,再逐漸拆分。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/9qer4GmHFPVBofziMo90HA