WildFly,前身是JBoss AS,從V8開始為區別於JBoss EAP,更名為WildFly。HornetQ是JBoss開發的一個獨立的消息中間件,被整合進WildFly作為消息子系統。
HornetQ完全支持JMS,HornetQ不但支持JMS1.1 API同時也定義屬於自己的消息API(如下圖中的Core Client),以最大限度地提升HornetQ的性能和靈活性。
圖 1 客戶程序HornetQ的兩種交互模式
關於Core Client的API介紹請參見:http://docs.jboss.org/hornetq/2.3.0.CR2/docs/api/hornetq-client/
HornetQ與JMS保持一致支持兩種消息類型:Point-to-Point和Publish/Subscribe。
Point-to-Point
圖 2 消息類型之Point-to-Point
URL:http://www.bianceng.cn/Programming/Java/201410/45833.htm
Publish/Subscribe
圖 3 消息類型之Publish/Subscribe
為了能夠更好地使用WildFly的消息子系統,有必要對其專用術語做一下說明。其中有兩組概念比較重要:
Acceptors and Connectors
Invm and Netty
Acceptor
指定HornetQ Server接受什麼類型的連接(Connection)
Connector
為客戶端指定連接HornetQ Server的方式
相關配置定義在standalone以及domain的profile中,以下列舉片段供參考。
<connectors> <netty-connector name="netty" socket-binding="messaging" /> <netty-connector name="netty-throughput" socket-binding="messaging-throughput"> <param key="batch-delay" value="50" /> </netty-connector> <servlet-connector name="servlet" socket-binding="http" host="default-host" /> <in-vm-connector name="in-vm" server-id="0" /> </connectors> <acceptors> <netty-acceptor name="netty" socket-binding="messaging" /> <netty-acceptor name="netty-throughput" socket-binding="messaging-throughput"> <param key="batch-delay" value="50" /> <param key="direct-deliver" value="false" /> </netty-acceptor> <in-vm-acceptor name="in-vm" server-id="0" /> </acceptors>
Acceptor和Connector是個相對的概念,因此定義時需要成對定義。而Invm和Netty就是用來定義Client和HornetQ Server是否在同一個JVM中。Invm標識Client和HornetQ Server在同一個JVM中;Netty標識Client和HornetQ Server在不同的JVM中
WildFly中的消息是默認做持久化並持久化到文件中(Persistent Journal, 請參見圖15客戶程序HornetQ的兩種交互模式)。文件操作有以下兩種方式:
Java Non-blocking IO (NIO)
利用Java標准的NIO API操作文件以獲取更好的性能,需要Java SE 6及更新版本。
Linux Asynchronous IO (AIO)
使用Linux的本地異步IO庫進行操作,對Linux(內核2.6及以上)系統強依賴。該方式性能優於Java NIO。
WildFly默認使用AIO進行消息持久操作,以獲取最佳性能 ,如果在不具備Linux AIO的條件下,會自動切換到Java NIO方式進行消息持久化。
圖 4 消息持久化場景模式
從上圖中可以看出,WildFly的消息子系統中消息持久化除了支持本地文件系統操作,也支持NFS,基於SAN的GFS V2共享文件系統的操作。
[注意事項]
(1) 在使用模式2或者3時,Linux AIO為唯一文件操作方式。
(2) 如果使用模式1,即每個HornetQ服務器都將消息持久化到所在主機的本地文件系統,在做HornetQ服務器的HA特性(Failoerver)時,需要做消息復制(將消息日志由主HornetQ服務器復制到從HornetQ服務器上)。
【筆者觀點】
HornetQ的消息持久化方式比較單一,沒有靈活的持久化方式(比如數據庫持久化)供用戶選擇或定制。好在提供了通過NFS或者GFS V2 on SAN進行消息持久化共享的方式,從而避免了在集群情況下由於做消息復制而造成的性能損耗。
在消息中間件負載要求過高的場景下,如果在消息持久層(文件系統)與HornetQ集群之間加入緩存集群(Infinispan)做消息共享,可以提供更好的HA特性。
消息復制是高可用性的前提功能,在集群環境中通過消息復制保持主(Master)節點和從(Slave)節點的狀態對等(消息一致),當主節點失效後,從節點能夠立刻替代主節點保證客戶應用程序的運行不受影響。在消息持久化中講到了消息持久化的3種模式,集群中的各節點在模式2(NFS)和3(GFS)的場景下可以通過共享文件系統保證消息一致;在模式1的場景下,要保證主從節點間的消息一致需要通過消息復制來實現。
圖 5 消息復制
URL:http://www.bianceng.cn/Programming/Java/201410/45833.htm
[注意事項]
在某節點被標識為從節點,並啟動後,主節點上已經有消息(persistent journal)存在的情況下,從節點首先會從主節點上同步已存在的數據,在同步完成之前無法提供容錯 功能。
試想以下兩種消息處理場景:
場景1
消息由消息發送者(message sender)到消息目標服務器(message target),目標服務器或者網絡在消息發送之後,目標服務器接受到消息之前發生故障。
場景2
消息由消息發送者(message sender)到消息目標服務器(message target),目標服務器或者網絡在消息到達目標服務器,並且由目標服務器對消息處理完成之後,目標服務器返回響應之前發生故障。
消息發送者沒有辦法對以上兩種場景進行辨別,統一做消息重新發送。對於場景2而言,同樣的消息消費了2次。這對於一些訂購系統(比如網上購物)而言,如果不做消息去重,在場景2中,對於同一件物品發生2次訂購,對於消費者而言是不可接受的。
HornetQ提供了消息去重的機制,實現思路如下圖所示:
圖 6 消息去重原理
從上圖可以看出,HornetQ的消息去重實現原理很簡單:
消息發送者為每一條消息附加帶唯一值(官方建議用UUID)的消息頭;
目標服務器在接收消息之後,處理消息之前,先從本地緩存(Duplicate ID Cahce)中查找該消息ID是否已經存在;
如果消息頭在本地緩存中已經存在則忽略該消息;如果不存在則處理該消息;
目標服務器處理完消息後在本地緩存中緩存該消息的消息頭,以供去重檢測用。
【筆者觀點】
上述的處理邏輯在一定程度上可以避免消息去重,在極端情況下(目標服務器緩存也崩潰的時候)也難以避免消息被重復處理的情況。如果要考慮到極端情況的處理,就要犧牲一定的性能特別是分布式場景下。在實際業務場景中,比如訂單系統與積分系統,支付系統,物流系統等系統間消息投遞的場景中,出於性能考慮,一般不考慮如此極端的場景。淘寶/阿裡的消息中間件(Notify與MetaQ)都沒有為極端場景做特別設計。
鑒於目前分布式緩存大行其道,比如Teracotta的BigMemory,Oracle Coherence等等,可以采用類似於統一Session管理的方案,對Duplicate ID也做統一管理,這樣集群中無論哪一個節點崩潰都可以避免消息重復消費的情況。
為了說明消息的順序消費的重要性,下圖中勾畫了一個網上購物的場景。
圖 7 嚴格消息順序消費場景
① A客戶訂購一台iPad 4
② 訂購消息加入消息隊列
③ A客戶取消①中訂購的iPad4
④ 取消訂購消息加入消息隊列
⑤ 從隊列中消費訂購消息
⑥ 從隊列中消費取消訂購消息
⑦ 往數據庫中寫入訂購消息
⑧ 從數據庫中刪除訂購消息
如果⑦和⑧的處理順序顛倒,將導致客戶的訂購沒有取消成功。
如何保證消息消費的順序呢?
JMS規范(截至JMS2.0)僅僅對“一個生產者,一個QUEUE,一個消費者”的場景做了“消息的發送順序必須與消費順序嚴格一致”的規定,但對於分布式環境中,沒有對消息發送與接受的順序一致做強制要求。因此嚴格順序保證依賴各消息中間件提供商的具體實現。
IBM的WebSphere MQ中的消息分組與Oracle的WebLogic JMS的Message Unit-of-Order都可以解決上述場景中的問題。HornetQ也提供了解決方案:Message Grouping。
Message Grouping通過將同一業務類型的消息分為一組,確保該組中的所有消息被同一個消費者消費(即使在集群環境中),從而確保消息能夠被順序消費。通過HornetQ的Message Grouping圖21的消息消費路由將變成(如下圖所示)。
圖 8 采用Message Grouping 後的消息順序消費
【筆者觀點】
HornetQ的Message Grouping方案有以下前提:
Queue中消息順序是正確的。即需要消息發送端意識到消息的先後順序
消費端不可以使用多線程去處理消息。
另外需要注意的是,集群環境中由於負載均衡消息可能分布在不同的Queue上面,這種情況下HornetQ也難以保證消息消費順序的正確性。當然可以通過修改負載均衡算法,借助類似於sticky session的技術將來自於同一session的消息,都發往同一個HornetQ服務器上的同一個Destination。