一、简介
消息中间件因为其特性在大型互联网项目中随处可见,其主要核心作用体现在我们经常提到的服务解耦、削峰填谷、异步通知等功能,通过发布订阅模式实现广播、点对点的通信方式。
时下业界内比较流行的消息队列还是比较多的,当然应用广泛的且各方面性能都很优势的,大概有Kafka、本文将要介绍的RocketMQ以及最近比较火的Pulsar等。每种消息中间件都都有自己的优势,需要根据业务特性进行选型,利用优势解决问题。
本文介绍的RocketMQ是由阿里巴巴在2012年开源, 并捐赠给Apache基金会,已经于2016年11月成为 Apache 孵化项目,定义为一款开源的、java语言编写的、队列模型的分布式消息中间件,支持事务消息、顺序消息、批量消息、定时消息、消息回溯、消息过滤等功能。下图展示了RocketMQ和业界内比较流行的消息队列的性能特点比较。
RocketMQ、ActiveMQ以及Kafka消息中间件对比(RocketMQ官网):
中间件 | 客户端SDK | 协议和规范 | 有序消息 | 调度消息 | 批量消息 | 广播消息 | 消息过滤 | 服务触发 | 消息存储 | 消息回溯 | 消息优先级 | 高可用&容错 | 消息轨迹 | 配置 | 管理操作工具 |
ActiveMQ | Java, .NET, C++ etc | 推送模型,支持openwire、stomp、amqp、mqtt、jms | 独占消费者或独占队列可以确保排序 | 支持 | 不支持 | 支持 | 支持 | 不支持 | 支持非常快的、高性能的使用JDBC的存储系统,例如levelDB, kahaDB | 支持 | 支持 | 支持,取决于存储,如果使用kahadb,则需要zookeeper服务 | 不支持 | 默认低配级别,用户按需优化配置 | 支持 |
Kafka | Java, Scala etc | 拉取模型,支持tcp | 确保有序消息在同一个分区中 | 不支持 | 支持,使用异步生产 | 不支持 | 支持,使用kafka流过滤消息 | 不支持 | 高性能文件存储 | 支持,偏移量 | 不支持 | 支持,需要使用zookeeper服务 | 支持 | 键值对格式进行配置。可以从文件或编程方式提供 | 支持 |
RocketMQ | Java, C++, Go etc | 拉取模型,支持tcp,jms, OpenMessaging | 确保消息的严格排序,并且可以优雅地扩展 | 支持 | 支持,使用同步模式避免消息丢失 | 支持 | 支持 | 支持 | 高性能、低延迟的文件存储 | 支持,时间戳和偏移量 | 不支持 | 支持主从模式,不需要其他中间件 | 支持 | 开箱即用,用户只需注意几个配置 | 支持 |
上图来自于RocketMQ官网,业界经常会对Kafka和RocketMQ做对比,二者在性能和功能上都是很优秀的消息中间件,而ActiveMQ正是RocketMQ诞生的最初原因,三者在性能和功能上的概括和对比描述可参考上图。
二、结构
下面从技术架构和部署结构上了解一下RocketMQ的实现方式,方便我们易于了解它的工作原理,为后边理解源码工作流程打基础。
RocketMQ技术架构如下:
主要是四部分组成:
-
Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
-
Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
-
NameServer:NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。
NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。 -
BrokerServer:Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块,如下图所示:
1.Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
2.Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
3.Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
4.HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
5.Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。
RocketMQ部署角色如下:
1.NameServer Cluster是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
2.Broker Cluster部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave,Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
3.Producer Cluster与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
4.Consumer Cluster与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
RocketMQ逻辑部署结构如下:
Producer Group
用来表示一个发送消息应用,一个Producer Group下包含多个Producer实例,可以是多台机器,也可以是一台机器的多个进程,或者一个进程的多个Producer对象。一个Producer Group可以发送多个Topic消息,Producer Group作用如下:
1.标识一类Producer
2.可以通过运维工具查询这个发送消息应用下有多个Producer实例
3.发送分布式事务消息时,如果Producer中途意外宕机,Broker会主动回调Producer Group内任意一台机器来确认事务状态
Consumer Group
用来表示一个消费消息应用,一个Consumer Group下包含多个Consumer实例,可以是多台机器,也可以是多个进程,或者是一个进程的多个Consumer对象。一个Consumer Group下的多个Consumer以均摊方式消费消息,如果设置为广播方式,那么这个Consumer Group下的每个实例都消费全量数据。
Topic
Topic 是一种消息的逻辑分类,比如说你有订单类的消息,也有库存类的消息,那么就需要进行分类,一个是订单 Topic 存放订单相关的消息,一个是库存 Topic 存储库存相关的消息。
Message
Message 是消息的载体。一个 Message 必须指定 topic,相当于寄信的地址。Message 还有一个可选的 tag 设置,以便消费端可以基于 tag 进行过滤消息。也可以添加额外的键值对,例如你需要一个业务 key 来查找 broker 上的消息,方便在开发过程中诊断问题。
Tag
标签可以被认为是对 Topic 进一步细化。一般在相同业务模块中通过引入标签来标记不同用途的消息。
RocketMQ数据存储结构:
从图可知,RocketMQ采取了一种数据与索引分离的存储方法,消息存储通过二级索引来进行,其中实际消息存储在Commit Log的逻辑队列中(磁盘文件消息顺序写),consume queue保存着每个消息消费队列的待消费的数据并且指向commit Log,有效降低文件资源、IO资源,内存资源的损耗。即便是海量数据,高并发场景也能够有效降低端到端延迟,并具备较强的横向扩展能力。
集群工作流程:
1.启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
2.Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
3.收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
4.Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
5.Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
三、特性
RocketMQ的特性概括如下:
- 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点
- Producer、Consumer、队列都可以分布式
- Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合
- 能够保证严格的消息顺序
- 提供丰富的消息拉取模式
- 高效的订阅者水平扩展能力
- 实时的消息订阅机制
- 亿级消息堆积能力
- 较少的依赖
1.消息顺序
消息顺序是指消费者在进行消息消费时,要按照消息发送的顺序进行消费,比如在购买商品时下单产生的消息:订单创建、订单支付、订单完成,消费者在进行消费的时候要按照这个顺序消费才是有意义的,且订单之间是并发消费的,RocketMQ可以严格的支持顺序消费,分为全局有序和局部有序:
全局有序:是指在一个Topic内消息都是顺序消费的,满足FIFO的消费方式,这种方式性能是比较低的,因此适合对性能要求不高,需要整个Topic的消息都是顺序发送和消费的。
局部有序:是指一个Topic内,分区内的消息是顺序发送和消费的,也就是说是同分区内的消息是顺序发送和消费的,这种方式重在挑选决定分区的字段,也就是Sharding key的选取,适合对性能要求比较高,分区内可以满足FIFO的顺序消息的场景。
RocketMQ实现的是消息局部有序,以Topic为消费最小单位,在内部则是以队列进行区分的,队列对应就是上面提到的分区概念,也就是说RocketMQ在队列内是能保证FIFO的顺序发送和消费的,因为如果需要实现整个Topic都是有序的则可以指定消息有一个队列。可以依据业务进行区分。
2.消息过滤
RocketMQ的消息过滤是在Broker端实现的,这样可以避免不需要的消息发送至consumer侧,耗费资源增加额外的负担。从支持的功能性上来看,支持低级和高级两种用法:
Message Tag过滤:Producer在消息发送时,指定消息的Tag标签,Consumer进行消息订阅的时候,可以通过指定Tag进行消息的过滤,这种使用方式比较简单,当然功能也是单一的,下面以一个订单的流程产生的三个消息为例:
Service Filter过滤:使用类似于SQL标准的过滤方式,丰富了消息过滤的方式,满足一些稍微复杂的消息过滤需求,逻辑含义如下:
3.消息可靠性
RocketMQ在消息的可靠性上是做了一定的保障的,另外提供了一些参数在高可靠和高性能上可以进行配置调整,供用户依据业务进行选择,下面我们依据可能发生的意外情况来看下RocketMQ的可靠性如何保证:
- Broker非正常关闭
- Broker异常Crash
- OS Crash
- 机器掉电,但是能立即恢复供电情况
- 机器无法开机(可能是cpu、主板、内存等关键设备损坏)
- 磁盘设备损坏
1)、2)、3)、4) 四种情况都属于硬件资源可立即恢复情况,RocketMQ在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。
5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与钱相关的应用。注:RocketMQ从3.0版本开始支持同步双写。
4.至少一次
RocketMQ支持至少一次的消息投递,consumer在pull消息之后,成功消费之后,会返回一个ACK给Broker,这样才代表这个消息是消费成功的,因此可以保证至少一次的特性。
5.消息回溯
消息回溯:是指消费之前消费过的消息,由于业务的需要,需要对某个时间点前的消息进行再次消费,这个要求Broker对历史消息做了存储,这样才能进行消息回溯。一般需要此功能的场景是Cosumer由于出现问题,需要对某个时间之后的消息进行消费,则可以使用时间条件进行消息回溯。RocketMQ支持消息回溯,以时间戳的方式。
6.事务消息
RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。
7.定时消息
定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。 broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。可以配置自定义messageDelayLevel。注意,messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。level有以下三种情况:
- level == 0,消息为非延迟消息
- 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
- level > maxLevel,则level== maxLevel,例如level==20,延迟2h
定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。
需要注意的是,定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高。
8.消息重试
Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer消费消息失败通常可以认为有以下几种情况:
- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以最好提供一种定时重试机制,即过10秒后再重试。
- 由于依赖的下游应用服务不可用,例如db连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用sleep 30s,再消费下一条消息,这样可以减轻Broker重试消息的压力。
RocketMQ会为每个消费组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。
9.消息重投
生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer负载变化也会导致重复消息。如下方法可以设置消息重试策略:
- retryTimesWhenSendFailed:同步发送失败重投次数,默认为2,因此生产者会最多尝试发送retryTimesWhenSendFailed + 1次。不会选择上次失败的broker,尝试向其他broker发送,最大程度保证消息不丢。超过重投次数,抛出异常,由客户端保证消息不丢。当出现RemotingException、MQClientException和部分MQBrokerException时会重投。
- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。
- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。
10.流量控制
生产者流控,因为broker处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。
生产者流控:
- commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,参数默认为1000ms,返回流控。
- 如果开启transientStorePoolEnable == true,且broker为异步刷盘的主机,且transientStorePool中资源不足,拒绝当前send请求,返回流控。
- broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,返回流控。
- broker通过拒绝send 请求方式实现流量控制。
注意,生产者流控,不会尝试消息重投。
消费者流控:
- 消费者本地缓存消息数超过pullThresholdForQueue时,默认1000。
- 消费者本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB。
- 消费者本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000。
消费者流控的结果是降低拉取频率。
11.死信队列
死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在RocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。
四、资源地址
官网:http://rocketmq.apache.org/
文档:
http://jm.taobao.org/2017/01/12/rocketmq-quick-start-in-10-minutes/