99999久久久久久亚洲,欧美人与禽猛交狂配,高清日韩av在线影院,一个人在线高清免费观看,啦啦啦在线视频免费观看www

熱線電話:13121318867

登錄
首頁精彩閱讀案例分析:基于消息的分布式架構(gòu)
案例分析:基于消息的分布式架構(gòu)
2016-02-29
收藏

美國計算機(jī)科學(xué)家,LaTex的作者Leslie Lamport說:“分布式系統(tǒng)就是這樣一個系統(tǒng),系統(tǒng)中一個你甚至都不知道的計算機(jī)出了故障,卻可能導(dǎo)致你自己的計算機(jī)不可用?!币徽Z道破了開發(fā)分布式系統(tǒng)的玄機(jī),那就是它的復(fù)雜與不可控。所以Martin Fowler強(qiáng)調(diào):分布式調(diào)用的第一原則就是不要分布式。這句話看似頗具哲理,然而就企業(yè)應(yīng)用系統(tǒng)而言,只要整個系統(tǒng)在不停地演化,并有多個子系統(tǒng)共同存在時,這條原則就會被迫打破。蓋因為在當(dāng)今的企業(yè)應(yīng)用系統(tǒng)中,很難尋找到完全不需要分布式調(diào)用的場景。Martin Fowler提出的這條原則,一方面是希望設(shè)計者能夠?qū)徤鞯貙Υ植际秸{(diào)用,另一方面卻也是分布式系統(tǒng)自身存在的缺陷所致。無論是CORBA,還是EJB 2;無論是RPC平臺,還是Web Service,都因為駐留在不同進(jìn)程空間的分布式組件,而引入額外的復(fù)雜度,并可能對系統(tǒng)的效率、可靠性、可預(yù)測性等諸多方面帶來負(fù)面的影響。

然而,不可否認(rèn)的是在企業(yè)應(yīng)用系統(tǒng)領(lǐng)域,我們總是會面對不同系統(tǒng)之間的通信、集成與整合,尤其當(dāng)面臨異構(gòu)系統(tǒng)時,這種分布式的調(diào)用與通信變得越重要,它在架構(gòu)設(shè)計中就更加凸顯其價值。并且,從業(yè)務(wù)分析與架構(gòu)質(zhì)量的角度來講,我們也希望在系統(tǒng)架構(gòu)中盡可能地形成對服務(wù)的重用,通過獨立運行在進(jìn)程中服務(wù)的形式,徹底解除客戶端與服務(wù)端的耦合。這常常是架構(gòu)演化的必然道路。在我的同事陳金洲發(fā)表在InfoQ上的文章《架構(gòu)腐化之謎》中,就認(rèn)為可以通過“將獨立的模塊放入獨立的進(jìn)程”來解決架構(gòu)因為代碼規(guī)模變大而腐化的問題。

隨著網(wǎng)絡(luò)基礎(chǔ)設(shè)施的逐步成熟,從RPC進(jìn)化到Web Service,并在業(yè)界開始普遍推行SOA,再到后來的RESTful平臺以及云計算中的PaaS與SaaS概念的推廣,分布式架構(gòu)在企業(yè)應(yīng)用中開始呈現(xiàn)出不同的風(fēng)貌,然而殊途同歸,這些分布式架構(gòu)的目標(biāo)仍然是希望回到建造巴別塔的時代,系統(tǒng)之間的交流不再為不同語言與平臺的隔閡而產(chǎn)生障礙。正如Martin Fowler在《企業(yè)集成模式》一書的序中寫道:“集成之所以重要是因為相互獨立的應(yīng)用是沒有生命力的。我們需要一種技術(shù)能將在設(shè)計時并未考慮互操作的應(yīng)用集成起來,打破它們之間的隔閡,獲得比單個應(yīng)用更多的效益”。這或許是分布式架構(gòu)存在的主要意義。

1、集成模式中的消息模式

歸根結(jié)底,企業(yè)應(yīng)用系統(tǒng)就是對數(shù)據(jù)的處理,而對于一個擁有多個子系統(tǒng)的企業(yè)應(yīng)用系統(tǒng)而言,它的基礎(chǔ)支撐無疑就是對消息的處理。與對象不同,消息本質(zhì)上是一種數(shù)據(jù)結(jié)構(gòu)(當(dāng)然,對象也可以看做是一種特殊的消息),它包含消費者與服務(wù)雙方都能識別的數(shù)據(jù),這些數(shù)據(jù)需要在不同的進(jìn)程(機(jī)器)之間進(jìn)行傳遞,并可能會被多個完全不同的客戶端消費。在眾多分布式技術(shù)中,消息傳遞相較文件傳遞與遠(yuǎn)程過程調(diào)用(RPC)而言,似乎更勝一籌,因為它具有更好的平臺無關(guān)性,并能夠很好地支持并發(fā)與異步調(diào)用。對于Web Service與RESTful而言,則可以看做是消息傳遞技術(shù)的一種衍生或封裝。在《面向模式的軟件架構(gòu)(卷四)》一書中,將關(guān)于消息傳遞的模式劃歸為分布式基礎(chǔ)設(shè)施的范疇,這是因為諸多消息中間件產(chǎn)品的出現(xiàn),使得原來需要開發(fā)人員自己實現(xiàn)的功能,已經(jīng)可以直接重用。這極大地降低了包括設(shè)計成本、實現(xiàn)成本在內(nèi)的開發(fā)成本。因此,對于架構(gòu)師的要求也就從原來的設(shè)計實現(xiàn),轉(zhuǎn)變?yōu)閷I(yè)務(wù)場景和功能需求的判斷,從而能夠正確地進(jìn)行架構(gòu)決策、技術(shù)選型與模式運用。

常用的消息模式

在我參與過的所有企業(yè)應(yīng)用系統(tǒng)中,無一例外地都采用(或在某些子系統(tǒng)與模塊中部分采用)了基于消息的分布式架構(gòu)。但是不同之處在于,讓我們做出架構(gòu)決策的證據(jù)卻迥然而異,這也直接影響我們所要應(yīng)用的消息模式。

消息通道(Message Channel)模式

我們常常運用的消息模式是Message Channel(消息通道)模式,如圖1所示。



消息通道作為在客戶端(消費者,Consumer)與服務(wù)(生產(chǎn)者,Producer)之間引入的間接層,可以有效地解除二者之間的耦合。只要實現(xiàn)規(guī)定雙方需要通信的消息格式,以及處理消息的機(jī)制與時機(jī),就可以做到消費者對生產(chǎn)者的“無知”。事實上,該模式可以支持多個生產(chǎn)者與消費者。例如,我們可以讓多個生產(chǎn)者向消息通道發(fā)送消息,因為消費者對生產(chǎn)者的無知性,它不必考慮究竟是哪個生產(chǎn)者發(fā)來的消息。

雖然消息通道解除了生產(chǎn)者與消費者之間的耦合,使得我們可以任意地對生產(chǎn)者與消費者進(jìn)行擴(kuò)展,但它又同時引入了各自對消息通道的依賴,因為它們必須知道通道資源的位置。要解除這種對通道的依賴,可以考慮引入Lookup服務(wù)來查找該通道資源。例如,在JMS中就可以通過JNDI來獲取消息通道Queue。若要做到充分的靈活性,可以將與通道相關(guān)的信息存儲到配置文件中,Lookup服務(wù)首先通過讀取配置文件來獲得通道。

消息通道通常以隊列的形式存在,這種先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)無疑最為適合這種處理消息的場景。微軟的MSMQ、IBM MQ、JBoss MQ以及開源的RabbitMQ、Apache ActiveMQ都通過隊列實現(xiàn)了Message Channel模式。因此,在選擇運用Message Channel模式時,更多地是要從質(zhì)量屬性的層面對各種實現(xiàn)了該模式的產(chǎn)品進(jìn)行全方位的分析與權(quán)衡。例如,消息通道對并發(fā)的支持以及在性能上的表現(xiàn);消息通道是否充分地考慮了錯誤處理;對消息安全的支持;以及關(guān)于消息持久化、災(zāi)備(fail over)與集群等方面的支持。因為通道傳遞的消息往往是一些重要的業(yè)務(wù)數(shù)據(jù),一旦通道成為故障點或安全性的突破點,對系統(tǒng)就會造成災(zāi)難性的影響。在本文的第二部分,我將給出一個實際案例來闡釋在進(jìn)行架構(gòu)決策時應(yīng)該考慮的架構(gòu)因素,并由此做出正確地決策。

發(fā)布者-訂閱者(Publisher-Subscriber)模式

一旦消息通道需要支持多個消費者時,就可能面臨兩種模型的選擇:拉模型與推模型。拉模型是由消息的消費者發(fā)起的,主動權(quán)把握在消費者手中,它會根據(jù)自己的情況對生產(chǎn)者發(fā)起調(diào)用。如圖2所示:


拉模型的另一種體現(xiàn)則由生產(chǎn)者在狀態(tài)發(fā)生變更時,通知消費者其狀態(tài)發(fā)生了改變。但得到通知的消費者卻會以回調(diào)方式,通過調(diào)用傳遞過來的消費者對象獲取更多細(xì)節(jié)消息。

在基于消息的分布式系統(tǒng)中,拉模型的消費者通常以Batch Job的形式,根據(jù)事先設(shè)定的時間間隔,定期偵聽通道的情況。一旦發(fā)現(xiàn)有消息傳遞進(jìn)來,就會轉(zhuǎn)而將消息傳遞給真正的處理器(也可以看做是消費者)處理消息,執(zhí)行相關(guān)的業(yè)務(wù)。在本文第二部分介紹的醫(yī)療衛(wèi)生系統(tǒng),正是通過引入Quartz.NET實現(xiàn)了Batch Job,完成對消息通道中消息的處理。

推模型的主動權(quán)常常掌握在生產(chǎn)者手中,消費者被動地等待生產(chǎn)者發(fā)出的通知,這就要求生產(chǎn)者必須了解消費者的相關(guān)信息。如圖3所示:



對于推模型而言,消費者無需了解生產(chǎn)者。在生產(chǎn)者通知消費者時,傳遞的往往是消息(或事件),而非生產(chǎn)者自身。同時,生產(chǎn)者還可以根據(jù)不同的情況,注冊不同的消費者,又或者在封裝的通知邏輯中,根據(jù)不同的狀態(tài)變化,通知不同的消費者。

兩種模型各有優(yōu)勢。拉模型的好處在于可以進(jìn)一步解除消費者對通道的依賴,通過后臺任務(wù)去定期訪問消息通道。壞處是需要引入一個單獨的服務(wù)進(jìn)程,以Schedule形式執(zhí)行。而對于推模型而言,消息通道事實上會作為消費者觀察的主體,一旦發(fā)現(xiàn)消息進(jìn)入,就會通知消費者執(zhí)行對消息的處理。無論推模型,拉模型,對于消息對象而言,都可能采用類似Observer模式的機(jī)制,實現(xiàn)消費者對生產(chǎn)者的訂閱,因此這種機(jī)制通常又被稱為Publisher-Subscriber模式,如圖4所示:



通常情況下,發(fā)布者和訂閱者都會被注冊到用于傳播變更的基礎(chǔ)設(shè)施(即消息通道)上。發(fā)布者會主動地了解消息通道,使其能夠?qū)⑾l(fā)送到通道中;消息通道一旦接收到消息,會主動地調(diào)用注冊在通道中的訂閱者,進(jìn)而完成對消息內(nèi)容的消費。

對于訂閱者而言,有兩種處理消息的方式。一種是廣播機(jī)制,這時消息通道中的消息在出列的同時,還需要復(fù)制消息對象,將消息傳遞給多個訂閱者。例如,有多個子系統(tǒng)都需要獲取從CRM系統(tǒng)傳來的客戶信息,并根據(jù)傳遞過來的客戶信息,進(jìn)行相應(yīng)的處理。此時的消息通道又被稱為Propagation通道。另一種方式則屬于搶占機(jī)制,它遵循同步方式,在同一時間只能有一個訂閱者能夠處理該消息。實現(xiàn)Publisher-Subscriber模式的消息通道會選擇當(dāng)前空閑的唯一訂閱者,并將消息出列,并傳遞給訂閱者的消息處理方法。

目前,有許多消息中間件都能夠很好地支持Publisher-Subscriber模式,例如JMS接口規(guī)約中對于Topic對象提供的MessagePublisher與MessageSubscriber接口。RabbitMQ也提供了自己對該模式的實現(xiàn)。微軟的MSMQ雖然引入了事件機(jī)制,可以在隊列收到消息時觸發(fā)事件,通知訂閱者。但它并非嚴(yán)格意義上的Publisher-Subscriber模式實現(xiàn)。由微軟MVP Udi Dahan作為主要貢獻(xiàn)者的NServiceBus,則對MSMQ以及WCF做了進(jìn)一層包裝,并能夠很好地實現(xiàn)這一模式。

消息路由(Message Router)模式

無論是Message Channel模式,還是Publisher-Subscriber模式,隊列在其中都扮演了舉足輕重的角色。然而,在企業(yè)應(yīng)用系統(tǒng)中,當(dāng)系統(tǒng)變得越來越復(fù)雜時,對性能的要求也會越來越高,此時對于系統(tǒng)而言,可能就需要支持同時部署多個隊列,并可能要求分布式部署不同的隊列。這些隊列可以根據(jù)定義接收不同的消息,例如訂單處理的消息,日志信息,查詢?nèi)蝿?wù)消息等。這時,對于消息的生產(chǎn)者和消費者而言,并不適宜承擔(dān)決定消息傳遞路徑的職責(zé)。事實上,根據(jù)S單一職責(zé)原則,這種職責(zé)分配也是不合理的,它既不利于業(yè)務(wù)邏輯的重用,也會造成生產(chǎn)者、消費者與消息隊列之間的耦合,從而影響系統(tǒng)的擴(kuò)展。

既然這三種對象(組件)都不宜承擔(dān)這樣的職責(zé),就有必要引入一個新的對象專門負(fù)責(zé)傳遞路徑選擇的功能,這就是所謂的Message Router模式,如圖5所示:


通過消息路由,我們可以配置路由規(guī)則指定消息傳遞的路徑,以及指定具體的消費者消費對應(yīng)的生產(chǎn)者。例如指定路由的關(guān)鍵字,并由它來綁定具體的隊列與指定的生產(chǎn)者(或消費者)。路由的支持提供了消息傳遞與處理的靈活性,也有利于提高整個系統(tǒng)的消息處理能力。同時,路由對象有效地封裝了尋找與匹配消息路徑的邏輯,就好似一個調(diào)停者(Meditator),負(fù)責(zé)協(xié)調(diào)消息、隊列與路徑尋址之間關(guān)系。

除了以上的模式之外,Messaging模式提供了一個通信基礎(chǔ)架構(gòu),使得我們可以將獨立開發(fā)的服務(wù)整合到一個完整的系統(tǒng)中。 Message Translator模式則完成對消息的解析,使得不同的消息通道能夠接收和識別不同格式的消息。而且通過引入這樣的對象,也能夠很好地避免出現(xiàn)盤根錯節(jié),彼此依賴的多個服務(wù)。Message Bus模式可以為企業(yè)提供一個面向服務(wù)的體系架構(gòu)。它可以完成對消息的傳遞,對服務(wù)的適配與協(xié)調(diào)管理,并要求這些服務(wù)以統(tǒng)一的方式完成協(xié)作。

2、消息模式的應(yīng)用場景

基于消息的分布式架構(gòu)總是圍繞著消息來做文章。例如可以將消息封裝為對象,或者指定消息的規(guī)范例如SOAP,或者對實體對象的序列化與反序列化。這些方式的目的只有一個,就是將消息設(shè)計為生產(chǎn)者和消費者都能夠明白的格式,并能通過消息通道進(jìn)行傳遞。

場景一:基于消息的統(tǒng)一服務(wù)架構(gòu)

在制造工業(yè)的CIMS系統(tǒng)中,我們嘗試將各種業(yè)務(wù)以服務(wù)的形式公開給客戶端的調(diào)用者,例如定義這樣的接口:



之所以能夠設(shè)計這樣的服務(wù),原因在于我們對業(yè)務(wù)信息進(jìn)行了高度的抽象,以消息的形式在服務(wù)之間傳遞。此時的消息其實是生產(chǎn)者與消費者之間的契約或接口,只要遵循這樣的契約,按照規(guī)定的格式對消息進(jìn)行轉(zhuǎn)換與抽取,就能很好地支持系統(tǒng)的分布式處理。

在這個CIMS系統(tǒng)中,我們將消息劃分為ID,Name和Body,通過定義如下的接口方法,可以獲得消息主體的相關(guān)屬性:



消息主體類Message實現(xiàn)了IMessage接口。在該類中,消息體Body為IMessageItemSequence類型。這個類型用于獲取和設(shè)置消息的內(nèi)容:Value和Item:



Value為字符串類型,它利用了HashTable存儲Key和Value的鍵值對。Item則為IMessageItem類型,在IMessageItemSequence的實現(xiàn)類中,同樣利用了HashTable存儲Key和Item的鍵值對。

IMessageItem支持消息體的嵌套。它包含了兩部分:SubValue和SubItem。實現(xiàn)的方式和IMessageItemSequence相似。通過定義這樣的嵌套結(jié)構(gòu),使得消息的擴(kuò)展成為可能。一般的消息結(jié)構(gòu)如下所示:



各個消息對象之間的關(guān)系如圖6所示:


在實現(xiàn)服務(wù)進(jìn)程通信之前,我們必須定義好各個服務(wù)或各個業(yè)務(wù)的消息格式。通過消息體的方法在服務(wù)的一端設(shè)置消息的值,然后發(fā)送,并在服務(wù)的另一端獲得這些值。例如發(fā)送消息端定義如下的消息體:



我們在客戶端引入了一個ServiceLocator對象,它通過MessageQueueListener對消息隊列進(jìn)行偵聽,一旦接收到消息,就獲取該消息中的name去定位它所對應(yīng)的服務(wù),然后調(diào)用服務(wù)的Execute(aMessage)方法,執(zhí)行相關(guān)的業(yè)務(wù)。

ServiceLocator承擔(dān)的定位職責(zé)其實是對存儲在ServiceContainer容器中的服務(wù)進(jìn)行查詢。ServiceContainer容器可以讀取配置文件,在啟動服務(wù)的時候初始化所有的分布式服務(wù)(注意,這些服務(wù)都是無狀態(tài)的),并對這些服務(wù)進(jìn)行管理。它封裝了服務(wù)的基本信息,諸如服務(wù)所在的位置,服務(wù)的部署方式等,從而避免服務(wù)的調(diào)用者直接依賴于服務(wù)的細(xì)節(jié),既減輕了調(diào)用者的負(fù)擔(dān),還能夠較好地實現(xiàn)服務(wù)的擴(kuò)展與遷移。

在這個系統(tǒng)中,我們主要引入了Messaging模式,通過定義的IMessage接口,使得我們更好地對服務(wù)進(jìn)行抽象,并以一種扁平的格式存儲數(shù)據(jù)信息,從而解除服務(wù)之間的耦合。只要各個服務(wù)就共用的消息格式達(dá)成一致,請求者就可以不依賴于接收者的具體接口。通過引入的Message對象,我們就可以建立一種在行業(yè)中通用的消息模型與分布式服務(wù)模型。事實上,基于這樣的一個框架與平臺,在對制造行業(yè)的業(yè)務(wù)進(jìn)行開發(fā)時,開發(fā)人員最主要的活動是與領(lǐng)域?qū)<揖透鞣N業(yè)務(wù)的消息格式進(jìn)行討論,這樣一種面向領(lǐng)域的消息語言,很好地掃清了技術(shù)人員與業(yè)務(wù)人員的溝通障礙;同時在各個子系統(tǒng)之間,我們也只需要維護(hù)服務(wù)間相互傳遞的消息接口表。每個服務(wù)的實現(xiàn)都是完全隔離的,有效地做到了對業(yè)務(wù)知識與基礎(chǔ)設(shè)施的合理封裝與隔離。

對于消息的格式和內(nèi)容,我們考慮引入了Message Translator模式,負(fù)責(zé)對前面定義的消息結(jié)構(gòu)進(jìn)行翻譯和解析。為了進(jìn)一步減輕開發(fā)人員的負(fù)擔(dān),我們還可以基于該平臺搭建一個消息-對象-關(guān)系的映射框架,引入實體引擎(Entity Engine)將消息轉(zhuǎn)換為領(lǐng)域?qū)嶓w,使得服務(wù)的開發(fā)者能夠以完全面向?qū)ο蟮乃枷腴_發(fā)各個服務(wù)組件,并通過調(diào)用持久層實現(xiàn)消息數(shù)據(jù)的持久化。同時,利用消息總線(此時的消息總線可以看做是各個服務(wù)組件的連接器)連接不同的服務(wù),并允許異步地傳遞消息,對消息進(jìn)行編碼。這樣一個基于消息的分布式架構(gòu)如圖7所示:


場景二:消息中間件的架構(gòu)決策

在一個醫(yī)療衛(wèi)生系統(tǒng)中,我們面臨了客戶對系統(tǒng)性能/可用性的非功能需求。在我們最初啟動該項目時,客戶就表達(dá)了對性能與可用性的特別關(guān)注??蛻粝M罱K用戶在進(jìn)行復(fù)雜的替換刪除操作時,能夠具有很好的用戶體驗,簡言之,就是希望能夠快速地得到操作的響應(yīng)。問題在于這樣的替換刪除操作需要處理比較復(fù)雜的業(yè)務(wù)邏輯,同時牽涉到的關(guān)聯(lián)數(shù)據(jù)量非常大,整個操作若需完成,最壞情況下可能需要幾分鐘的時間。我們可以通過引入緩存、索引、分頁等多種方式對數(shù)據(jù)庫操作進(jìn)行性能調(diào)優(yōu),但整個操作的耗時始終無法達(dá)到客戶的要求。由于該系統(tǒng)是在一個遺留系統(tǒng)的基礎(chǔ)上開發(fā),如果要引入Map-Reduce來處理這些操作,以滿足質(zhì)量需求,則對架構(gòu)的影響太大,且不能很好地重用之前系統(tǒng)的某些組件。顯然,付出的成本與收益并不成正比。

通過對需求進(jìn)行分析,我們注意到最終客戶并不需要實時獲得結(jié)果,只要能夠保證最終結(jié)果的一致性和完整性即可。關(guān)鍵在于就用戶體驗而言,他們不希望經(jīng)歷漫長的等待,然后再通知他們操作究竟是成功還是失敗。這是一個典型需要通過后臺任務(wù)進(jìn)行異步處理的場景。

在企業(yè)應(yīng)用系統(tǒng)中,我們常常會遭遇這樣的場景。我們曾經(jīng)在一個金融系統(tǒng)中嘗試通過自己編寫任務(wù)的方式來控制后臺線程的并發(fā)訪問,并完成對任務(wù)的調(diào)度。事實證明,這樣的設(shè)計并非行之有效。對于這種典型的異步處理來說,基于消息傳遞的架構(gòu)模式才是解決這一問題的最佳辦法。

因為消息中間件的逐步成熟,對于這一問題的架構(gòu)設(shè)計,已經(jīng)由原來對設(shè)計實現(xiàn)的關(guān)注轉(zhuǎn)為如何進(jìn)行產(chǎn)品選型和技術(shù)決策。例如,在.NET平臺下,架構(gòu)師需要重點考慮的是應(yīng)該選擇哪種消息中間件來處理此等問題?這就需要我們必須結(jié)合具體的業(yè)務(wù)場景,來識別這種異步處理方式的風(fēng)險,然后再根據(jù)這些風(fēng)險去比較各種技術(shù),以求尋找到最適合的方案。

通過分析業(yè)務(wù)場景以及客戶性質(zhì),我們發(fā)現(xiàn)該業(yè)務(wù)場景具有如下特征

在一些特定情形下,可能會集中發(fā)生批量的替換刪除操作,使得操作的并發(fā)量達(dá)到高峰;例如FDA要求召回一些違規(guī)藥品時,就需要刪除藥品庫中該藥品的信息;

操作結(jié)果不要求實時性,但需要保證操作的可靠性,不能因為異常失敗而導(dǎo)致某些操作無法進(jìn)行;

自動操作的過程是不可逆轉(zhuǎn)的,因此需要記錄操作歷史;

基于性能考慮,大多數(shù)操作需要調(diào)用數(shù)據(jù)庫的存儲過程;

操作的數(shù)據(jù)需要具備一定的安全性,避免被非法用戶對數(shù)據(jù)造成破壞;

與操作相關(guān)的功能以組件形式封裝,保證組件的可重用性、可擴(kuò)展性與可測試性;

數(shù)據(jù)量可能隨著最終用戶的增多而逐漸增大;

針對如上的業(yè)務(wù)需求,我們決定從以下幾個方面對各種技術(shù)方案進(jìn)行橫向的比較與考量。

并發(fā):選擇的消息隊列一定要很好地支持用戶訪問的并發(fā)性;

安全:消息隊列是否提供了足夠的安全機(jī)制;

性能伸縮:不能讓消息隊列成為整個系統(tǒng)的單一性能瓶頸;

部署:盡可能讓消息隊列的部署更為容易;

災(zāi)備:不能因為意外的錯誤、故障或其他因素導(dǎo)致處理數(shù)據(jù)的丟失;

API易用性:處理消息的API必須足夠簡單、并能夠很好地支持測試與擴(kuò)展;

我們先后考察了MSMQ、Resque、ActiveMQ和RabbitMQ,通過查詢相關(guān)資料,以及編寫Spike代碼驗證相關(guān)質(zhì)量,我們最終選擇了RabbitMQ。

我們選擇放棄MSMQ,是因為它嚴(yán)重依賴Windows操作系統(tǒng);它雖然提供了易用的GUI方便管理人員對其進(jìn)行安裝和部署,但若要編寫自動化部署腳本,卻非常困難。同時,MSMQ的隊列容量不能查過4M字節(jié),這也是我們無法接收的。Resque的問題是目前僅支持Ruby的客戶端調(diào)用,不能很好地與.NET平臺集成。此外,Resque對消息持久化的處理方式是寫入到Redis中,因而需要在已有RDBMS的前提下,引入新的Storage。我們比較傾心于ActiveMQ與RabbitMQ,但通過編寫測試代碼,采用循環(huán)發(fā)送大數(shù)據(jù)消息以驗證消息中間件的性能與穩(wěn)定性時,我們發(fā)現(xiàn)ActiveMQ的表現(xiàn)并不太讓人滿意。至少,在我們的詢證調(diào)研過程中,ActiveMQ會因為頻繁發(fā)送大數(shù)據(jù)消息而偶爾出現(xiàn)崩潰的情況。相對而言,RabbitMQ在各個方面都比較適合我們的架構(gòu)要求。

例如在災(zāi)備與穩(wěn)定性方面,RabbitMQ提供了可持久化的隊列,能夠在隊列服務(wù)崩潰的時候,將未處理的消息持久化到磁盤上。為了避免因為發(fā)送消息到寫入消息之間的延遲導(dǎo)致信息丟失,RabbitMQ引入了Publisher Confirm機(jī)制以確保消息被真正地寫入到磁盤中。它對Cluster的支持提供了Active/Passive與Active/Active兩種模式。例如,在Active/Passive模式下,一旦一個節(jié)點失敗,Passive節(jié)點就會馬上被激活,并迅速替代失敗的Active節(jié)點,承擔(dān)起消息傳遞的職責(zé)。如圖8所示:



在并發(fā)處理方面,RabbitMQ本身是基于erlang編寫的消息中間件,作為一門面向并發(fā)處理的編程語言,erlang對并發(fā)處理的天生優(yōu)勢使得我們對RabbitMQ的并發(fā)特性抱有信心。RabbitMQ可以非常容易地部署到Windows、Linux等操作系統(tǒng)下,同時,它也可以很好地部署到服務(wù)器集群中。它的隊列容量是沒有限制的(取決于安裝RabbitMQ的磁盤容量),發(fā)送與接收信息的性能表現(xiàn)也非常好。RabbitMQ提供了Java、.NET、Erlang以及C語言的客戶端API,調(diào)用非常簡單,并且不會給整個系統(tǒng)引入太多第三方庫的依賴。 例如.NET客戶端只需要依賴一個程序集。

即使我們選擇了RabbitMQ,但仍有必要對系統(tǒng)與具體的消息中間件進(jìn)行解耦,這就要求我們對消息的生產(chǎn)者與消費者進(jìn)行抽象,例如定義如下的接口:



在這兩個接口的實現(xiàn)類中,我們封裝了RabbitMQ的調(diào)用類,例如:




我們用Quartz.Net來實現(xiàn)Batch Job。通過定義一個實現(xiàn)了IStatefulJob接口的Job類,在Execute()方法中完成對隊列的偵聽。Job中RabbitMQSubscriber類的ListenTo()方法會調(diào)用Queue的Dequeue()方法,當(dāng)接收的消息到達(dá)隊列時,Job會偵聽到消息達(dá)到的事件,然后以同步的方式使得消息彈出隊列,并將消息作為參數(shù)傳遞給Action委托。因此,在Batch Job的Execute()方法中,可以定義消息處理的方法,并調(diào)用RabbitMQSubscriber類的ListenTo()方法,如下所示(注意,這里傳遞的消息事實上是Job的Id):



隊列的相關(guān)信息例如隊列名都存儲在配置文件中。Execute()方法調(diào)用了request對象的MakeRequest()方法,并將獲得的消息(即JobId)傳遞給該方法。它會根據(jù)JobId到數(shù)據(jù)庫中查詢該Job對應(yīng)的信息,并執(zhí)行真正的業(yè)務(wù)處理。

在對基于消息處理的架構(gòu)進(jìn)行決策時,除了前面提到的考慮因素外,還需要就許多設(shè)計細(xì)節(jié)進(jìn)行多方位的判斷與權(quán)衡。例如針對Job的執(zhí)行以及隊列的管理,就需要考慮如下因素:

對Queue中Job狀態(tài)的監(jiān)控與查詢;

對Job優(yōu)先級的管理;

能否取消或終止執(zhí)行時間過長的Job;

是否能夠設(shè)定Job的執(zhí)行時間;

是否能夠設(shè)定Poll的間隔時間;

能否跨機(jī)器分布式的放入Job;

對失敗Job的處理;

能否支持多個隊列,命名隊列;

能否允許執(zhí)行Job的工作進(jìn)程對應(yīng)特定的隊列;

對Dead Message的支持。

3、選擇的時機(jī)

究竟在什么時候,我們應(yīng)該選擇基于消息處理的分布式架構(gòu)?根據(jù)我參與的多個企業(yè)應(yīng)用系統(tǒng)的經(jīng)驗,竊以為需要滿足如下幾個條件:

對操作的實時性要求不高,而需要執(zhí)行的任務(wù)極為耗時;

存在企業(yè)內(nèi)部的異構(gòu)系統(tǒng)間的整合;

服務(wù)器資源需要合理分配與利用;

對于第一種情況,我們常常會選擇消息隊列來處理執(zhí)行時間較長的任務(wù)。此時引入的消息隊列就成了消息處理的緩沖區(qū)。消息隊列引入的異步通信機(jī)制,使得發(fā)送方和接收方都不用等待對方返回成功消息,就可以繼續(xù)執(zhí)行下面的代碼,從而提高了數(shù)據(jù)處理的能力。尤其是當(dāng)訪問量和數(shù)據(jù)流量較大的情況下,就可以結(jié)合消息隊列與后臺任務(wù),通過避開高峰期對大數(shù)據(jù)進(jìn)行處理,就可以有效降低數(shù)據(jù)庫處理數(shù)據(jù)的負(fù)荷。前面提到的醫(yī)療衛(wèi)生系統(tǒng)正是這樣一種適用場景。

對于不同系統(tǒng)乃至于異構(gòu)系統(tǒng)的整合,恰恰是消息模式善于處理的場景。只要規(guī)定了消息的格式與傳遞方式,就可以有效地實現(xiàn)不同系統(tǒng)之間的通信。在為某汽車制造商開發(fā)一個大型系統(tǒng)時,分銷商作為.NET客戶端,需要將數(shù)據(jù)傳遞到管理中心。這些數(shù)據(jù)將被Oracle的EBS(E-Business Suite)使用。分銷商管理系統(tǒng)(Dealer Management System,DMS)采用了C/S結(jié)構(gòu),數(shù)據(jù)庫為SQL Server,汽車制造商管理中心的EBS數(shù)據(jù)庫為Oracle 10g。我們需要解決兩種不同數(shù)據(jù)庫間數(shù)據(jù)的傳遞。解決方案就是利用MSMQ,將數(shù)據(jù)轉(zhuǎn)換為與數(shù)據(jù)庫無關(guān)的消息數(shù)據(jù),并在兩端部署MSMQ服務(wù)器,建立消息隊列以便于存儲消息數(shù)據(jù)。實現(xiàn)架構(gòu)如圖9所示。



首先,分銷商的數(shù)據(jù)通過MSMQ傳遞到MSMQ Server,再將數(shù)據(jù)插入到SQL Server數(shù)據(jù)庫的同時,利用FTP將數(shù)據(jù)傳送到專門的文件服務(wù)器上。EBS App Server會將文件服務(wù)器中的文件,基于接口規(guī)范寫入到Oracle數(shù)據(jù)庫,從而實現(xiàn).NET系統(tǒng)與Oracle系統(tǒng)之間的整合。

分布式系統(tǒng)通常能夠緩解單個服務(wù)器的壓力,通過將不同的業(yè)務(wù)操作與數(shù)據(jù)處理以不同的服務(wù)形式部署并運行在不同的服務(wù)器上,就可以有效地分配與利用服務(wù)器資源。在這種情況下,部署在不同服務(wù)器上的服務(wù),既可能作為服務(wù)端,用以處理客戶端調(diào)用的請求,也可能作為客戶端,在處理完自己的業(yè)務(wù)后,將其余業(yè)務(wù)請求委派給其他服務(wù)。在早期的CORBA系統(tǒng)中,通過建立統(tǒng)一的Naming Service,用以管理和分派服務(wù),并通過Event Service實現(xiàn)事件的分發(fā)與處理。但CORBA系統(tǒng)采用的是RPC的方式,需要將服務(wù)設(shè)計和部署為遠(yuǎn)程對象,并建立代理。如果通過消息通道的方式,則既可以解除這種對遠(yuǎn)程對象的依賴,又可以很好地支持異步調(diào)用模型。在前面提到的CIMS系統(tǒng),就是通過消息總線提供消息傳遞的基礎(chǔ)設(shè)施,并建立統(tǒng)一的消息處理服務(wù)模型,解除服務(wù)見的依賴,使得各個服務(wù)能夠獨立地部署到不同服務(wù)器上。

4、面臨的困難

由于消息模式自身的特殊性,我們在運用消息模式建立基于消息的分布式架構(gòu)時,常常會面臨許多困難。

首先是系統(tǒng)集成的問題。由于系統(tǒng)之間的通信靠消息進(jìn)行傳遞,就必須保證消息的一致性,同時,還需要維護(hù)系統(tǒng)之間(主要是服務(wù)之間)接口的穩(wěn)定性。一旦接口發(fā)生變化,就可能影響到該接口的所有調(diào)用者。即使服務(wù)通過接口進(jìn)行了抽象,由于消息持有雙方服務(wù)規(guī)定的業(yè)務(wù)數(shù)據(jù),在一定程度上違背了封裝的要義。換言之,生產(chǎn)與消費消息的雙方都緊耦合于消息。消息的變化會直接影響到各個服務(wù)接口的實現(xiàn)類。然而,為了盡可能保證接口的抽象性,我們所要處理的消息都不是強(qiáng)類型的,這就使得我們在編譯期間很難發(fā)現(xiàn)因為消息內(nèi)容發(fā)生變更產(chǎn)生的錯誤。在我之前提到的汽車零售商管理系統(tǒng)就存在這樣的問題。當(dāng)時我負(fù)責(zé)的CRM模塊需要同時與多個子系統(tǒng)進(jìn)行通信,而每個子系統(tǒng)又是由不同的團(tuán)隊進(jìn)行開發(fā)。團(tuán)隊之間因為溝通原因,常常未能及時地同步接口表。雖然各個子系統(tǒng)的單元測試和功能測試都已通過,但直到對CRM進(jìn)行集成測試,才發(fā)現(xiàn)存在大量消息不匹配的集成問題,這些問題的起因都是因為消息的變更。

解決的方案是引入充分的集成測試,甚至是回歸測試,并需要及時運行這些測試,以快速地獲得反饋。我們可以將集成測試作為提交代碼的驗證們,要求每次提交代碼都必須運行集成測試與指定的回歸測試 。這正是持續(xù)集成的體現(xiàn)。通過在本地構(gòu)建與遠(yuǎn)程構(gòu)建運行集成測試與回歸測試,有效地保證本地版本與集成后的版本不會因為消息的改變使得功能遭受破壞。一旦遭受破壞,也能夠及時獲得反饋,發(fā)現(xiàn)問題,即刻解決這些問題,而不是等到項目后期集中進(jìn)行集成測試。

另一個問題是后臺任務(wù)的非實時性帶來的測試?yán)щy。由于后臺任務(wù)是定期對消息隊列中的消息進(jìn)行處理,因而觸發(fā)的時機(jī)是不可預(yù)測的 。對于這種情況,我們通常會同時運用兩種方案,雙管其下地解決問題。首先,我們會為系統(tǒng)引入一個同步實現(xiàn)功能的版本,并通過在配置文件中引入toggle的開關(guān)機(jī)制,隨時可以在同步功能與異步功能之間進(jìn)行切換。如果我們能夠保證消息隊列處理與后臺任務(wù)執(zhí)行的正確性,就可以設(shè)置為同步功能,這樣就能快速而準(zhǔn)確地對該任務(wù)所代表的功能進(jìn)行測試,并及時收獲反饋。同時,我們可以在持續(xù)集成服務(wù)器上建立一個專門的管道(pipeline),用以運行基于消息處理的異步版本。這個管道對應(yīng)的任務(wù)可以通過手動執(zhí)行,也可以對管道設(shè)置定時器,在指定時間執(zhí)行(例如在凌晨兩點執(zhí)行一次,這樣在第二天開始工作之前可以獲得反饋)。我們需要為該管道準(zhǔn)備特定的執(zhí)行環(huán)境,并將后臺任務(wù)的偵聽與執(zhí)行時間修改為可以接受的值。這樣既能夠及時了解功能是否正確,又能保證基于消息的系統(tǒng)是工作正常的。

當(dāng)然,分布式系統(tǒng)還存在解析消息、網(wǎng)絡(luò)傳遞的性能損耗。對于這些問題,需要架構(gòu)師審慎地分析業(yè)務(wù)場景,正確地選擇架構(gòu)方案與架構(gòu)模式。相比較本地系統(tǒng)而言,分布式系統(tǒng)的維護(hù)難度可能成倍遞增。這既需要我們在進(jìn)行架構(gòu)決策與設(shè)計時,充分考慮系統(tǒng)架構(gòu)的穩(wěn)定性,同時還需要引入系統(tǒng)日志處理。更好的做法是為日志處理增加錯誤通知的功能,只要發(fā)生消息處理的錯誤信息,就通過郵件、短信等方式通知系統(tǒng)管理員,及時地處理錯誤。因為只有在發(fā)生錯誤的當(dāng)時查詢錯誤日志,才能夠更好對問題進(jìn)行定位。同時,還可以為系統(tǒng)引入Error Message Queue以及Dead Message Queue,以便于處理錯誤和異常情況。

對于分布式系統(tǒng)而言,還需要考慮服務(wù)執(zhí)行結(jié)果的一致性,尤其是當(dāng)某個業(yè)務(wù)需要多個服務(wù)參與到一個會話中時,一旦某個服務(wù)發(fā)生故障,就可能導(dǎo)致應(yīng)用出現(xiàn)狀態(tài)不一致的情況,因為只有所有參與者都成功執(zhí)行了任務(wù),才能視為完全成功。這就牽涉到分布式事務(wù)的問題,此時任務(wù)的執(zhí)行就變成了事務(wù)型的:即任務(wù)必須是原子的,結(jié)果狀態(tài)必須保持一致。在任務(wù)處理過程中,狀態(tài)修改是彼此隔離的,成功的狀態(tài)修改在整個事務(wù)執(zhí)行過程中是持久的。這就是事務(wù)的ACID(Atomic,Consistent,Isolated與Durable)屬性。

一種方案是引入分布式事務(wù)協(xié)調(diào)器,即DTC(Distributed Transaction Coordinator),將事務(wù)分為兩段式甚至三段式提交,要求整個事務(wù)的所有參與者以投票形式?jīng)Q定事務(wù)是完全成功還是失敗。另一種方案是降低對結(jié)果一致性的要求。根據(jù)eBay的最佳實踐,考慮到分布式事務(wù)的成本,獲得分布式資源即時的一致性是不必要的,也是不現(xiàn)實的。在Randy Shoup的文章《可伸縮性最佳實踐:來自eBay的經(jīng)驗》中提到了Eric Brewer的CAP公理:分布式系統(tǒng)的三項重要指標(biāo)——一致性(Consistency)、可用性(Availability)和 分區(qū)耐受性(Partition-tolerance)——在任意時刻,只有兩項能同時成立。我們應(yīng)該根據(jù)不同的應(yīng)用場景,權(quán)衡這三個要素。在不必要保證即時的一致性前提下,我們可以考慮合理地劃分服務(wù),盡量將可能作用在同一個事務(wù)范圍的業(yè)務(wù)操作部署在同一個進(jìn)程中,以避免分布式部署。如果確實需要多個分布式服務(wù)之間保持執(zhí)行結(jié)果的一致,可以考慮引入數(shù)據(jù)核對,異步恢復(fù)事件或集中決算等手段。

數(shù)據(jù)分析咨詢請掃描二維碼

若不方便掃碼,搜微信號:CDAshujufenxi

數(shù)據(jù)分析師資訊
更多

OK
客服在線
立即咨詢
客服在線
立即咨詢
') } function initGt() { var handler = function (captchaObj) { captchaObj.appendTo('#captcha'); captchaObj.onReady(function () { $("#wait").hide(); }).onSuccess(function(){ $('.getcheckcode').removeClass('dis'); $('.getcheckcode').trigger('click'); }); window.captchaObj = captchaObj; }; $('#captcha').show(); $.ajax({ url: "/login/gtstart?t=" + (new Date()).getTime(), // 加隨機(jī)數(shù)防止緩存 type: "get", dataType: "json", success: function (data) { $('#text').hide(); $('#wait').show(); // 調(diào)用 initGeetest 進(jìn)行初始化 // 參數(shù)1:配置參數(shù) // 參數(shù)2:回調(diào),回調(diào)的第一個參數(shù)驗證碼對象,之后可以使用它調(diào)用相應(yīng)的接口 initGeetest({ // 以下 4 個配置參數(shù)為必須,不能缺少 gt: data.gt, challenge: data.challenge, offline: !data.success, // 表示用戶后臺檢測極驗服務(wù)器是否宕機(jī) new_captcha: data.new_captcha, // 用于宕機(jī)時表示是新驗證碼的宕機(jī) product: "float", // 產(chǎn)品形式,包括:float,popup width: "280px", https: true // 更多配置參數(shù)說明請參見:http://docs.geetest.com/install/client/web-front/ }, handler); } }); } function codeCutdown() { if(_wait == 0){ //倒計時完成 $(".getcheckcode").removeClass('dis').html("重新獲取"); }else{ $(".getcheckcode").addClass('dis').html("重新獲取("+_wait+"s)"); _wait--; setTimeout(function () { codeCutdown(); },1000); } } function inputValidate(ele,telInput) { var oInput = ele; var inputVal = oInput.val(); var oType = ele.attr('data-type'); var oEtag = $('#etag').val(); var oErr = oInput.closest('.form_box').next('.err_txt'); var empTxt = '請輸入'+oInput.attr('placeholder')+'!'; var errTxt = '請輸入正確的'+oInput.attr('placeholder')+'!'; var pattern; if(inputVal==""){ if(!telInput){ errFun(oErr,empTxt); } return false; }else { switch (oType){ case 'login_mobile': pattern = /^1[3456789]\d{9}$/; if(inputVal.length==11) { $.ajax({ url: '/login/checkmobile', type: "post", dataType: "json", data: { mobile: inputVal, etag: oEtag, page_ur: window.location.href, page_referer: document.referrer }, success: function (data) { } }); } break; case 'login_yzm': pattern = /^\d{6}$/; break; } if(oType=='login_mobile'){ } if(!!validateFun(pattern,inputVal)){ errFun(oErr,'') if(telInput){ $('.getcheckcode').removeClass('dis'); } }else { if(!telInput) { errFun(oErr, errTxt); }else { $('.getcheckcode').addClass('dis'); } return false; } } return true; } function errFun(obj,msg) { obj.html(msg); if(msg==''){ $('.login_submit').removeClass('dis'); }else { $('.login_submit').addClass('dis'); } } function validateFun(pat,val) { return pat.test(val); }