基於WCFMSMQ構建釋出/訂閱訊息匯流排(Pub/Sub Message Bus

 
本文是翻譯Tom Hollander先生的blog文章《Building a Pub/Sub Message Bus with WCF and MSMQ》,對英文blog文章感興趣的朋友,可以直接訪問文章下面的連結。-譯者:EntLib.com開源論壇小組
 
這幾年經常談到採用事件驅動架構(event-driven architecture)技術來構建可擴充套件的、可維護的系統。我發現在一些方案中,這是非常有趣的模型。但是,在Microsoft平臺上這種架構一直沒有很好的支援。因此,許多人發現比較難以實現。幾年以前,在一個系統中,我採用.Net Remoting / MSMQ / HTTP 構建了釋出/訂閱訊息匯流排(Pub/Sub Message Bus),但是覺得不是很完美。很多地方實現比較困難,並且需要定製的程式碼,如承載佇列監聽(host the queue listeners)、編碼/解碼訊息、處理可靠性和管理訂閱者等等。
 
在我目前的專案中,我再一次嘗試了這一模型。然而,這幾年技術發展有很大的變化,我高興的說:我的體驗比以前要好多了。在說明我的方案之前,我想提醒的一點是:我僅僅是描述實現這一模型的一種方法,在不同的應用需求情況下,應該有其他更合適的方法。因為我工作的專案是一個大的.Net應用系統,因此跨平臺的互操作不需要考慮(真幸運!)。
 
我們完成的專案構建在.NET Framework 3.0 平臺,並使用了WCF / MSMQ 4.0 / IIS 7.0,部署在Windows Server 2008平臺上。
 
定義Service Contract
第一步定義Contact,釋出者用來通知訂閱者“發生事件”。在我們的專案中,有許多不同的事件型別,但是為了儘可能重用程式碼,我們使用了泛型Service Contract:
[ServiceContract]
public interface IEventNotification<TLog>
{
    [OperationContract(IsOneWay = true)]
    void OnEventOccurred(TLog value);
}   
 
對於任一事件型別,我們可以簡單定義一個Data Contract 來負載資料(carry the payload),並提供一個繼承的Service Contract 型別,如下所示:
[ServiceContract]
public interface IAccountEventNotification : IEventNotification<AccountEventLog>
{
}
實現釋出者(Publisher
釋出/訂閱模型中最重要的一點是釋出和訂閱方應該是低耦合的。尤其是釋出者不應該知道訂閱者的任何事情,包括多少個訂閱者和訂閱者在哪裡。起初,我們試圖採用MSMQ的PGM多播特性來實現 – 實質上讓你定義單個佇列地址,悄悄地路由相同的訊息到多個目標佇列。雖然這可以滿足要求,但是這一方案也存在一些缺點。第一,在WCF中使用多播佇列地址的唯一方式是採用MsmqIntegrationBinding,MsmqIntegrationBinding沒有NetMsmqBinding靈活。其次,多播地址僅僅適用非事務佇列(non-transactional queues),這樣對我們的系統產生不能接受的可靠性影響。
 
因此,我們放棄了這一方案,決定在釋出者實現我們自己的輕量級多播。技術上而言,這樣打破了“釋出者完全不瞭解訂閱者”這一黃金規則,所有訂閱者的資訊完全存放在配置檔案中。這意味著,我們增加、修改或刪除訂閱者完全不影響程式碼。
 
我們開發一個元件-ServiceFactory(與p&p Web Service Software Factory 沒有關係),ServiceFactory只是一個簡單的抽象,通過讀取配置檔案來建立本地或WCF例項。ServiceFactory元件沒有對外公佈,但是你可以簡單替換為你喜歡的依賴注入框架(Dependency Injection Framework),達到相同的效果。在我們的系統中,Web Services的配置檔案web.config 可能有如下定義的依賴服務:
<serviceFactory>
    <
services>
        <
addname=”EmailUtility” contract=”MyProject.IEmailUtility, MyProject” type=”MyProject.EmailUtility, MyProject” mode=”SameAppDomain” instanceMode=”Singleton” enablePolicyInjection=”false” />
        <
addname=SubsctiberXAccountEventNotificationcontract=MyProject.Contracts.IAccountEventNotification, MyProject.Contractsmode=Wcfendpoint=SubsctiberXAccountEventNotification />
        <
addname=”SubsctiberYAccountEventNotification” contract=”MyProject.Contracts.IAccountEventNotification, MyProject.Contracts” mode=”Wcf” endpoint=”SubsctiberYAccountEventNotification” />
    </
services>
</
serviceFactory>
 
我們使用ServiceFactory建立單個例項,程式碼如下:
IEmailUtility email = ServiceFactory.GetService<IEmailUtility>();
 
根據上面的配置檔案,上述程式碼將獲得一個本地EmailUtility單件例項,但是不同的配置可以返回一個WCF proxy 代理類例項。可以非常方便重用ServiceFactory元件,返回所有配置的、匹配特定Contract的服務。我們將根據這些,構建NotificationPublisher類,程式碼如下:
public class NotificationPublisher<TInterface, TLog>
    where TInterface : class, IEventNotification<TLog>                   
{
    public static void OnEventOccurred(TLog value)
    {
        List<TInterface> subscribers = ServiceFactory.GetAllServices<TInterface>();
 
        foreach (TInterface subscriber in subscribers)
        {
            subscriber.OnEventOccurred(value);
        }
    }
}
 
根據上述程式碼,釋出者釋出事件所需要做的是:傳入合適的泛型引數,例項化NotificationPublisher物件,並呼叫OnEventOccured 方法。假定我們使用IAccountEventNotification 介面和上述配置,這樣事件將通過WCF到達SubscriberXAccountEventNotification 和 SubscriberYAccountNotification 端點,並觸發相關事件。
 
配置釋出者
釋出端最後一部分是WCF配置。如上述所提及的,我們選擇使用MSMQ提供可靠的、非同步的訊息傳遞。過去編寫MSMQ程式碼比較困難,但是對WCF程式設計模型而言,MSMQ與其他傳輸協議沒什麼區別。在我們的案例中,我們選擇了NetMsmqBinding,NetMsmqBinding 為核心MSMQ特性提供了全面訪問WCF功能(與MsmqIntegrationBinding不同,MsmqIntegrationBinding提供了更豐富的MSMQ支援,但是限制了WCF功能)。
 
如下是客戶端的WCF的配置示例:
<system.serviceModel>

    <

bindings>
        <
netMsmqBinding>
           <
bindingname=TransactionalMsmqBindingexactlyOnce=truedeadLetterQueue=System” />
        </
netMsmqBinding>
    </
bindings>

    <

client>
        <
endpointname=SubscriberXAccountEventNotification
            address=net.msmq://localhost/private/SubscriberX/accounteventnotification.svc
            binding=netMsmqBindingbindingConfiguration=TransactionalMsmqBinding
            contract=MyProject.Contracts.IAccountEventNotification” />
   
        <
endpointname=SubscriberYAccountEventNotification
            address=net.msmq://localhost/private/SubscriberY/accounteventnotification.svc
            binding=netMsmqBindingbindingConfiguration=TransactionalMsmqBinding
            contract=MyProject.Contracts.IAccountEventNotification” />
    </
client>
</
system.serviceModel>
 
上述配置沒什麼特別的地方 – 需要關注的是 exactlyOnce=”true” 設定,這是事務佇列必須的設定。另外就是 net.msmq:// 地址語法,這是NetMsmqBinding 協議所需要的。私有佇列分別為 SubscriberX/accounteventnotification.svc 和 SubscriberY/accountnotification.svc。為什麼我給佇列這樣愚蠢的命名呢?繼續讀下面內容……
 
承載和配置訂閱者
在過去,如果說建立MSMQ客戶端是煩人的,那麼建立MSMQ服務更是噩夢。你不得不建立你自己的host(一般而言為Windows Service)或者使用一些靈活的MSMQ觸發器功能。然後,你需要做很多工作,確保你的服務沒有丟失訊息,或者沒有被“poison messages”所阻塞,因為poison message錯誤的訊息體(malformed payload)會不斷導致你的服務失敗。
 
就像在客戶端一樣,WCF需要很多工作在服務端 – 但是這些不是直接幫助承載服務和監聽佇列。幸運的是,這一問題由Windows Vista和Windows Server 2008中提供的IIS 7.0和Windows Activation Services (WAS) 輕鬆解決。IIS 7.0 負責監聽MSMQ / TCP / Named Pipes,並且啟用WCF 服務,就像 IIS 6.0監聽HTTP一樣。聽起來不錯,但是需要提醒的是 – 這些需要靈巧的配置。
 
首先,你需要在IIS中設定application 指向service,包括.svc檔案和web.config配置檔案,這與在IIS 通過HTTP部署service一樣。
 
接著,你需要建立訊息佇列,你可以通過Vista的Computer Management console 或Windows Server 2008 的Server Manager配置。佇列的名稱必須匹配application name 加上.svc 檔名,例如 SubscriberX/accounteventnotification.svc。在建立佇列時,確保標記佇列支援事務,因為隨後不能改變。你也需要設定佇列的許可權,這樣執行Net.Msmq Listener 服務的帳號(預設為NETWORK SERVICE)能夠接收訊息,任何執行Client/Publisher 都能夠傳送訊息(預設為NETWORK SERVICE)。
 
最後,你需要配置IIS和WAS 的站點和特定application支援Net.Msmq 監聽器(在開始操作之前,確保你已經安裝了WAS和non-HTTP啟用windows元件)。最簡單的辦法是使用appcmd.exe 命令列(system32inetsrv目錄下):
  • appcmd set site “Default Web Site” -+bindings.[protocol=`net.msmq`,bindingInformation=`localhost`]
  • appcmd set app “Default Web Site/SubscriberX” /enabledProtocols:net.msmq
配置好IIS後,接下來是確保service的WCF配置是正確的。如同你期望的那樣,這將與客戶端的配置非常相似。
<system.serviceModel>
    <
bindings>
        <
netMsmqBinding>
            <
bindingname=TransactionalMsmqBindingexactlyOnce=truedeadLetterQueue=SystemreceiveErrorHandling=Move“/>
        </
netMsmqBinding>
    </
bindings>

    <

services>
        <
servicename=SubscriberX.NotificationService>
            <
endpointcontract=MyProject.Contracts.IAccountEventNotification
                bindingConfiguration=TransactionalMsmqBinding
                binding=netMsmqBinding
                address=net.msmq://localhost/private/SubscriberX/accounteventnotification.svc“/>
        </
service>
    </
services>   
</
system.serviceModel>
 
值得說明的一點是:receiveErrorHandling=”Move”,這一屬性可以幫助節省我們一個月的工作。這一屬性讓WCF轉移多次重複處理失敗的訊息到MSMQ的子佇列poison,然後繼續處理下一個訊息,而不是阻塞服務。這一子佇列和遠端佇列事務性讀取等待功能是Vista 和 Windows Server 2008中MSMQ 4.0的一些新特性。
 
實現訂閱者(Subscribers)
最後一件事是實現訂閱者。當然,最多的程式碼是特定業務邏輯的實現,因為我僅僅描述service interface的實現。在我們的系統中,確保沒有訊息丟失是非常重要的。既然MSMQ能夠確保訊息的到達,因此訊息不會無緣故的消失。事實上,大多數訊息的丟失是在MSMQ成功傳遞訊息到達service 之後。有可能service 接收到訊息之後,在service 成功處理訊息之前,發生異常導致失敗(可能由於bug或配置問題)。避免這一問題最好的辦法是使用事務,跨越從佇列接收訊息和業務邏輯的處理。如果發生失敗,將回滾事-包括從佇列中接收的訊息。如果只是一個臨時的小故障,訊息將再次被成功處理。如果問題持續或是錯誤的訊息,在經過多次嘗試後,該訊息將被認為是poison 訊息。如前面提及的,該訊息將被轉移到poison 子佇列,由管理員手動處理。
 
上述所有的工作非常簡單,因為MSMQ和WCF支援所有這些特性(假定你使用事務性佇列)。你需要做的工作是由一些attributes標記你的服務實現,宣告當訊息從佇列取出後,業務邏輯應該登記事務。
 
public class NotificationService : IAccountEventNotification
{
    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
    public void OnEventOccurred(AccountEventLog value)
    {
        // Business-specific logic
    }
}
如對上述solution 的實現有疑問或改進建議,歡迎到我們的論壇([url]http://forum.entlib.com[/url] )進行交流。
 
結論
這是我最近最長的blog文章之一,這一解決方案非常強大且特別簡單去實現,這是由於WCF / MSMQ / IIS 技術的先進性。在過去,許多人(包括我)花了幾個月的時間盡力去實現pub/sub模式,往往不能達到預期的效果。現在,使用這些的技術消除了大量的定製程式碼,事實上,這篇文章中少量的程式碼和配置指令碼實現了pub/sub模式。
 
譯者注:
我們是EntLib.com 開源論壇小組,負責EntLib.com / YAF 開源ASP.NET/C# 論壇的開發工作,免費提供專案原始碼下載。歡迎您訪問、下載、交流和學習,包括Enterprise Message Bus / WCF / MSMQ / BizTalk / SSB / SQL Server / .Net Framework ……等等。
 
下載EntLib.com / YAF 開源論壇 v2.5 (ASP.NET/C#)
 
英文原文:
Building a Pub/Sub Message Bus with WCF and MSMQ, by Tom Hollander