设计理念
RocketMq设计基于主题的发布与订阅模式,其核心功能主要包括消息发送、消息存储、消息消费、整体设计追求简单与性能第一,主要体现在如下三个方面
- 首先NameServer设计极其简单,屏蔽了业界最长使用的Zookeeper充当信息管理的“注册中心”,而是自研一款NameServer来实现元数据的管理(Topic路由信息等)。从实际需求出发,因为Topic路由信息无需在集群之间保持强一致,追求最终一致性,并且能容忍分钟级的不一致。正是基于这种情况,RocketMQ的NameServer集群之间互相不通信,极大降低了NameServer的实现复杂度,对网络的要求也降低了不少,但是性能对于Zookeeper有了极大的提升。
- 其次是高效的IO存储机制,追求消息发送的高吞吐量,存储文件设计成文件组的概念,组内单个文件大小固定,方便引入内存映射机制,减少数据拷贝。所有主题的消息存储都是基于顺序写,极大的提升了消息 写性能,同时为了消息消费和查找,引入了消息消费队列和索引文件
- 最后是容忍设计存在缺陷,适当将某些工作放给RocketMq使用者。消息中间件的实现者经常会遇到一个难题:如何保证消息一定给消息消费,并且保证只消费一次。RocketMq的设计者给出的解决方案是不解决,而是退而求其次,只保证消息被消费者消费,但设计上允许被重复消费,这样极大简化了消息中间件的内核,使得实现消息发送高可用变得非常简单和高效,消息重复问题由消费者在消费消息时实现幂等。
特性
RocketMq消息中间件的特性
主题与分区
在RocketMQ中还存在两个特别重要的概念:主题(Topic)与队列(Queue)。Rocket中的消息以主题为基本单元进行归类,生产者负责将发送到特定的主题(发送到 RocketMQ 集群中每一个消息都要指定一个主题),而消费者负责订阅主题并进行消费
主题是一个逻辑上的概念,它还可以细分为多个队列,一个队列只属于单个主题,单个主题下可以存在多个队列。同一主题下的不同队列包含的消息是不同的,队列在存储层面可以看作是一个可追加的日志(Log)文件,消息在被追加到队列日志文件的时候都会被分配一个特定的偏移量(offset)。offset是消息在队列中的唯一表示标识,RocektMQ通过它来保证消息在队列内的顺序性,不过offset并不跨队列,也就是说,RocketMQ保证的是队列内有序而不是主题有序
同一个消费者组的消费者只能消费同一主题下不同的队列,不同的消费者组的消费者可以消费相同的队列。换句话说,就是指同一个消费者组的不同消费者不能消费Topic主题下相同的队列
订阅与发布
消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息,进而从该topic消费数据。
消息顺序
消息有序是指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了 三条消息分别是创建订单,订单付款,订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMq可以严格的保证消息有序
顺序消息分为全局顺序消息与局部顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可
- 全局顺序 对于指定的一个Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。使用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景
- 分区顺序 对于指定的一个Topic,所有消息根据 sharding key 进行区块分区。同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。Sharing key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格按照 FIFO 原则进行消息发布和消费的场景
消息过滤
RocketMQ的消费者可以根据Tag进行消息过滤,也支持自定义属性过滤。消息过滤目前是在Broker端实现的,优点是减少了对于Consumer无用消息的网络传输,缺点是增加了Broker的负担、而且实现相对复杂。
消息可靠性
BrokerMQ支持消息的高可靠,影响消息可靠性的几种情况:
- Broker非正常关闭
- Broker异常Crash (宕机)
- OS Crash(宕机)
- 机器掉电,但是能立即恢复供电情况
- 机器无法开机(可能是cpu、主板、内存等关键设备损坏)
- 磁盘设备损坏
1)、2)、3)、4)四种情况都属于硬件资源可立刻回复情况,RocketMQ在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。
5)、6)属于单点故障,且无法恢复,一旦发生,在此节点上的消息全部丢失,RocektMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合。例如与 Money 相关的应用。 同步双写指的是 向同一个BrokerName下的Master节点写消息的时候,需要同步到其他Slave节点,直到都成功才会向客户端返回。
至少一次
至少一次(At least Once)指每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息,所以RocketMQ可以很好的支持此特性
回溯消费
回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后仍然需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒
事物消息
RocktMQ事物消息是指应用本地事物和发送消息操作可以被定义在全局事物中,那么同时成功,要么同时失败。RocketMQ的事物消息提供类似 XA 的分布式事务功能,通过事物消息能达到分布式事务的最终一致。
定时消息
定时消息(延迟队列)是指消息发送给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会调度地消费SHCHEDULE_TOPIC_XXXX,将消息写入真实的topic。
需要注意的是,定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高。
消息重试
Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer消费消息失败通常可以认为有以下几种情况:
- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如花费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以提供一种定时重试机制,即过10秒后再重试
- 由于依赖的下游应用服务不可用,例如db连接不可用,外系统网络不可大等,。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用sleep 30s,再消费下一条消息
RocketMQ会为每个消费者组都设置一个Topic名称为 “%RETRY%+consumerGroup“的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费者组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多延迟就越大,RocketMQ对于重试消息的处理是先保存至Topic 名称为 “SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+cousumerGroup”的重试队列中
消息重投
生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大,网络抖动,消息重复就会是大概率事件。另外,生产者主动重复、consumer负载变化也会导致重试消息。如下方法可以设置消息重试策略:
retryTImesWhenSendFailed:同步发送失败重投次数,默认为2,因此生产者会最多尝试 retryTImesWhenSendFailed + 1次。并且不会选择上次发送失败的broker,尝试向其他broker发送,最大程度保证消息不丢。超过重投次数,抛出异常,由客户端保证消息不丢。当出现 RemotingException、MqClientException和部分MQBrokerException时会重投retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送给其他broker,默认false。十分重要消息可以开启。
流量控制
生产者流控,因为broker处理能力达到瓶劲;消费者流控,因为消费能力达到瓶颈。
生产者流控:
commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时候,参数默认为1000ms,返回流控- 如果开启
transientStorePoolEable==true,且broker为异步刷盘的主机,且transientStorePool中资源不足,拒绝当前send请求,返回流控 broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,返回流控broker通过拒绝send请求方式实现流量控制
注意,生产者流控,不会尝试消息重投。
消费者流控:
- 消费者本地缓存消息数超过
pullThresholdForQueue时,默认1000 - 消费者本地缓存消息大小超过
pullThreadsholdSizeQueue时,默认100MB - 消费者本地缓存消息跨度超过
consumeConcurrentlyMaxSpan时,默认2000.
消费者流控的结果是降低拉取频率。
死信队列
死信队列用于处理无法被正常消费的消息,当一条消息初次消费失败,消息队列回自动进行消息重试;达到最大重试消息次数后,若消费依然失败,则表明消费者在正常情况下无法正确的消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在RocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。
架构设计
RocketMQ架构上主要分为四部分,如上图所示:
-
Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择响应的Broker集群队列进行消息投递,投递的过程支持快速失败并且并且低延迟。
-
Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
-
NameServer:NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由消息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Consumer可以通过NameServer就可以知道Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通信,Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其他NameServer同步其路由信息,Producer和Consumer仍然可以动态感知Broker的路由的信息。
BrokerServer:Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。
- Remoting Module:整个Broker的实体,负责处理来自Client端的请求。 站在Broker的视角Client包含Producer和Consumer
- Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
- Store Service : 提供方便简单的API接口处理消息存储到物理磁盘和查询功能
- HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
- Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。
部署架构
RocektMQ 网络部署特点
- NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
- Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。 Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有的NameServer。注意:当前RocketMQ版本在部署架构上支持一Master多Slave,单只有BrokerId = 1的从服务器才会参与消息的读负载
- Producer与NameServer集群中的一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向Topic服务的Master Broker建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
- Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接、定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave的Broker节点建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在Master拉取消息的时候,Master服务器会根据拉取偏移量与最大偏移量的判断(判断是否读老消息,产生读IO),以及从服务器是否可读等因此建议下一次是从Master还是Slave拉取消息。
结合部署架构图,描述集群工作流程:
- 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心
- Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP + 端口等)以及存储的所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
- 收到消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
- Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并且从NameServer中获取当前发送的Topic存在哪些Broker上,轮训从队列队列中选择一个队列,然后与队列所在的Broker建议来连接从而向Broker发送消息。
- Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。