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。
为了能够更好地使用WildFly的消息子系统,有必要对其专用术语做一下说明。其中有两组概念比较重要:
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服务器上)。
【笔者观点】
消息复制是高可用性的前提功能,在集群环境中通过消息复制保持主(Master)节点和从(Slave)节点的状态对等(消息一致),当主节点失效后,从节点能够立刻替代主节点保证客户应用程序的运行不受影响。在消息持久化中讲到了消息持久化的3种模式,集群中的各节点在模式2(NFS)和3(GFS)的场景下可以通过共享文件系统保证消息一致;在模式1的场景下,要保证主从节点间的消息一致需要通过消息复制来实现。
图 5 消息复制
[注意事项]
在某节点被标识为从节点,并启动后,主节点上已经有消息(persistent journal)存在的情况下,从节点首先会从主节点上同步已存在的数据,在同步完成之前无法提供容错 功能。
试想以下两种消息处理场景:
场景1
消息由消息发送者(message sender)到消息目标服务器(message target),目标服务器或者网络在消息发送之后,目标服务器接受到消息之前发生故障。
场景2
消息由消息发送者(message sender)到消息目标服务器(message target),目标服务器或者网络在消息到达目标服务器,并且由目标服务器对消息处理完成之后,目标服务器返回响应之前发生故障。
消息发送者没有办法对以上两种场景进行辨别,统一做消息重新发送。对于场景2而言,同样的消息消费了2次。这对于一些订购系统(比如网上购物)而言,如果不做消息去重,在场景2中,对于同一件物品发生2次订购,对于消费者而言是不可接受的。
HornetQ提供了消息去重的机制,实现思路如下图所示:
图 6 消息去重原理
从上图可以看出,HornetQ的消息去重实现原理很简单:
【笔者观点】
上述的处理逻辑在一定程度上可以避免消息去重,在极端情况下(目标服务器缓存也崩溃的时候)也难以避免消息被重复处理的情况。如果要考虑到极端情况的处理,就要牺牲一定的性能特别是分布式场景下。在实际业务场景中,比如订单系统与积分系统,支付系统,物流系统等系统间消息投递的场景中,出于性能考虑,一般不考虑如此极端的场景。淘宝/阿里的消息中间件(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上面,这种情况下HornetQ也难以保证消息消费顺序的正确性。当然可以通过修改负载均衡算法,借助类似于sticky session的技术将来自于同一session的消息,都发往同一个HornetQ服务器上的同一个Destination。
吴杰(新浪微博@WildJay),南京JUG联合创始人;05年始在富士通南大软件技术有限公司,从事富士通中间件产品研发8年,现就职于苏宁云商电子商务总部信息中心负责中间件相关事宜。