按照Enterprise Integration Pattern搭建服務系統

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

 

尋找最優的解決方案數據庫

  相信您在讀前一篇文章時就已經能感受到,在使用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的系統,最須要考慮的是什麼?服務器

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

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

  第一個使系統具備合適性能的前提就是使子系統擁有合適的粒度。若是子系統的粒度較小,那麼對一個業務邏輯的處理就須要經由更多的子系統。這既增長了管道的數量,又增長了消息在管道中傳輸的次數。除此以外,子系統粒度過細也會給消息系統帶來很大的壓力。在前一篇文章中已經提到過,管道會將消息保存起來,所以每一個管道都會佔用一部份內存。若是子系統的粒度過細,那麼整個系統就須要更多的管道,對消息系統所在的服務器形成更大的壓力,也會提升系統出錯的可能。異步

  一個擁有較粗粒度的系統最常出現的問題就是系統過載。這時咱們該怎麼作呢?答案就是對其進行橫向擴展。在《服務的擴展性》一文中咱們已經提到過,一個服務的擴展方式分爲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

公衆號必定幫忙別標成原創,由於協調起來太麻煩了。。。

相關文章
相關標籤/搜索