按照Enterprise Integration Pattern搭建服務系統


  在前一篇文章中,我們已經對Enterprise Integration Pattern中所包含的各個組成進行了簡單地介紹。限於篇幅(20頁Word以內),我並沒有深入地討論各個組成。但是如果要真正地按照Enterprise Integration Pattern搭建一個系統,僅僅是了解它們實際上還差得很遠。因此在本文中,我將會對Enterprise Integration Pattern中較容易產生混淆的部分以及一些系統搭建時常常使用的一些方法進行介紹。

 

尋找最優的解決方案

  相信您在讀前一篇文章時就已經能感覺到,在使用Enterprise Integration Pattern搭建一個系統時,我們常常可以通過不同的組成來滿足類似的需求。例如在需要對消息進行轉換時,我們常常可以使用Content Enricher或Content Filter等組成來添減內容,更可以使用Massaging Mapper等組成來完成類似的功能。那么我們應該什么時候使用Content Enricher或Content Filter,而什么時候使用Massaging Mapper呢?

  其實答案就存在於過濾器和Endpoint之間的不同。兩者之間的不同主要在於,過濾器是Pipes and Filters模型之中的一個獨立的過濾器,而Endpoint則是過濾器中的一個用來令過濾器內部的業務邏輯實現與消息系統關聯的組成。例如我們有一個應用提供了一系列與消息系統不兼容的API。此時我們就需要使用一個Endpoint將其與消息系統關聯。而它們則共同組成了一個過濾器:

  在了解了這點不同之后,相信您就會明白,為什么Endpoint中所介紹的那些功能與很多用來完成消息路由及轉化的過濾器類似了:一類是單獨存在的過濾器,一類則是過濾器中的組成。相較於使用一個獨立的過濾器,Endpoint能夠減少一次在管道中傳輸數據的消耗,從而提高了消息的處理速度:

  如上圖所示,相較於經由過濾器處理,一個基於Endpoint的具有相同功能的組成可以減少一次消息通過管道進行傳遞的過程。因此基於Endpoint的解決方案擁有更好的性能。反過來,由於Endpoint是與其后的業務邏輯處於同一個過濾器中的,因此其無法將消息發送到其它子系統之中。也就是說,其靈活性有所下降。甚至說,每次對Endpoint之后的業務邏輯的更新同樣需要對Endpoint進行維護,以保證其能正常工作。

  這就引出來了一個話題,在開發一個基於Enterprise Integration Pattern的系統,最需要考慮的是什么?

  通常情況下,這些考慮的因素主要有:性能,靈活度,可維護性,高可用性。而最終的解決方案則常常是這些因素相互平衡的產物。

  性能不必多說。在一個基於消息的系統中,對用戶請求的處理最終會轉化為一系列在管道中傳遞的消息。由於消息的傳遞是一個異步操作,因此對單個消息處理時的性能不會比同步操作更好。

  第一個使系統具有合適性能的前提就是使子系統擁有合適的粒度。如果子系統的粒度較小,那么對一個業務邏輯的處理就需要經由更多的子系統。這既增加了管道的數量,又增加了消息在管道中傳輸的次數。除此之外,子系統粒度過細也會給消息系統帶來很大的壓力。在前一篇文章中已經提到過,管道會將消息保存起來,因此每個管道都會占用一部分內存。如果子系統的粒度過細,那么整個系統就需要更多的管道,對消息系統所在的服務器造成更大的壓力,也會提高系統出錯的可能。

  一個擁有較粗粒度的系統最常出現的問題就是系統過載。這時我們該怎么做呢?答案就是對其進行橫向擴展。在《服務的擴展性》一文中我們已經提到過,一個服務的擴展方式分為XYZ軸之分。而在處理系統過載問題上,我們常常需要執行X軸擴展,有時也需要進行Y軸擴展:

  X軸擴展相對簡單:使用多個服務實例對消息進行處理。在這些服務之前,我們可以使用Message Dispatcher或Competing Consumers這類Endpoint,或者通過Dynamic Router等過濾器來完成對消息的分發。只是有時通過X軸擴展並不能完全地解決問題。就像上圖顯示的那樣,對整個子系統進行擴展實際上會導致系統的某些組成利用率非常低。在這種時候,我們就可以將這個過載的子系統中的各個組成分離出來,並作為一個獨立的基於消息的子系統,然后對其真正形成瓶頸的子系統進行擴展:

  上圖中展示了如何在一個子系統實例遇到瓶頸的時候執行Y軸擴展。在一開始,該子系統是作為一個獨立的系統存在的。在需要處理一個消息的時候,消息從其輸入管道流入,並在處理完畢后從其輸出管道流出。但是在該子系統成為整個系統的瓶頸時,我們就需要將該子系統分割為多個粒度稍小的子系統,並對其中成為瓶頸的子系統添加新的實例。這樣,我們就解決了整個系統中的瓶頸。

  但是這樣做的壞處則在於,一個消息常常需要經過更多的管道才能被處理完畢。就以上圖所展示的子系統分割邏輯為例,可以看到,一個消息在被新系統處理時將首先需要經過綠色的結點,接下來還至少需要經過橘紅色的結點。也就是說,對消息的處理至少多出了消息流經一個管道的時間。因此在分割一個對消息處理時間要求較高的系統時,我們常常需要考慮的是,如何使消息經過較少的管道。

  而在對消息處理吞吐量的要求超過對消息處理時間的要求時,我們則需要盡量地使每個實例最大程度地發揮它的處理能力。這是常識,所以我們不再深入討論。

  在考慮系統性能時,我們也常常需要考慮這樣一點,那就是有些消息系統提供了創建於內存中的管道。在該管道中傳遞消息的性能要比經由網絡傳遞消息的性能高出很多。因此在設計基於消息系統的服務時,我們應盡可能地使用這種存在於內存中的管道。

  而從物理結構上來看,從原有子系統中所分割出來的各個子系統實例也需要和消息系統服務所在的實例進行交互。我們當然可以將管道直接添加到原有的消息系統服務上,而另一個較為常見的方法則是創建一個新的消息系統服務。這可以帶來非常多的好處:使消息系統服務能夠承擔更多的負載,使得整個系統的物理拓撲邏輯變得更為清晰,更可以在不同的消息服務上使用不同的安全配置,例如將其設置為只接受從中心消息服務所發送的消息,進而提高整個系統的安全性:

  而一個與性能常常有沖突的地方就是系統的靈活度。我們可以回想一下前一篇文章中對Content-Based Router及Dynamic Router的講解。兩者之間的不同主要在於Dynamic Router可以令一個過濾器注冊自身所能接收消息的條件,從而能夠動態地加入或離開系統。而這也並非沒有代價。為了提供這種靈活度,我們需要添加額外的管道,從而為消息服務帶來了更多的負載。

  但是過多的靈活性反而也會導致問題的大量出現。例如對於某些服務,我們可以假設其可能會由於業務的快速增長而達到系統的瓶頸,因此為其設計較強的靈活性是有必要的。而對於某些系統,我們常常不應該設計有太大的靈活性。例如在當前需求僅僅是針對用戶類型來提供相應推薦的推薦系統而言,當天的推薦實際上是固定的一系列推薦項的組合,因此也不存在什么需要根據用戶偏好動態計算推薦項的功能。對於這樣的一個服務,只要需求沒有發生變化,整個系統的計算負載也不會高,因此也不必為它的處理能力擴展留下太多的靈活性。

  而在需求變化時,例如我們現在需要根據用戶的瀏覽記錄來推薦物品,那么我們就需要考慮系統的靈活性了。因為此時推薦系統的計算結果可能會隨着用戶的當前瀏覽而隨時發生變化。隨着用戶的快速增多,這種負載將會越來越重,從而造成子系統的過載。

  況且,過多的靈活性也會導致可維護性變得更為困難。還是以Content-Based Router以及Dynamic Router為例。Dynamic Router之所以出現,就是因為當其中一個參與消息處理的子系統發生變化時,我們還需要更改Content-Based Router。這會導致Content-Based Router之后的整個子系統暫時不可用。

  這實際上就對參與消息系統中的各個組成之間的耦合性提出了要求。如果對某個組成的修改會導致我們更改其它一些組成,那么它們之間就是耦合的。對於不常變動的組成關系,這種耦合是正常的,而對於常常會發生變化的組成,尤其是在為整個系統設計高可用性,熱插拔功能時,這些耦合就是相對致命的。

  可以這么說,對於一個重要的系統,如何讓它在發生變化時不需要停止服務常常是其所最為看重的。高可用性是其中的一種需求,在維護時不需要停止服務也是一種非常重要的需求。因此在設計一個系統時,我們常常考慮的是:哪里可能會經常發生變化?發生變化之后我們需要更改哪些組成?如果系統的某個相關組成失效,整個系統是否能夠繼續正常提供服務?除此之外,是否有不必要的消息傳遞?

 

選擇合適的組成

  好。上一節我們已經介紹了在設計一個基於消息的系統時所需要考慮的各種因素。而在本節中,我們將對Enterprise Integration Pattern中所介紹的一些組成進行分析,從而使您更清楚地了解這些組成之間的優點和缺點,並最終能夠正確地使用它們。

  就像Open-Close原則一樣,我們在基於Enterprise Integration Pattern設計一個系統的時候也需要考慮這些系統中各個子系統之間的變與不變。變在這里主要分為兩種:系統中的各個子系統之間的關系發生變化,以及路由過程中消息自身的路由方式發生變化。搞清系統中的變與不變能夠提供較高的靈活性和可維護性。但是由於靈活性和可維護性常常需要引入一系列額外的組成,因此其常常會影響整個系統的性能。因此除了需要考慮整個系統的性能之外,我們還需要考慮各個組成的性能。在這兩種思考方式下,消息系統各個組成之間的異同就會顯得十分清晰明了。

  這些額外引入的組成常常意味着性能的下降以及維護成本的增加。例如就以使用一個Content-Based Router完成消息的分發這種最為簡單的情況為例,它的好處就是能讓我們把所有的路由邏輯都集中在一個組成中完成。這樣只要消息中的數據發生了變化,或者有新的子系統添加到路由邏輯之中,我們只需要更改這些路由邏輯即可。這便是集中管理信息的好處,或者是SRP(笑,Single Responsibility Principle,思想類似,隨便扯扯)。但是反過來,如果一個子系統所能接收的消息類型發生了變化,那么我們就需要同時修改該子系統以及相應的路由器。而這就是一種並不受待見的耦合,尤其是在一個接收端會經常發生變化的系統中,這種變化所帶來的困擾遠大於我們集中管理信息所帶來的好處。

  反過來,很多消息系統也同樣允許我們創建自定義的各個組成,例如自定義的路由器,自定義的消息轉換邏輯等。在這些情況下,我們也可以通過一系列業界常用的思想來解決這些問題。

  就讓我們從最先介紹的Content-Based Router說起。一個Content-Based Router會根據消息中所包含的信息來決定到底由哪個子系統對該消息進行處理。這也就是說,Content-Based Router知道到底有哪些子系統,同時它還知道如何去分析這些消息。那么一旦這些處理消息的子系統的可見性發生了變化,或者消息中所包含的信息發生了變化,那么我們就需要對Content-Based Router內的邏輯進行更改。

  而為了避免這些維護上的問題,Enterprise Integration Pattern提出了Dynamic Router。其允許各個接收消息的子系統向其注冊處理問題的條件。那是不是Content-Based Router就沒有任何價值了呢?不是的。相對於Dynamic Router,Content-Based Router是一個更輕量級的解決方案。因此在篩選條件不會發生變化而且參與消息分發的子系統不會發生變化的情況下,其反而是最佳的解決方案。除此之外,如何避免參與分發的各個子系統向Dynamic Router所注冊的條件不會發生彼此相互重疊的情況也是一個需要討論的問題。這也是Dynamic Router的這種靈活性所帶來的副作用。

  你仔細想一想就會發現,實際上這就是一個依賴注入。只不過我們不是在具體編程過程中對其進行使用,而是在整個系統設計時候完成的。所注入的,則是Dynamic Router所需要的作為消息分發依據的邏輯。同樣的,Service Locator也會幫我們解決一系列耦合的問題。在Enterprise Integration Pattern中,典型的借鑒Service Locator的組成似乎並不多,但是在實際使用中,我們也的確可以通過這樣設計系統來完成各個系統之間的解耦。

  我說的意思實際是,雖然說不同層次上所常用的各種方法會存在着一些不同,例如我們無法像創建一個派生類一樣對子系統進行派生,但是很多時候思想是通用的。

  OK,這段扯得有點遠。我們拉回到如何區分並合理地使用各個組成這樣一個話題中。我們在前面已經講解過什么時候使用過濾器,什么時候使用Endpoint。因此在這里我們將會把精力主要集中在負責路由的各個過濾器上。因為這常常是很多人產生疑惑的地方。

  在Enterprise Integration Pattern一書中列入了如下的一個用來決定一個系統中所需要使用的路由器的判斷邏輯圖:

  但是我個人認為這個圖是根據各個路由過濾器的特性來去分類的。而在我實際決策過程中,我更趨向於根據業務邏輯以及消息處理本身的需求來決定到底使用哪個路由器。該判斷邏輯如下所示:

  因為我一直覺得,對一個消息如何進行處理才是與業務邏輯關聯最密切的。業務邏輯以及某些非功能性需求決定了到底我們需要什么樣的路由邏輯。而且在上圖中,我也把Dynamic Router包含進了Content-Based Router中了。因為實際上,Dynamic Router就是一種特殊的Content-Based Router。當然,仁者見仁,智者見智。不是說原書中的決策邏輯不好,而僅僅是將我所使用的決策邏輯介紹給大家。

  而對於用來進行消息轉化的Transformer以及各個Endpoint,由於我覺得它們實際上還是很容易區分的,因此在本文中就不再做細致的講解了。

 

管理基於消息的系統

  在前面的講解中,我們只介紹了應該如何通過Enterprise Integration Pattern所提及的各種組成搭建一個系統。但是除了業務邏輯之外,我們還需要令我們的系統滿足一定的非功能性需求,例如高可用性,可測試性等。因此在搭建了一個系統之后,我們還需要做一系列的工作,才能讓我們的系統穩定持續地提供服務。

  但是對這些非功能性需求的保證則沒有那么簡單。例如,在一個基於消息的系統中,消息的生產者和消費者並不知道彼此,同時對消息的傳送常常是一個異步的調用,其只對消息的可靠傳遞進行了保證,卻沒有對消息的傳遞時間進行保證。因此如何滿足這些非功能性需求則是更為困難的一件事。

  Enterprise Integration Pattern一書中提供了一系列用以提供這些非功能性需求的解決方案。在本節中,我們就將對這些解決方案進行簡單地介紹。

  首先要介紹的就是Control Bus。在該方案中,Control Bus將使用獨立的管道與系統中的各個子系統關聯,以動態地監控各組成的運行狀態,如子系統是否正常工作,與其運行相關的統計數據,其是否過載,消息的處理是否有較高的延遲等。甚至在監控到了某些異常狀態之后,其還需要通過這些管道向這些子系統發送消息,以更改這些子系統的配置:

  那么我們應該如何通過這些消息來判斷一個子系統是否正常工作呢?簡單地說,我們可以令子系統向管道中送入一系列心跳消息的方式來解決。這種心跳消息可能僅僅是一個簡單的通知消息,更可以在消息中包含子系統當前的狀態信息,如處理了多少消息,每個消息的處理時間,整個系統的狀態等。

  但是這些信息僅僅用來描述子系統的當前運行狀態。我們怎么判斷子系統的業務邏輯是否正常執行呢?此時我們就需要使用Enterprise Integration Pattern中所介紹的Test Message方案:

  從上圖中可以看到,Test Message主要包含了四個組成:Generator將首先生成測試消息,接下來,該測試消息將會通過Injector與實際的業務消息發送到子系統中。在子系統處理完畢之后,Separator將會把這些測試消息對應的處理結果分離,並將這些處理結果發送給Verifier進行驗證。而這些驗證的結果將被發送到Control Bus中,以方便Control Bus管理這些子系統。

  好了,現在我們已經知道了如何探測一個子系統是否在正常工作。下一步則是為我們追蹤及調試系統作准備。為了能夠完成這些功能,我們首先需要能夠偵聽在兩個子系統之間所傳遞的消息,才能通過這些偵聽到的消息來進行調試。當然,在一個Publish-Subscribe管道上偵聽消息是非常簡單的:我們只需要偵聽該管道上的消息即可。但是由於Point-to-Point管道將只能對消息進行點對點傳輸,因此我們不能簡單地對該管道上的消息進行偵聽。為了解決這個問題,Enterprise Integration Pattern則提出了Wire Tap方案。該方案會將Point-to-Point管道的一端連接到Wire Tap上,然后由其向目標子系統以及偵聽方轉發該消息:

  好的,現在我們能夠偵聽這些消息了,下一步則是找到一個地方把它們存起來。該功能是通過Message Store來完成的:

  從上圖中可以看到,Message Store會要求各個子系統在向輸出管道放置消息時也向消息的存儲發送一個相同的消息,從而完成對這些消息的持久化。但是我們怎么才能知道一個消息到底是如何在系統中流動的呢?答案是通過Message History來記錄消息所經過的各個子系統:

  而為了重現並調試某些出錯的情況,我們則需要讓某個消息能夠經過一系列特殊的子系統,從而允許軟件開發人員對出錯的情況進行調試。此時我們就需要使用Detour方案。該方案會使用一個Context-Based Router判斷某個消息是否滿足特殊條件,如果是,那么將其傳遞給特定的輸出管道:

  但是這里有一個問題,那就是我們更改了消息的路由路徑。這明顯會影響Request-Reply類型的消息的執行。為了解決這個問題,我們需要使用一個Smart Proxy。該組成能夠緩存原消息的Return Address。這樣當一個消息經過該組成時,其將首先緩存該消息的Return Address,並使用自己的響應輸入管道地址替換消息中的返回地址。當消息從該管道返回時,Smart Proxy則會找到原有的Return Address並將消息送回。

  至於Enterprise Integration Pattern中所提到的最后一個組成Channel Purger則非常容易理解。由於消息是在消息系統中緩存的。當我們重新啟動某個子系統,或者對某個子系統進行調試時,其管道中所存留的消息將會明顯地影響我們的調試。Channel Purger則會幫助我們解決這個問題:其會將管道中的不需要的消息移除。

 

適當地使用EIP

  最后一節,我們則主要用來討論您應該如何在合適的時機以合適的方式使用Enterprise Integration Pattern所提供的各種功能。

  首先要明白的就是什么時候使用Enterprise Integration Pattern。試想一下,如果一個系統對一個用戶請求的處理需要5秒鍾,那么一個瀏覽器用戶需要很長時間才能完成對頁面上所有數據的加載。對於不同的任務,用戶對該行為的忍受能力其實並不相同。例如如果用戶加載一個服務的首頁都需要2分鍾,那么他極有可能放棄使用該服務。但是如果一個功能是在后台做了非常耗時的操作,如部署虛機並在其上安裝運行服務所需要的軟件,那么對該請求的處理耗時10分鍾都不足為過。此時我們只需要提供給用戶一個界面並定時地刷新任務的執行狀態,以通知我們的系統正在工作既可。

  因此,一個原本就需要較長時間耗時的,或者是至少用戶能夠理解為較為耗時的功能,才能使用Enterprise Integration Pattern對其進行組織。很多直接面向用戶的功能,如電子商務,博客,很少直接使用到這些需要長時間耗時的操作,因此使用Enterprise Integration Pattern來組織這些功能只會讓您的服務質量變得更差。

  那么我們應該在什么時候使用Enterprise Integration Pattern呢?答案實際上就存在於Enterpriese這一個詞上。很多企業級應用常常包含一系列非常耗時的操作。就以現在最流行的雲來說吧。我做的產品就是一個雲管理軟件。這個軟件能讓用戶通過簡單地拖拽就能定義其在特定雲上所需要部署的服務。接下來,用戶只要點擊一下部署,在幾十分鍾后,該應用就將被部署完畢。

  讓我們想一想這個雲管理軟件在部署時做了哪些事情呢?從Amazon上請求資源,對資源進行配置,在這些資源上部署服務所需要的各個軟件,配置這些軟件,並最終啟動服務。可以想象到的是,這里面的每一步都是一個較為耗時的操作。而且它是一個非常典型的按照Pipes and Filters模型組織的業務邏輯:

  而為了能讓用戶能夠知道我們的應用正在正常工作,我們則會將當前部署任務的狀態回填到數據庫中。這樣用戶在請求查看當前任務的運行狀態時,我們只需要從數據庫中將該狀態讀出返回既可。因此,雖然我們的部署服務所需要消耗的時間較長,但是用戶在請求查看時,我們就能非常快速地返回,不是么?

  其實這是業內非常常見的一種對耗時任務的一種展示方法。只是由於這可能涉及到我們公司產品的內部實現,因此為了避免一些不必要的麻煩,我會找機會在介紹其它公司的產品,例如Amazon的CloudFormation,Beanstalk或OpsWorks等再對它的內部執行邏輯進行講解。

  而且從雲這個領域來看,其實現在對雲服務提供Enterprise Integration Pattern的原生支持這一要求的呼聲也是很高的。這也就是所謂的Cloud Orchestration的一個重要的組成部分。當然啊,這玩藝挺大也挺虛的。我盡量把它們一步步細化地講解掉,畢竟我這一系列和Web Service的文章都是一步步地向着這個目標前進的。從前面的負載平衡,后面的擴展性,然后還有以后要講的高可用性(尤其是基於雲的),Amazon雲所提供的功能等,我都會抽出時間寫成博客。

 

轉載請注明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/5185353.html

商業轉載請事先與我聯系:silverfox715@sina.com

公眾號一定幫忙別標成原創,因為協調起來太麻煩了。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM