RocketMQ组成角色
Broker消息服务器在启动时向所有NameServer注册,消息生产者(Producer)在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer与每台Broker服务器保持长连接,并间隔10s检测Broker是否存活,如果检测到Broker宕机,则从路由注册表中将其移除。但是路由变化不会马上通知消息生产者。
启动RocketMQ的顺序是先启动NameServer,再启动Broker,这时候消息队列已经可以提供服务了,想发送消息就使用Producer来发送,想接收消息就使用Consumer来接收。
RocketMQ基于订阅发布机制,一个Topic拥有多个消息队列,一个Broker为每一主题默认创建4个读队列4个写队列。多个Broker组成一个集群,BrokerName由相同的多台Broker组成Master-Slave架构,brokerId为0代表Master,大于0表示Slave。
一、NameServer
NameServer 是整个消息队列中的状态服务器,集群的各个组件通过它来了解全局的信息。各个角色的机器都要定期向NameServer 上报自己的状态,超时不上报的话, NameServer 会认为某个机器出故障不可用了,其他的组件会把这个机器从可用列表里移除。
NameServer本身的高可用是通过部署多台NameServer服务器来实现,但彼此之间互不通讯,也就是NameServer服务器之间在某一时刻的数据并不完全相同,但这对消息发送并不会造成任何影响。
其他角色同时向多个NameServer机器上报状态信息,从而达到热备份的目的。NameServer 本身是无状态的, NameServer 中的Broker 、Topic 等状态信息不会持久存储,都是由各个角色定时上报并存储到内存中的。
集群状态的存储结构
在org.apache.rocketmq.namesrv.routeinfo类中,有五个变量,集群的状态就保存在这五个变量中。
- private final HashMap<String/* topic */, List<QueueData>> topicQueueTable:这个结构的 Key 是 Topic 的名称,它存储了所有 Topic 的属性信息。Value 是个 QueueData 队列,队列的长度等于这个 Topic 数据存储的 Master Broker 的个数, QueueData 里存储着 Broker 的名称,读写 queue 的数量、同步标识等。
- private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable:以BrokerName 为索引,相同名称的Broker 可能存在多台机器, 一个Master 和多个Slave 。这个结构存储着一个BrokerName 对应的属性信息,包括所属的Cluster 名称, 一个Master Broker 和多个Slave Broker的地址信息。
- private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable:存储的是集群中C luster 的信息,结果很简单,就是一个Cluster 名称对应一个由BrokerName 组成的集合。
- private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable:key 是 BrokerAddr,也就是对应着一台机器,value是存储的是这台 Broker 机器的实时状态,包括上次更新状态的时间戳,NameServer 会定期检查这个时间戳,超时没有更新就认为这个 Broker 无效了, 将其从Broker 列表里面清除。
- private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable:Filter Server 是过滤服务器,是 RocketMQ 的一种服务端过滤方式,一个 Broker 可以有一个或多个 Filter Server,这个结构的 Key是Broker的地址,Value是和这个Broker关联的多个 Filter Server的地址。
状态维护逻辑
RocketMQ路由注册是通过Broker与NameServer的心跳功能实现的。Broker启动时向集群中所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,NameServer每隔10s检查一次,如果连续120s没有收到心跳包,NameServer将移除该Broker的路由信息同时关闭Socket连接。
RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不主动推送给客户端,而是由客户端定时拉取主题最新的路由。
为何不用ZooKeeper?
ZooKeeper的功能很强大,包括自动Master选举等,RocketMQ的架构设计决定了它不需要进行Master选举, 用不到这些复杂的功能,只需要一个轻量级的元数据服务器就足够了。
RocketMQ的NameServer只有很少的代码,容易维护,所以不需要再依赖另一个中间件,从而减少整体维护成本。
总结
NameServer在RocketMQ集群中扮演调度中心的角色。各个Producer、Consumer 上报自己的状态上去,同时从NameServer获取其他角色的状态信息。NameServer 的功能虽然非常重要,但是被设计得很轻量级,代码量少并且几乎无磁盘存储,所有的功能都通过内存高效完成。
二、Producer
发送和接收消息前,先创建Topic,针对某个Topic发送和接收消息。一个Topic可以根据需求设置一个或多个Message Queue,Message Queue类似分区或Partition。Topic有了多个Message Queue后,消息可以并行地向各个Message Queue发送,消费者也可以并行地从多个Message Queue读取消息并消费。如果集群中有多个master角色的Broker,默认在每个Broker上创建8个读写队列。
RocketMQ3种消息发送方式
同步(sync):发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结果。
异步(async):发送者向MQ执行发送消息API时,指定消息发送成功后的回调函数,然后调用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中执行。
单向(oneway):消息发送者向MQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数,简单地说,就是只管发,不在乎消息是否成功存储在消息服务器上。
消息发送基本流程
消息发送流程主要的步骤:验证消息、查找路由、消息发送(包含异常处理机制)。
消息发送之前,首先确保生产者处于运行状态,然后验证消息是否符合相应的规范,具体的规范要求是主题名称、消息体不能为空、消息长度不能等于0且默认不能超过允许发送消息的最大长度4M(maxMessageSize=1024 * 1024 * 4)。
消息发送之前,首先需要获取主题的路由信息,只有获取了这些信息我们才知道消息要发送到具体的Broker节点。如果生产者中缓存了topic的路由信息,且该路由信息中包含了消息队列,则直接返回该路由信息,如果没有缓存或没有包含消息队列,则向NameServer查询该topic的路由信息。如果最终未找到路由信息,则抛出异常:无法找到主题相关路由信息异常。
消息发送端采用重试机制,由retryTimesWhenSendFailed指定同步方式重试次数,异步重试机制在收到消息发送结构后执行回调之前进行重试。如果消息重试次数超过允许的最大重试次数,消息将进入到DLQ延迟队列。延迟队列主题: %DLQ%+消费组名。
消息队列如何进行负载?
消息生产者在发送消息时,如果本地路由表中未缓存topic的路由信息,向NameServer发送获取路由信息请求,更新本地路由信息表,并且消息生产者每隔30s从NameServer更新路由表。
选择消息队列:根据路由信息选择消息队列,返回的消息队列按照broker、序号排序。举例说明,如果topicA在broker-a,broker-b上分别创建了4个队列,返回的消息队列,那么RocketMQ如何选择消息队列呢?
[
{"brokerName":"broker-a","queueId":0},
{"brokerName":"broker-a","queueId":1},
{"brokerName":"broker-a","queueId":2},
{"brokerName":"broker-a","queueId":3},
{"brokerName":"broker-b","queueId":0},
{"brokerName":"broker-a","queueId":1},
{"brokerName":"broker-a","queueId":2},
{"brokerName":"broker-a","queueId":3}
]
首先在一次消息发送过程中,可能会多次执行算则消息队列这个方法,lastBrokerName就是上一次选择的执行发送消息失败的Broker。第一次执行消息队列选择时,lastBrokerName为null,此时直接用sendWhichQueue自增再获取值,与当前路由表中消息队列个数取模,返回该位置的MessageQueue(selectOneMessageQueue()方法),如果消息发送再失败的话,下次进行消息队列选择时规避上次MessageQueue所在的Broker,否则还是很有可能再次失败。
消息发送异常机制
消息发送高可用主要通过两个手段:重试与Broker规避。Broker规避就是在一次消息发送过程中发现错误,在某一时间段内,消息生产者不会选择该Broker(消息服务器)上的消息队列,提高发送消息的成功率。
批量消息发送
批量消息发送是将同一主题的多条消息一起打包发送到消息服务端,减少网络调用次数,提高网络传输效率。
* producerGroup: 生产者所属组,消息服务器在回查事务状态时会随机选择该组中任何一个生产者发起事务回查请求。