是(shì)時候改變你對微服務的認知(zhī)了!

2018-02-28 16:49:05 ortotra

大部分時候,微服務都是(shì)建立在一種基于請求和響應的協議(yì)之上。比如,REST等。這種方式是(shì)自然的。我們隻需要調用另外一個模塊就是(shì)了,然後等待響應返回,然後繼續。這樣的方式确實也滿足了我們的很多的場景:用戶通過點擊頁面的一個按鈕然後希望發生一些事情。


但(dàn)是(shì),當我們開始接觸許多獨立的service的時候,事情就發生改變了。随着service數量急速的增長,同步交互比例也随着service在急速增長。這時候,我們的service就會遇到很多的瓶頸。

于是(shì),不幸的ops工程師們就被我們坑了,他們疲憊的奔波于一個又(yòu)一個的service,拼湊在一起的二手信息片段,誰說了什麽,去(qù)往哪裏,什麽時候發生?等等。。。

這是(shì)一個非常典型的問題。市面上也有一些解決方案。一種方案就是(shì)确保您的個人服務具有比您的系統更高的SLA。 Google提供了這樣做的協議(yì)。另一種方法是(shì)簡單地分解将服務綁定在一起的同步關系。


上面的做法都沒有從模式上根本解決問題。我們可以使用異步機制來解決這個問題。比如,電商網站中你會發現(xiàn)這樣的同步接口,比如getImage()或者processOrder(),也許你感覺蠻正常。調用了然後希望馬上有一個響應。但(dàn)當用戶點擊了“購買”後,觸發了一個複雜(zá)且異步的處理過程。這個過程涉及到購買、送貨上門給用戶,這一切都是(shì)發生在當初的那一次的按鈕點擊。所以把一個程序處理邏輯切分成多個異步的處理,是(shì)我們需要解決的問題。這也正符合我們的真實的世界,真實世界本來就是(shì)異步的,擁抱異步吧。


在實際情況下(xià),我們其實已經自動擁抱了異步了。我們發現(xiàn)自己會定時輪詢數據庫表來更改又(yòu)或者通過cron定時job來實現(xiàn)一些更新。這些方法都是(shì)一些打破同步的方式,但(dàn)是(shì)這種做法總讓人感覺有種黑客範兒,感覺像是(shì)黑客行爲,怪怪的。


在本文中,我們将會讨論一種完全不同的架構:不是(shì)把service們通過命令鏈揉到一塊,而是(shì)通過事件流(stream of events)來做。這是(shì)一個不錯的方式。這種方式也是(shì)我們之後要讨論的一系列的一個基礎。


當我們進入正式的例子之前,我們需要先普及三個簡單的概念。一個service與另外一個service有三種交互方式:命令(Commands)、事件(Events)以及查詢(Queries)。


事件的美妙之處在于“外部數據”可以被系統中的任何service所重用。


而且從service的角度來說,事件要比命令和查詢都要解耦。這個很重要。


服務之間的交互有三種機制:


Commands 。命令是(shì)一個操作。希望在另一個服務中執行某些操作的一個請求。 會改變系統狀态的東西。 命令期待有響應。

Events 。事件既是(shì)一個事實也是(shì)一個觸發器。 發生了一些事情,表示爲通知(zhī)。

Queries 。查詢是(shì)一個請求,是(shì)一個查找一些東西的請求(request)。重要的是(shì),查詢不會使得系統狀态發生改變。


一個簡單事件驅動流程


讓我們開始一個簡單的例子:用戶購買一個小東西。那麽接下(xià)來要發生兩件事情:


支付。

系統檢查是(shì)否還有更多的商品需要被訂購。

在請求驅動(request-approach)的架構中,這兩個行爲被表現(xiàn)爲一個命令鏈條。交互就像下(xià)面這樣:


首先要注意的問題是(shì)“購買更多”的這個業務流程是(shì)随着訂單服務(Order Service)一塊被初始化的。這就使得責任不獨立,責任跨了兩個service。理想情況下(xià),我們希望separation of concerns,也就是(shì)關注隔離(lí)。


現(xiàn)在如果我們使用事件驅動,而不是(shì)請求驅動的方式的話(huà),那麽事情就會變得好一些。


在返回給用戶之前,UI service 發布一個OrderRequested事件,然後等待OrderConfirmed(或者Rejected)。

訂單服務(Orders Service)和庫存服務(Stock Service) react這個事件。


仔細看這裏,UI service和Orders Service并沒有改變很多,而是(shì)通過事件來通信,而不是(shì)直接調用另一個。


這個Stock service(庫存服務)很有趣。Order Service告訴他要做什麽。然後StockService自己決定是(shì)否參與本次交互,這是(shì)事件驅動架構非常重要的屬性,也就是(shì):Reciver Driven Flow Control,接收者驅動流程控制。一下(xià)子控制反轉了。


這種控制反轉給接收者,很好的解耦了服務之間的交互,這就爲架構提供了可插拔性。組件們可以輕松的被插入和替換掉,優雅!


随着架構變得越來越複雜(zá),這種可插拔性的因素變得更加重要。舉個例子,我們要添加一個實時管理定價的service,根據供需調整産品的價格。在一個命令驅動的世界裏,我們就需要引入一個可以由庫存服務(Stock Service)和訂單服務(Orders Service)調用的類似updatePrice()這樣的方法。


但(dàn)是(shì)在事件驅動(event-driven)世界更新價格的話(huà),service隻需要訂閱共享的stream就是(shì)了,當相(xiàng)應的條件符合時,就去(qù)執行更新價格的操作。


事件(Events)和查詢(Queries)的混合


上面的例子隻是(shì)命令和事件。并沒有說到查詢。别忘了,我們之前可是(shì)說到了三個概念。現(xiàn)在我們開始說查詢。我們擴展上面的例子,讓訂單服務(Orders Service)在支付之前檢查是(shì)否有足夠的庫存。


在請求驅動(request-driven)的架構中,我們可能會向庫存服務(Stock Service)發送一個查詢請求然後獲取到當前的庫存數量。這就導緻了模型混合,事件流純粹被用作通知(zhī),允許任何的service加入flow,但(dàn)查詢卻是(shì)通過請求驅動的方式直接訪問源。


對于服務(service)需要獨立發展的較大的生态系統,遠程查詢要涉及到很多關聯,耦合很嚴重,要把很多服務捆綁在一起。我們可以通過“内部化”來避免這種涉及多個上下(xià)文交叉的查詢。而事件流可以被用于在每個service中緩存數據集,這樣我們就可以在本地來完成查詢。


所以,增加這個庫存檢查,訂單服務(Order Service)可以訂閱庫存服務(Stock Service)的事件流,庫存一有更新,訂單服務就會收到通知(zhī),然後把更新存儲到本地的數據庫。這樣接下(xià)來就可以查詢本地這個“視圖(view)”來檢查是(shì)否有足夠的庫存。


純事件驅動系統沒有遠程查詢的概念 - 事件将狀态傳播到本地查詢的服務


通過事件來傳播( “Queryby Event Propagation”)的查詢有以下(xià)三個好處:


1、更好的解耦:在本地查詢。這樣就不涉及跨上下(xià)文調用了。這種做法涉及到的服務們遠遠不及那種”請求驅動”所涉及到的服務數量多。

2、更好的自治:訂單服務(Order Service)擁有一份庫存數據集的copy,所以訂單服務可以任意使用這個本地的數據集,

而不是(shì)說像請求驅動裏的那樣僅僅隻能檢查庫存限額,而且隻能通過Stock Service所提供的接口。

3、高效Join:如果我們在每次下(xià)訂單的時候都要去(qù)查詢庫存,就要求每次都要高效的做join,通過跨網絡對兩個service進行join。随着需求的增加,或者更多的數據源需要關聯,這可能會變得越來越艱巨。所以通過事件傳播來查詢(Query by Event Propagation)将查詢(和join)本地化後就可以解決這個問題(就是(shì)本地查詢)。


但(dàn)這種做法也不是(shì)沒有缺點。 Service從本質上變得有狀态了。這樣就使得他們需要被跟蹤和矯正這些數據集,随着時間的推移,也就是(shì)你得保證數據同步。狀态的重複也可能使一些問題更難理解(比如如何原子地減少庫存數量?),這些問題我們都要小心。但(dàn)是(shì),所有這些問題都有可行的解決方案,我們隻是(shì)需要多一點考慮而已。 


單一寫入者原則(Single Writer Principle)


針對這種風格的系統,也就是(shì)事件驅動風格的系統,一個非常有用的原則就是(shì)針對指定類型的傳播的事件分配責任的時候,應該隻分配給一個單一的service:單一的寫入者。什麽意思呢?就是(shì)Stock Service隻應該處理庫存這一件事情,而Order Service也隻屬于訂單們,等等。


這樣的話(huà)有助于我們通過單個代碼路徑(盡管不一定是(shì)單個進程)來排除一緻性,驗證和其他“寫入路徑(writepath)”問題。因此,在下(xià)面的示例中,請注意,訂單服務(Order Service)控制着對訂單進行的每個狀态的更改,但(dàn)整個事件流跨越了訂單(Orders),付款(Payments)和發貨(Shipments),每個都由它們各自的服務來管理。


分配“事件傳播”(event propagation)的責任很重要,因爲這些不僅僅是(shì)短暫的事件,或者是(shì)那種無須保存短暫的聊天。他們代表了共同的事實(facts),以及“數據在外部(data-on-the-outside)“。因此,随着時間的推移,服務(services)需要去(qù)負責更新和同步這些共享數據集(shared datasets):比如,修複錯誤,處理schema的變化等情況。


上圖中每個顔色代表Kafka的一個topic,針對下(xià)訂單(Order)、發貨和付款。  當用戶點擊“購買”時,會引發“Order Requested”,等待“Order Confirmed”事件,然後再回複給用戶。 另外三個服務處理與其工作流程部分相(xiàng)關的狀态轉換。 例如,付款處理完成後,訂單服務(Order Service)将訂單從“已驗證(Validated)”推送到“已确認(Confirmed)”。


模式(Patterns)和集群服務(Clustering Services)的混合


上面的說到的模型有點像企業消息(Enterprise Messaging),但(dàn)其實是(shì)有一些不同的。企業消息,在實踐中,主要關注狀态的轉換,通過網絡有效地将數據庫捆綁在一起。


而事件協作(Event Collaboration)則更偏重的是(shì)協作,既然是(shì)協作就不簡單的是(shì)狀态轉換,事件協作是(shì)關于服務(service)通過一系列事件進行一些業務目标,這些事件将觸發service的執行。所以這是(shì)業務處理(business processing)的一種模式,而不是(shì)簡單的轉換狀态的機制。


我們通常希望在我們構建的系統中這種模式具有兩面性。事實上,這種模式的美妙之處在于它确實既可以處理微觀又(yòu)可以處理宏觀,或者在有些情況下(xià)可以被混合。


模式組合使用也很常見(jiàn)。我們可能希望提供遠程查詢的方便靈活性,而不是(shì)本地維護數據集的成本,特别是(shì)數據集增長時。這樣的話(huà)就會讓我們的查詢變得更加的簡單,我們隻需要輕松部署簡單的函數就可以了。而且我們現(xiàn)在很多都是(shì)無狀态的,比如容器或者浏覽器,在這種情況下(xià)也許遠程查詢是(shì)一種合适的選擇。


遠程查詢設計的訣竅就是(shì)限制這些查詢接口的範圍,理想情況下(xià)應該是(shì)在有限的上下(xià)文中(context)。通常情況下(xià),建立一個具有多個特定,具體視圖的架構,而不是(shì)單一的共享數據存儲。注意是(shì)多個具體的視圖,而不是(shì)單一的共享數據存儲。(一個獨立(bounded)的上下(xià)文,或者說是(shì)偏向原子,這裏說的原子不是(shì)側重微服務中常說的那個“原子服務”。獨立上下(xià)文,一般是(shì)指有那麽一組service,它們共享同一個發布流水線(xiàn)或者是(shì)同一個領域模型【domain model】)。


爲了限制遠程查詢(remote queries)的邊界(scope),我們可以使用一種叫做“集群式上下(xià)文模式(clustered context pattern)”。這種情況下(xià),事件就流純粹是(shì)用作上下(xià)文之間的通信。但(dàn)在一個上下(xià)文裏的具體service們則可以既有事件驅動(event-driven)的處理,同時也有請求驅動(request-driven)的視圖(view),具體根據實際情況需要。


在下(xià)面的例子中,我們有三個部分,三個之間隻通過事件相(xiàng)互溝通。在每一個内部,我們使用了更細粒度的事件驅動流。其中一些包括視圖層(查詢層)。


還是(shì)看下(xià)圖吧:


集群上下(xià)文模型(Clustered Context Model)


事件驅動(event-driven)五個關鍵好處:

 解耦:把一個很長的同步執行鏈的命令給分解,異步化。 分解同步工作流。 Brokers 或topic解耦服務(service),所以更容易插入新的服務(service),具有更強的插拔性。

離(lí)線(xiàn)/異步流:當用戶點擊按鈕時,很多事情都會發生。 一些同步,一些異步。 對能力的設計,無論是(shì)以前的,還是(shì)将來的,都是(shì)更自由的。提高了性能,提高了自由度。

狀态同步更新:事件流對分布式數據集提供了一種有效的機制,數據集可以在一個有界的上下(xià)文裏被重構(“傳播”或“更新”)和查詢。

 Joins:從不同的服務(service)組合/join/擴展數據集更容易。 join更快速,而且還是(shì)本地化的。

可追溯性: 當有一個統一化的,中心化的,不可變的,保持性的地方來記錄每個互動時,它會及時展現(xiàn),debug的時候也更容易定位問題,而不是(shì)陷入一場關于“分布式”的謀殺。(這裏有點晦澀)

總結


Ok,在事件驅動的方法中我們使用事件(Events)而不是(shì)命令(Commands)。事件觸發業務處理過程。事件也可以用到更新本地視圖上。然後我們向你介紹了,在必要時,我們可以再回到遠程同步查詢這種方式,特别是(shì)在較小的系統中,而且我們還将遠程同步查詢的範圍擴大到更大的範圍(理想情況下(xià),還是(shì)要僅限于單個獨立的上下(xià)文,也就是(shì)單個領域模型,不能再擴大了,剛剛好才是(shì)真的好)。


而且所有這些方法都隻是(shì)模式(pattern)。模式就會有框得太死的問題。模式覆蓋不到的地方,我們就要具體情況具體對待了。例如,單點登錄服務,全局查詢的service仍然是(shì)一個好主意,因爲它很少更新。


這裏的秘訣就是(shì)從事件的基準出發去(qù)考慮問題。事件讓服務之間不再耦合,并且将控制(flow-control)權轉移到接收者,這就有了更好的“分離(lí)關注(separated concerns)”和更好的可插拔性。


關于事件驅動方法的另一個有趣的事情是(shì),它們對于大型,複雜(zá)的架構同樣适用,就像它們對于小型,高度協作的架構一樣。事件讓service們可以自主的決定自己的所有事情,爲服務們提供自由發展所需的自主權。


然後我們向你介紹了事件和查詢混合的場景。說到查詢,在純事件驅動方法中,查詢完全基于本地的數據集,而沒有遠程查詢。本地數據集則是(shì)通過事件觸發來更新狀态。然而,很多時候,基于請求驅動的查詢方式在很多時候也是(shì)比較方便的,因爲本地數據集的方式,狀态的同步更新确實是(shì)一件更加需要成本的事情。


然後我們說到了單一寫入z者原則。單一寫入者讓我們數據更新有了統一的入口,有助于我們通過單個代碼路徑(盡管不一定是(shì)單個進程)來排除一緻性,驗證和其他“寫入路徑(writepath)”問題。


然後我們讨論了集群上下(xià)文模型。每個領域模型組成一個獨立的區域,然後再由多個區域共同組成一個領域模型集群,模型之間又(yòu)通過Kafka來交互。每個領域模型裏又(yòu)可以包含幾種模式的混合,比如Events、Views、UI,這些裏邊可以既有事件驅動模式,又(yòu)有請求驅動模式。


大體就這麽多。


感謝Antony Stubbs,Tim Berglund,Kaufman Ng,GwenShapira和Jay Kreps,他們幫助我們回顧了這篇文章。


譯者曰:最近也恰好在做有關事件流的内容,對本文中講到的異步解耦和拆解同步請求鏈條過長問題深有感觸,也非常認同。另外最近有人聊到有關數據庫查詢效率問題,通過閱讀本文也許會讓你對查詢有一個全新的認識。這些微服務理念看起來好像專屬于“微服務”,好像其他人就不需要了解一樣。其實也許微服務的這些先進理念就像其他任何的先進的架構理念一樣,他們都是(shì)我們軟件架構知(zhī)識體系的儲備之一,也許在哪天你正在進行的項目遇到了瓶頸,沒準本文讨論的這些内容就能派上用場了,不僅僅限于本文舉的那個例子。


微服務"交互方式"觀念轉變:


 是(shì)時候更新一下(xià)你對于構建微服務的一些知(zhī)識體系了。如果你認爲REST就是(shì)微服務構建的主要交互方式的話(huà),那麽也許你錯了;如果你認爲rpc就是(shì)構建微服務的的主要交互方式的話(huà),那麽也許你又(yòu)錯了。


因爲這兩種都屬于一種類型,那就是(shì)他們都屬于請求驅動(request-driven)模式,而這種模式很多時候是(shì)同步的,一條鏈上挂了很多的服務調用,勢必在鏈條變長後,性能堪憂。


本文向你推薦了一個構建微服務的新的工具,或者說是(shì)向你補充了。那就是(shì)事件驅動(event-driven)的模式。它解耦、異步,帶來了更好的擴展性和性能。很多時候,同步會讓事情變得異常糟糕!


如果以後有人和讨論起微服務的模式的時候,你可以說REST、rpc(請求驅動)以及事件驅動共同混合使用才會構建出更好的微服務來!


ps:文中部分段落翻譯用詞略顯晦澀,我曾嘗試用大白話(huà)來翻譯,但(dàn)發現(xiàn)會損失原意,故請仔細斟酌消化。