Step 1:检查 productGroup 是否符合要求;
Step 2: 根据 客户端 IP+instanceName , 生成clientID , 根据clientID 获取 或 创建MQClientInstance 实例
同一个 jvm 中 的不同消费者和不同生产者在启动时获取到的相同ClientID的 MQClientInstance
优点: 不用创建重复的连接 .
Step3 : 将 producer 或 consumer 注册到 MQClientInstance 中 , 方便后续网络请求心跳等.
step4 : 如果没启动, 就启动MQclientInstance
注意:
复用一个MQClientInstance会有怎么样的问题呢?这种情况会出现在你在一个JVM里启动了多个Producer时,且没有设置instanceName和unitName,那么这两个Producer会公用一个MQClientInstance,发送的消息会路由到同一个集群。例如,你起了两个Producer,并且配置的NameServer地址不一样,本意是让这两个Producer往不同集群上分配消息,但是由于共用了一个MQClientInstance,这个MQClientInstance是基于先来的Producer配置构建的,第二个Producer和他公用后被认为是同一instance,配置是相同的,消息的路由就是相同的,就没有达到想要的效果。
Producer 发送消息
1、获取此主题的 队列路由
topic 是一个逻辑结构, 到底要存到哪个Broker上? 哪个队列上呢? 去nameServer 获取路由撒
相当于 topicA 总共有8个队列! 分布在了2台broker上 .
前提 : Nameserver 检测到broker宕机时 , 是不会推给 producer变化的 , 只有producer 30S 更新路由的请求到达时, 才会更新topic路由.
发送失败策略:
消息存储+文件+刷盘
从主流的几种MQ消息队列采用的存储方式来看,主要会有三种
1、: 分布式KV 存储 比如ActiveMQ中采用的levelDB、Redis, 这种存储方式对于消息读写能力要求不高的情况下可以使用
2、: 文件系统刷盘存储. 常见的比如kafka、RocketMQ、RabbitMQ都是采用消息刷盘到所部署的机器上的 文件系统来做持久化,这种方案适合对于有高吞吐量要求的消息中间件,因为消息刷盘是一种高效 率,高可靠、高性能的持久化方式,除非磁盘出现故障,否则一般是不会出现无法持久化的问题.
3、: 关系型数据库存储 . 比如ActiveMQ可以采用mysql作为消息存储,关系型数据库在单表数据量达到千 万级的情况下IO性能会出现瓶颈,所以ActiveMQ并不适合于高吞吐量的消息队列场景。
RocketMQ 的所有[Topic+队列]消息信息 都放在一个 commitLog中
保证了插入的磁盘顺序写.
队列中的消息还要回查commitLog 会存在随机读 ! 由 pageCache 去解决
RocketMQ消息的存储结构
消息插入commitLog , 再由dispatcher 转发到不同的consumeQueue中
消息的存储由ConsumeQueue 和 CommitLog 配合完成 .
CommitLog 是完整消息真正的存储.
ConsumeQueue 是topic队列的消息存储 . 存的像是一个索引文件, 根据里面的内容去commitLog 查找真正的消息体
CommitLog:
是用来存放所有消息的物理文件 , 和kafka 不一样, 没有分成多个 segament
CommitLog 默认大小 1G , 当一个文件写满后, 会生成一个新的CommitLog, 所有topic的数据 顺序写入commitLog
文件名也代表了offset .
ConsumeQueue :
各个topic 下的 队列
里面的消息 包含了 在commitLog的offset , messageTag 和 消息大小.
每个consume队列中的文件也是有大小的 . 满了会再生成一个 .
IndexFile:
index文件提供了对commitLog 进行数据检索 . 通过key 和 时间区间 来查找 commitLog 中的消息.
物理存储上以 时间戳为文件名 .
CheckPoint:
记录不同日志文件的刷盘时间点 . 当上一次broker是异常结束时,会根据StoreCheckpoint的数据进行恢复
physicMsgTimestamp: commitlog 文件刷盘时间点 。
logicsMsgTimestamp : 消息消费队列文件刷盘时间点 。
indexMsgTimestamp :索引 文件刷盘时间点 。
根据消息更新 ConsumeQueue
当commitLog 写完后就认为持久化成功, 由异步线程将commitLog的消息派发到 consumeQueueLog 中.
Step1 : 根据topic + queueId 锁定一个 consumeQueue文件
Step2 : 将消息体追加到consumeQueue的内存映射文件[mmap使用] 只追加, 不刷盘 , consumeQueue固定是异步刷盘 . 保证了commitLog的完全顺序写.
其他 : 更新 indexFile 等.
还没更新ConsumeQueue , Broker挂了?
如果消息成功存储到 Commitlog 文件中,转发任务未 成功执行,此时消息服务器 Broker 由 于某个原因宕机,导致 Commitlog 、 ConsumeQueue 、 IndexFile 文件数据不一致 [ commitLog中有, consumeQueue中没有 ]。因此在加载时 , 需要一定的校验和数据修复.
如果abort 文件存在, 说明重启后需要要进行异常文件恢复
根据checkPoint 找到上次最后一个正常commitlog的文件 , 将checkPoint 时间点后的消息重新发一遍 !! 确保消息不丢失.
CommitLog文件刷盘机制
RocketMQ 的存储与读写是基于 JDK NIO 的内存映射机制( MappedByteBuffer)
同步刷盘 : org.apache.rocketmq.store.CommitLog#submitFlushRequest
消费发送线程将消息追加到内存映射文件后,将同步任务 GroupCommitRequest 提交到 GroupCommitService 线程,然后调用阻塞等待刷盘结果,超时时间默认为 5s , 等待刷盘成功.
提交线程每10ms执行一遍请求的刷盘, 并唤醒阻塞的线程 .
细节: 实际上是 先收集一波刷盘请求, 10MS之后进行统一的刷盘, 再将这些请求的阻塞线程唤醒.
异步刷盘 : org.apache.rocketmq.store.CommitLog.FlushRealTimeService
使用一个单独线程, 设定一个频率定时进行刷盘操作. Commitlog#handleDiskFlush()
flushlntervalCommitLog: FlushRealTimeService 线程任务运行间隔 默认 刷盘频率 500ms
flushPhysicQueueLeastPages : 一次刷写任务至少包含页数, 如果待刷 写数据不足, 小于该参数配置的值,将忽略本次刷写任务,默认 4 页 。
flushPhysicQueueThoroughlnterval :两次真实刷写任务最大间隔, 默认 10s 。
什么时候清理物理文件 ?
1、消息文件过期(默认72小时),且到达清理时点(默认是凌晨4点),删除过期文件
2、消息文件过期(默认72小时),且磁盘空间达到了水位线(默认75%),删除过期文件
3、磁盘已经达到必须释放的上限(85%水位线)的时候,则开始批量清理文件(无论是否过期),直到空间充足。
4、若磁盘空间达到危险水位线(默认90%),出于保护自身的目的,broker会拒绝写入服务。
消费者
消息消费概述
消费者 以 组的模式展开 , 一个消费组内可以有N个消费者
推模式 : 消息到达Broker后, 主动推送给消费者
拉模式 : 消费者 主动向Broker 获取消息
RocketMQ 的推模式实现是基于拉模式的 , 一个拉取任务完成后立刻开始下一个拉取任务
多个消费者消费多个队列的思想: 每个消费者可以同时消费多个队列 , 但是 一个队列只能被一个消费者消费.
局部顺序消息 : 发送到同一个队列中 , 顺序的消费 .
全局顺序消息 : 只能设置topic 的队列数为1 了 .
RocketMQ 消费者初始化
Step1 : 订阅Topic
1、一种来源是我们业务subscribe方法注册的topic订阅
2、订阅一个基于GroupName的 retry topic , 用于消费重试消息.
Step2 : 初始化 MQClientlnstance 、 Rebalancelmpl (消息重新负载实现类)、OffsetStore 、消费Service 等
Step3 : 向MQClientInstance 注册消费者, 并尝试启动 . [ 同一个rocketMQ集群 , 只需要一个client ]
消息队列负载
RocketMQ 队列重新分布是由 RebalanceService 线程来实现 , 每隔20S 重新分配一遍.
每隔20S , 获取 TOPIC所有的 consumeQueue , 获取所有的 消费者. 进行负载分配
(AllocateMessageQueueAveragely)平均分配算法(默认)
(AllocateMessageQueueAveragelyByCircle)环状分配消息队列
(AllocateMessageQueueByConfig)按照配置来分配队列: 根据用户指定的配置来进行负载
(AllocateMessageQueueByMachineRoom)按照指定机房来配置队列
(AllocateMachineRoomNearby)按照就近机房来配置队列
(AllocateMessageQueueConsistentHash)一致性hash,根据消费者的cid进行
对比消息队列有无发生变化 ,
比如 当前消费的队列 是 q1 , q2 , 如果新分配到的总队列是 q1,q2,q3 那么需要创建一个q3的pullRequest 去拉取任务
如果当前消费的队列 是 q1 , q2 , 如果新分配到的总队列是 q3 , 那么需要先停止q1 q2 的消费并保存进度 , 并且创建一个q3的pullRequest
其他 : 当有其他消费者上线后 , consumer 会register到Broker , Broker会通知此Topic的所有Consumer进行重新负载.
消息拉取
RocketMQ 并没有真正实现推模式,而是消费者主动向消息服务器拉取消息, RocketMQ 推模式是循环向消息服务端发送消息拉取请求
普通的拉取模式能够让消费端量力而行, 但间隔时间又较难设置 , 因此长连接轮询更好!
如果请求到达服务端, 发现没有可用的消息, 则会在服务端等待 几段时间, 每5秒尝试一次, 有数据直接返回, 尝试3次后返回客户端没找到数据.