微服務之間的最佳調用方式

在微服務架構中,須要調用不少服務才能完成一項功能。服務之間如何互相調用就變成微服務架構中的一個關鍵問題。服務調用有兩種方式,一種是RPC方式,另外一種是事件驅動(Event-driven)方式,也就是發消息方式。消息方式是鬆耦合方式,比緊耦合的RPC方式要優越,但RPC方式若是用在適合的場景也有它的一席之地.html

耦合的種類: 咱們總在談耦合,那麼耦合到底意味着什麼呢?數據庫

  1. 時間耦合:客戶端和服務端必須同時上線才能工做。發消息時,接受消息隊列必須運行,但後臺處理程序暫時不工做也不影響。
  2. 容量耦合:客戶端和服務端的處理容量必須匹配。發消息時,若是後臺處理能力不足也沒關係,消息隊列會起到緩衝的做用。
  3. 接口耦合:RPC調用有函數標籤,而消息隊列只是一個消息。例如買了商品以後要調用發貨服務,若是是發消息,那麼就只需發送一個商品被買消息。
  4. 發送方式耦合:RPC是點對點方式,須要知道對方是誰,它的好處是可以傳回返回值。消息既能夠點對點,也能夠用廣播的方式,這樣減小了耦合,但也使返回值比較困難。

下面咱們來逐一分析這些耦合的影響。 第一,時間耦合,對於多數應用來說,你但願能立刻獲得回答,所以即便使用消息隊列,後臺也須要一直工做。第二,容量耦合,若是你對回覆有時間要求,那麼消息隊列的緩衝功能做用不大,由於你但願及時響應。真正須要的是自動伸縮(Auto-scaling),它能自動調整服務端處理能力去匹配請求數量。第三和第四,接口耦合和發送方式耦合,這兩個確實是RPC方式的軟肋。編程

事件驅動(Event-Driven)方式:

Martin Fowler把事件驅動分紅四種方式(What do you mean by 「Event-Driven」),簡化以後本質上只有兩種方式。 一種就是咱們熟悉的的事件通知(Event Notification),另外一種是事件溯源(Event Sourcing)。事件通知就是微服務之間不直接調用,而是經過發消息來進行合做。事件溯源有點像記帳,它把全部的事件都記錄下來,做爲永久存儲層,再在它的基礎之上構建應用程序。實際上從應用的角度來說,它們並不該該分屬一類,它們的用途徹底不一樣。事件通知是微服務的調用(或集成)方式,應該和RPC分在一塊兒。事件溯源是一種存儲數據的方式,應該和數據庫分在一塊兒。api

事件通知(Event Notification)方式:

讓咱們用具體的例子來看一下。在下面的例子中,有三個微服務,「Order Service」, 「Customer Service」 和「Product Service」.服務器

file

圖片來源架構

先說讀數據,假設要建立一個「Order」,在這個過程當中須要讀取「Customer」的數據和「Product」數據。若是用事件通知的方式就只能在「Order Service」本地也建立只讀「Customer」和「Product」表,並把數據用消息的方式同步過來。併發

再說寫數據,若是在建立一個「Order」時須要建立一個新的「Customer」或要修改「Customer」的信息,那麼能夠在界面上跳轉到用戶建立頁面,而後在「Customer Service」建立用戶以後再發」用戶已建立「的消息,「Order Service」接到消息,更新本地「Customer」表。app

這並非一個很好的使用事件驅動的例子,由於事件驅動的優勢就是不一樣的程序之間能夠獨立運行,沒有綁定關係。但如今「Order Service」須要等待「Customer Service」建立完了以後才能繼續運行,來完成整個建立「Order」的工做。主要是由於「Order」和「Customer」自己從邏輯上來說就是緊耦合關係,沒有「Customer」你是不能建立「Order」的。運維

在這種緊耦合的狀況下,也可使用RPC。你能夠創建一個更高層級的管理程序來管理這些微服務之間的調用,這樣「Order Service」就沒必要直接調用「Customer Service」了。固然它從本質上來說並無解除耦合,只是把耦合轉移到了上一層,但至少如今「order Service」和「Customer Service」能夠互不影響了。之因此不能根除這種緊耦合關係是由於它們在業務上是緊耦合的。dom

再舉一個購物的例子。用戶選好商品以後進行「Checkout」,生成「Order」,而後須要「payment」,再從「Inventory」取貨,最後由「Shipment」發貨,它們每個都是微服務。這個例子用RPC方式和事件通知方式均可以完成。當用RPC方式時,由「Order」服務調用其餘幾個服務來完成整個功能。用事件通知方式時,「Checkout」服務完成以後發送「Order Placed」消息,「Payment」服務收到消息,接收用戶付款,發送「Payment received」消息。「Inventory」服務收到消息,從倉庫裏取貨,併發送「Goods fetched」消息。「Shipment」服務獲得消息,發送貨物,併發送「Goods shipped」消息。

file

圖片來源

對這個例子來說,使用事件驅動是一個不錯的選擇,由於每一個服務發消息以後它不須要任何反饋,這個消息由下一個模塊接收來完成下一步動做,時間上的要求也比上一個要寬鬆。用事件驅動的好處是下降了耦合度,壞處是你如今不能在程序裏找到整個購物過程的步驟。若是一個業務邏輯有它本身相對固定的流程和步驟,那麼使用RPC或業務流程管理(BPM)可以更方便地管理這些流程。在這種狀況下選哪一種方案呢?在我看來好處和壞處是大體至關的。從技術上來說要選事件驅動,從業務上來說要選RPC。不過如今愈來愈多的人採用事件通知做爲微服務的集成方式,它彷佛已經成了微服務之間的標椎調用方式。

事件溯源(Event Sourcing):

這是一種具備顛覆性質的的設計,它把系統中全部的數據都以事件(Event)的方式記錄下來,它的持久存儲叫Event Store, 通常是創建在數據庫或消息隊列(例如Kafka)基礎之上,並提供了對事件進行操做的接口,例如事件的讀寫和查詢。事件溯源是由領域驅動設計(Domain-Driven Design)提出來的。DDD中有一個很重要的概念,有界上下文(Bounded Context),能夠用有界上下文來劃分微服務,每一個有界上下文均可以是一個微服務。 下面是有界上下文的示例。下圖中有兩個服務「Sales」和「Support」。有界上下文的一個關鍵是如何處理共享成員, 在圖中是「Customer」和「Product」。在不一樣的有界上下文中,共享成員的含義、用法以及他們的對象屬性都會有些不一樣,DDD建議這些共享成員在各自的有界上下文中都分別建本身的類(包括數據庫表),而不是共享。能夠經過數據同步的手段來保持數據的一致性。下面還會詳細講解。

file 圖片來源

事件溯源是微服務的一種存儲方式,它是微服務的內部實現細節。所以你能夠決定哪些微服務採用事件溯源方式,哪些不採用,而沒必要全部的服務都變成事件溯源的。 一般整個應用程序只有一個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示例圖:

file

圖片來源

服務網關(API Gateway)不是爲了解決微服務之間調用的緊耦合問題,它主要是爲了簡化客戶端的工做。其實它還能夠用來下降函數之間的耦合度。 有了API Gateway以後,一旦服務接口修改,你可能只須要修改API Gateway, 而沒必要修改每一個調用這個函數的客戶端,這樣就減小了程序的耦合性。

服務調用:

能夠借鑑API Gateway的思路來減小RPC調用的耦合度,例如把多個微服務組織起來造成一個完整功能的服務組合,並對外提供統一的服務接口。這種想法跟上面的API Gateway有些類似,都是把服務集中起來提供粗顆粒(Coarse Granular)服務,而不是細顆粒的服務(Fine Granular)。但這樣創建的服務組合可能只適合一個程序使用,沒有多少共享價值。所以若是有合適的場景就採用,否側也沒必要強求。雖然咱們不能下降RPC服務之間的耦合度,卻能夠減小這種緊耦合帶來的影響。

下降緊耦合的影響:

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

通常有兩個辦法能夠解決這個問題:

  1. 同時支持多個版本:這個工做量比較大,所以大多數公司都不會採用這種方式。
  2. 服務端向後兼容:這是更通用的方式。例如你要加一個新功能或有些客戶要求給原來的函數增長一個新的參數,但別的客戶不須要這個參數。這時你只好新建一個函數,跟原來的功能差很少,只是多了一個參數。這樣新舊客戶的需求都能知足。它的好處是向後兼容(固然這取決於你使用的協議)。它的壞處是當之後新的客戶來了,看到兩個差很少的函數就糊塗了,不知道該用那個。並且時間越長越嚴重,你的服務端可能功能增長的很少,但類似的函數卻愈來愈多,沒法選擇。

它的解決辦法就是使用一個支持向後兼容的RPC協議,如今最好的就是Protobuf+gRPC,尤爲是在向後兼容上。它給每一個服務定義了一個接口,這個接口是與編程語言無關的中性接口,而後你能夠用工具生成各個語言的實現代碼,供不一樣語言使用。函數定義的變量都有編號,變量能夠是可選類型的,這樣就比較好地解決了函數兼容的問題。就用上面的例子,當你要增長一個可選參數時,你就定義一個新的可選變量。因爲它是可選的,原來的客戶端不須要提供這個參數,所以不須要修改程序。而新的客戶端能夠提供這個參數。你只要在服務端能同時處理這兩種狀況就好了。這樣服務端並無增長新的函數,但用戶的新需求知足了,並且仍是向後兼容的。

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

總的來講微服務的數量不要太多,否則會有比較重的運維負擔。有一點須要明確的是微服務的流行不是由於技術上的創新,而是爲了知足管理上的須要。單體程序大了以後,各個模塊的部署時間要求不一樣,對服務器的優化要求也不一樣,並且團隊人數衆多,很難協調管理。把程序拆分紅微服務以後,每一個團隊負責幾個服務,就容易管理了,並且每一個團隊也能夠按照本身的節奏進行創新,但它給運維帶來了巨大的麻煩。因此在微服務剛出來時,我一直以爲它是一個退步,弊大於利。但因爲管理上的問題沒有其餘解決方案,只有硬着頭皮上了。值得慶幸的是微服務帶來的麻煩都是可解的。直到後來,微服務創建了全套的自動化體系,從程序集成到部署,從全鏈路跟蹤到日誌,以及服務檢測,服務發現和註冊,這樣才把微服務的工做量降了下來。雖然微服務在技術上一無可取,但它的流行仍是大大推進了容器技術,服務網格(Service Mesh)和全鏈路跟蹤等新技術的發展。不過它自己在技術上仍是沒有發現任何優點。。直到有一天,我意識到單體程序其實性能調試是很困難的(很難分離出瓶頸點),而微服務配置了全鏈路跟蹤以後,能很快找到癥結所在。看來微服務從技術來說也不全是缺點,總算也有好的地方。但微服務的顆粒度不宜過細,不然工做量仍是太大。

通常規模的公司十幾個或幾十個微服務都是能夠承受的,但若是有幾百個甚至上千個,那麼毫不是通常公司能夠管理的。儘管現有的工具已經很齊全了,並且與微服務有關的整個流程也已經基本上所有自動化了,但它仍是會增長不少工做。Martin Fowler幾年之前建議先從單體程序開始(詳見 MonolithFirst),而後再逐步把功能拆分出去,變成一個個的微服務。可是後來有人反對這個建議,他也有些鬆口了。若是單體程序不是太大,這是個好主意。能夠用數據額庫表的數量來衡量程序的大小,我見過大的單體程序有幾百張表,這就太多了,很難管理。正常狀況下,一個微服務能夠有兩、三張表到5、六張表,通常不超過十張表。但若是要減小微服務數量的話,能夠把這個標準放寬到不要超過二十張表。用這個作爲大體的指標來建立微程序,若是使用一段時間以後仍是以爲太大了,那麼再逐漸拆分。固然,按照這個標準創建的服務更像是服務組合,而不是單個的微服務。不過它會爲你減小工做量。只要不影響業務部門的創新進度,這是一個不錯的方案。

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

內部微服務設計:

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

file

圖片來源

用這張圖作例子。這個圖裏的每一個圓角方塊大體是一個微服務,但咱們能夠把它做爲一個單體程序來設計,內部有五個微服務。每一個模塊都有本身的數據庫表,它們都在一個數據庫中,但模塊之間不能跨數據庫訪問(不要創建模塊之間數據庫表的外鍵)。「User」(在Conference Management模塊中)是一個共享的類,但在不一樣的模塊中的名字不一樣,含義和用法也不一樣,成員也不同(例如,在「Customer Service」裏叫「Customer」)。DDD(Domain-Driven Design)建議不要共享這個類,而是在每個有界上下文(模塊)中都建一個新類,並擁有新的名字。雖然它們的數據庫中的數據應該大體相同,但DDD建議每個有界上下文中都建一個新表,它們之間再進行數據同步。

這個所謂的「內部微服務設計」其實就是DDD,但當時尚未微服務,所以外表看起來是單體程序,但內部已是微服務的設計了。它的書在2003就出版了,當時就頗有名。但它更偏重於業務邏輯的設計,踐行起來也比較困難,所以你們談論得不少,真正用的較少。直到十年以後,微服務出來以後,人們發現它其實內部就是微服務,並且微服務的設計須要用它的思想來指導,因而就又從新煥發了青春,並且此次更猛,已經到了每一個談論微服務的人都不得不談論DDD的地步。不過一本軟件書籍,在十年以後還能指導新技術的設計,很是使人欽佩。

這樣設計的好處是它是一個單體程序,省去了多個微服務帶來的部署、運維的麻煩。但它內部是按微服務設計的,若是之後要拆分紅微服務會比較容易。至於何時拆分不是一個技術問題。若是負責這個單體程序的各個團隊之間不能在部署時間表,服務器優化等方面達成一致,那麼就須要拆分了。固然你也要應對隨之而來的各類運維麻煩。內部微服務設計是一個折中的方案,若是你想試水微服務,但又不肯意冒太大風險時,這是一個不錯的選擇。

結論:

微服務之間的調用有兩種方式,RPC和事件驅動。事件驅動是更好的方式,由於它是鬆耦合的。但若是業務邏輯是緊耦合的,RPC方式也是可行的(它的好處是代碼更簡單),並且你還能夠經過選取合適的協議(Protobuf+gRPC)來下降這種緊耦合帶來的危害。因爲事件溯源和事件通知的類似性,不少人把二者弄混了,但它們其實是徹底不一樣的東西。微服務的數量不宜太多,能夠先建立比較大的微服務(更像是服務組合)。若是你仍是不能肯定是否採用微服務架構,能夠先從「內部微服務設計」開始,再逐漸拆分。

索引:

1 What do you mean by 「Event-Driven」

[2] Domain-Driven Design

[3] BoundedContext

[4] Domain Events vs. Event Sourcing

[5] Using Kafka as a (CQRS) Eventstore. Good idea?

[6] Evenstore

[7] MonolithFirst

相關文章
相關標籤/搜索