概述
应用程序或组件之间可以使用消息中间件进行可靠的异步通讯来降低系统之间的耦合度,提高整个系统的可扩展性和可用性。
优缺点
使用消息队列的优点如下:
异步,减少请求响应时间,提升系统性能及用户体验。
削锋,应对高峰期消息过载,在低峰期消化堆积消息。
解耦,降低系统耦合性,提高系统可扩展性和可维护性。
消息队列的引入同样伴随着隐患:
系统可用性降低,引入的外部依赖越多,系统的鲁棒性越差,各系统强依赖消息队列本身的可用性。
系统的复杂度加大,如何保证消息的幂等性及有序性等问题均需考虑。
常见问题
保证消息队列的高可用:消息队列绝对不能是单点,可镜像集群或者主从集群。
保证消息的顺序性:消息队列本身可通过内部队列保证全局或局部有序性。保证有顺序则有单点问题(性能及可用性),若消息队列不保证消息的有序性,则由接收方保证,如在交易系统中,接收方采用正向状态机保证(1->2>3, 3的状态变更只能由2变过来,非法的变更暂时忽略掉),可以保证消息状态始终是最新的,保证幂等性。
保证消息的可靠传输,如何应对消息可能丢失的问题:生产时加入重试机制,写入时可通过数据持久化及事务确认机制保证,消费时可通过ack确认机制保证,消息队列本身可通过主从高可用保证。
保证消息的幂等性,不重复消费:结合具体业务,若是写数据库,每次写入前校验,逻辑去重,可通过去重表记录消息是否已处理、值变更采用比对赋值而不是直接增减;若是写缓存(Redis或Tair), set本身就保证天然幂等性;其他场景可进行消息全局Id校验。顺序和重复问题在分布式场景下,采用业务规避的方式效率是最高的,去重表还是有一定代价的。
消息队列的延时及过期失效:若因消费端问题导致消息积压,可将消费端紧急扩容临时解决;若消息队列消息超时,可低峰期批量重导。
目前有很多主流消息中间件,如ActiveMQ、RabbitMQ、Kafka、RocketMQ、Notify、MetaQ等,此处不比对各消息中间件应用场景及原理区分,仅概述集团内普遍使用的Notify和MetaQ。
Notify
概念
发送方:负责发送消息到Notify Server的应用。控制台申请了发送权限后,应用可以发送消息到Notify Server。
订阅方:订阅发送方消息的应用。控制台申请订阅权限后,订阅方引用启动成功后,Notify Server就会把消息投递到订阅方。发送方和订阅方是解耦的,发送方不需要关注订阅方的任何信息,订阅方的变更(增加/减少订阅)不会影响发送方。
Notify Server:负责接收发送方的消息,并且投递到订阅方。使发送方和订阅方实现异步通信、解耦。还可以起到削峰填谷的作用。
消息:Notify的消息主要包括四个部分:Topic、MessageType、用户自定义属性、消息体。其中,Topic、MessageType都是用来区分不同业务发送方发送的消息。
消息Id:消息的唯一标识,Notify自动生成。发送/订阅方不能调用Message.setMessageId自己设置消息Id,否则会导致消费异常。
功能
实时消息服务:推送和无序模型保障了高实时性,ms级别。无序模型,消息投递的并发度最高,而且不会因为一个消息消费失败,导致后面的消息消费不了;推送模型使得消息一旦到达服务器会立马推送给客户端,无间歇。
分布式事务:把应用的业务操作和消息发送组成一个分布式事务,保证业务操作和消息发送是个原子操作,保证交易状态最终一致性。通过message checker方式实现。
数据库双写,消息存储高度可靠。header订阅:有些业务场景比较复杂,纯粹的主题+消息类型二元组订阅已经无法满足需求。header订阅支持消息属性表达式过滤,提供更加动态灵活的订阅机制。
单元化支持:Notify具备单元化的特性,可以支持在不同单元之间的路由,把消息投递到准确的单元订阅者。
MetaQ
分布式、队列模型的消息中间件。基于发布订阅模式,分Push和Pull两种消费方式,支持严格的消费顺序,亿级别的堆积能力,支持消息回溯和多个维度的消息查询。
MetaQ采用长轮询方式从Broker拉消息,实时性同Push一致,消息的延迟时间大概几毫秒。MetaQ客户端和服务端Broker是维持长链接的,会定期30s向服务器发送自己的心跳数据,以防止服务端清理过期连接。
概念
Producer:消息生产者,负责消息的生产及发送。与NameServer集群的一个节点建立长连接,定期从NameServer获取其订阅Topic的路由信息,然后向Topic所在的broker发送消息。
Consumer:消息消费者,负责消息的拉取和消费。Consumer与NameServer集群的某个节点建立长连接,然后从NameServer上获取可以消费的Topic中某个MessageQueue所在Broker的路由信息,然后与其建立长连接,从而不断的拉取消息进行消费。
NameServer:整个消息队列的状态服务器,集群的各个组件通过它来了解全局信息,各个角色的机器会定期向NameServer上报自己的状态。如果超时不上报,NameServer 会认为某个机器出故障不可用,其他组件会把这个机器从可用列表中移除。NamerServer可以部署多个,相互之间独立,其他角色同时向多个机器上报状态信息,从而达到热备份的目的。
Broker:MetaQ的核心,分Master和容灾的Slaver。每个Broker需要与所有的NameServer建立长连接,从而获取Topic信息;Broker负责接收来自Producer发过来的消息、处理Consumer消费消息的请求、消息的持久化存储以及消息在服务端的过滤。
Group:组名,一类Producer/Consumer的集合名称,通常称为Producer/Consumer集群。
Topic:软分区,消息的主题,由用户定义并在服务端配置。
Tag:消息在Topic基础上的二级分类,每个Topic可以对应多个Tag。
Message:在生产者,消费者,服务器间传递的消息。
Message Queue:硬分区,物理上区分Topic,一个Topic可以对应多个Message Queue。
Offset:消息在服务器上的每个分区都是组织成一个文件列表,消费者拉取数据需要知道数据在文件中的偏移量,这个偏移量即是Offset。
广播消费:Producer向一些队列轮流发送消息,队列集合称为Topic,每一个Consumer实例消费这个Topic对应的所有队列,即一条消息会被同一个Group的每一个消费端消费。MetaQ广播消息不支持失败重试。
集群消费:多个Consumer实例平均消费这个Topic对应的队列集合,即一条消息只会被同一个Group里一个消费端消费。不同Group之间互不影响。
功能
消息管理控制台:用户可在此平台轻易创建发送消息的Topic,发送者,订阅者,以及自动化生成发送或消费示例代码。
消息回溯消费:通过控制台操作,可重新消费最新一段时间的消息。
完善的监控体系:展示各Topic消息堆积情况,提供报警机制。
消息轨迹追踪:消息投递成功或失败,提供消息追踪日志,方便应用查找问题。
注意事项
发布消息
1.对于非常重要的消息如订单消息,业务方需要有重复补偿机制,例如MetaQ服务短暂不可用,此时发往MetaQ的消息将失败,等MetaQ服务恢复后,业务方可以将之前发送失败的消息重新补偿发送。
2.对于Message Size特别的消息,推荐对消息进行拆分,原因如下:
- MetaQ通信层没有对大的请求做优化,采用的是典型的RPC方式,不适合大的请求传递,可能导致网络层Buffer异常。
- MetaQ服务器存储是典型的LRU系统,过大的消息会占用较多的Cache,对于其他应用Cache命中率产生影响。
- MetaQ的磁盘通常比较紧张。
- MetaQ暂不解决大消息存储问题。
订阅消息
1.MetaQ支持服务器消息过滤,订阅某个Topic,若只关心一部分数据,可以使用表达式方式过滤,可以避免无用的消息传输到客户端,降低了应用与MetaQ服务器的负载。
2.非顺序消息消费,耗时时间不做限制,但是应用应该尽可能保证耗时短,这样才能达到高性能,另外消费消息Hang住,会导致消息所在队列的消费动作暂停,直到Hang住的消息消费完。对其他队列不受影响。
Notify Vs MetaQ
- 消息推送方式:Notify主要为Push方式,解决事务消息,消息消费的发起者是Notify服务器,消费者是被动等待消息到来。MetaQ支持Push&Pull,主要为Pull方式,解决顺序消息和海量消息堆积问题。
- 实现方式:Notify是基于JMS模式实现,MetaQ是基于内部消息队列实现。
- 消息消费是否有序: 保证顺序性,即保证消息投递和消费均满足Happen Before原则,必然会导致吞吐量受限。为保证更高的吞吐量,Notify不考虑消息的顺序性,消息乱序消费。MetaQ基于内部消息队列,可以保证有序,但读取是随机读取。
- 数据持久化:Notify可以选择持久化或非持久化,MetaQ所有消息队列都是持久化且数据结构长度是无限制的。消费方式:Notify不支持集群广播,MetaQ支持集群内每台机器广播。
- 分布式事务:Notify支持分布式事务,MetaQ不支持分布式事务。
- 应用场景:Notify更适合需要保证高吞吐率的应用,如实时数据监控业务,集群日志的收集转存。MetaQ更适合消息峰值高或者需要保证消息顺序或者需要广播消息的应用,如电商的大促,抢红包,数据库binlog同步,商品库存缓存信息同步。