浅学RocketMQ

本文详细介绍了RocketMQ中autoCreateTopicEnable设置为true时的主题创建流程,包括生产者如何获取不存在主题的路由信息以及Broker的路由信息管理。同时,分析了RocketMQ的主从同步机制和消息消费进度同步,解释了在主服务器宕机后,从服务器接管消息读取的情况以及消息不会被重复消费的原因。最后,讨论了主题扩容后遇到的问题及其解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、生产环境中,autoCreateTopicEnable为什么不能设置true

autoCreateTopicEnable 设置为TRUE,broker服务器会自动创建一个topic

期望值:为了消息发送的高可用,希望新建的topic可在broker集群的每台服务器上创建对应的topic,避免broker的单节点故障。

RocketMQ 基本路由规则
Broker 在启动时会向NameServer注册存储在该服务器上的路由信息,并每隔30秒向NameServer发送心跳包,并更新路由。

NameServer 每隔10秒扫描路由表,如果检测到Broker宕机,则移出该路由信息。

消息生产者Producer每隔30秒向NameServer重新拉取topic的路由信息,并更新本地路由表,在消息发送之前,如果本地路由表中不存在对应的主题路由消息时,会主动向NameServer拉取该主题信息。

探究autoCreateTopicEnable机制

1、在broker启动流中,会构建TopicConfigManager对象,其构造方法中会首先判断是否开启了自动创建主题,如果启用了,则向topicConfigTable中添加默认主题的路由信息。
在这里插入图片描述

2、生产者寻找路由信息
生产者首先向NameServer查询路由信息,由于是一个不存在的主题,故返回的路由信息为空,RocketMQ会使用默认的主题再寻找一次,由于开启了自动创建路由信息,NameServer会向生产者返回默认主题的路由信息
问:消息发送者从NameServer获取到的默认的topic队列后,队列的个数会变么?答案是会的。

在这里插入图片描述
在Broker端的topic配置管理器中存在路由信息,会向NameServer发送心跳包,汇报到NameServer,另一方面会有一个定时任务,定时存储在broker端,具体路径 ${ROCKET_HOME}/store/config/topics.json 中,这样在Broker关闭后再重启,并不会丢失路由信息。

创建主题序列图三个关键点

1、启动autoCreateTopicEnable创建主题时,消息生产者往Broker端发送消息时才会在Broker端创建主题。
2、Broker端在一个心跳包周期内,会将新的路由信息发送到NameServer,与此同时,Broker端会有一个定时任务,定时将 内存中的路由信息持久化到Broker端的磁盘上。
3、消息发送者每隔30秒会向NameServer更新路由,如果消息发送者一段时间内未发送消息,就不会有消息发送到集群内的第二胎Broker上,那么NameServer中新建的topic的路由信息只会包含在Broker-a,消息发送者在一段时间内再次拉去NameServer中topic的路由信息,那么该topic的消息永远不会发送到另外一个Broker上。(其实我们快速发送9条消息(每个Broker中开启了8个队列),就有可能在2个Broker上都创建topic)

RocketMQ HA 核心工作机制

1、主从同步

在这里插入图片描述
RocketMQ的主从同步机制如下:

  • 首先启动Master并在指定端口监听
  • 客户端启动,主动连接Master,建立TCP连接
  • 客户端以每隔5s的间隔时间向服务器端拉去消息,如果是第一次的话,先获取本地commitlog文件中的最大偏移量,以该偏移量想服务器拉取消息
  • 服务器解析请求,并返回一批数据给客户端
  • 客户端收到一批消息后,将消息写入本地commitlog文件中,然后向Master汇报拉取进度,并更新下一次拉取偏移量
  • 然后重复第三个步骤

RocketMQ主从同步一个重要特征:主从同步不具备切换功能,即当住节点宕机后,从节点不会接管消息发送,但可以提供消息读取(功能开启的前提下)。

DefaultMessageStore#getMessage 
// maxOffsetPy 当前最大的物理偏移量。返回的偏移量为已存入到操作系统的 PageCache 中的内容
// maxPhyOffsetPulling 本次消息拉取最大物理偏移量,按照消息顺序拉取的基本原则,可以基本预测下次开始 拉取的物理偏移量将大于该值,并且就在其附近
// diff maxOffsetPy 与 maxPhyOffsetPulling 之间的间隔,getMessage 通常用于消息消 费时,即这个间隔可以理解为目前未处理的消息总大小
long diff = maxOffsetPy - maxPhyOffsetPulling; 
// 获取 RocketMQ 消息存储在 PageCache 中的总大小,如果当 RocketMQ 容量超过该阔值,将会将被置换出内存,如果要访问不在 PageCache 中的消 息,则需要从磁盘读取
// StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE 返回当前系统的总物理内存。参数
// accessMessageInMemoryMaxRatio 设置消息存储在内存中的阀值,默认为 40
long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0));
// 设置下次拉起是否从从拉取标记,触发下次从从服务器拉取的条件为:当前 所有可用消息数据(所有 commitlog)文件的大小已经超过了其阔值,默认为物理内存的 40%
getResult.setSuggestPullingFromSlave(diff > memory);
PullMessageProcessor#processRequest 
// 如果从 commitlog 文件查找消息时,发现消息堆积太多,默认超过物理内 存的 40%后,会建议从从服务器读取
if (getMessageResult.isSuggestPullingFromSlave()) { 
	responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerW henConsumeSlowly()); 
} else {
	responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); 
}
//如果当前服务器的角色为从服务器:并且 slaveReadEnable=true,则忽略 代码@1 设置的值,下次拉取切换为从主拉取
switch (this.brokerController.getMessageStoreConfig().getBrokerRole()) {
	case ASYNC_MASTER: 
	case SYNC_MASTER: break; 
	case SLAVE: 
	// 如果 slaveReadEnable=true(从允许读),并且建议从从服务器读取,则 从消息消费组建议当消息消费缓慢时建议的拉取 brokerId,由订阅组配置属性 whichBrokerWhenConsumeSlowly 决定;如果消息消费速度正常,则使用订阅组建议的 brokerId 拉取消息进行消费,默认为主服务器。如果不允许从可读,则固定使用从主拉取
		if (!this.brokerController.getBrokerConfig().isSlaveReadEnable()) { 
			response.setCode(ResponseCode.PULL_RETRY_IMMEDIATE LY); 	
			responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_I D); }
			break; 
		}
		if (this.brokerController.getBrokerConfig().isSlaveReadEnable()) {  // consume too slow ,redirect to another machine 
			if (getMessageResult.isSuggestPullingFromSlave()) { 
				responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.g etWhichBrokerWhenConsumeSlowly()); 
			}// consume ok 
			else {
				responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.ge tBrokerId()); 
			} 
		} else {
			responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); 
		}

2、消息消费进度同步机制

集群模式下消息消费进度存储文件谓语服务端${ROCKETMQ_HOME}/store/confi g/consumerOffset.json 消息消费者从服务器拉取一批消息后提交到消费组特定的线程 池中处理消息,当消息消费成功后会向 Broker 发送 ACK 消息,告知消费端已成功消费到 哪条消息,Broker 收到消息消费进度反馈后,首先存储在内存中,然后定时持久化到 consumeOffset.json 文件中。

问:主服务器宕机了,从服务器接管消息读取,一段时间后,主服务器重启,已消费的信息,会不会再次被消费?

答:第一种,消息消费者在内存中存在最新的消息消费进度,继续以该进度去服务器拉取消息,消息处理完后,会定向向Broker服务器反馈消费进度,在反馈消息消费进度时,会优先选择主服务器,此时主服务器的消息消费进度就立马更新了,从服务器此时只需要定时同步主服务器的消息消费进度即可。
第二种,消息消费者在向主服务器拉取消息时,如果是主服务器,在处理小时拉取时,也会更新消息消费进度。

主服务器消息拉取时更新消息消费进度

RocketMQ一个新的消费组初次启动时从何处开始消费?

  • CONSUME_FROM_MAX_OFFSET
    从消费队列最大的偏移量开始消费
  • CONSUME_FROM_FIRST_OFFSET
    从消费队列最小偏移量开始消费
  • CONSUME_FROM_TIMESTAMP
    从指定的时间戳开始消费,默认为消费者启动之前的 30 分钟处开始消费
case CONSUME_FROM_LAST_OFFSET: { 
	// 使用 offsetStore 从消息消费进度文件中读取消费消费进度,本文将以集群 模式为例展开
	long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STOR E); // @1 
	// 如果返回的偏移量大于等于 0,则直接使用该 offset,这个也能理解,大于 等于 0,表示查询到有效的消息消费进度,从该有效进度开始消费,但我们要特别留意 lastOffset 为 0 是什么场景,因为返回 0,并不会执行 CONSUME_FROM_LAST_OF FSET(语义)
	if (lastOffset >= 0) { // @2 
		result = lastOffset; 
	//如果 lastOffset 为-1,表示当前并未存储其有效偏移量,可以理解为第一次 消费,如果是消费组重试主题,从重试队列偏移量为 0 开始消费;如果是普通主题,则从 队列当前的最大的有效偏移量开始消费,即 CONSUME_FROM_LAST_OFFSET 语义 的实现。
	}else if (-1 == lastOffset) { // @3 
		if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
			 result = 0L; 
		} else { 
			try {result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq); 
			//如果从远程服务拉取最大偏移量拉取异常或其他情况,则使用-1 作为第一 次拉取偏移量
			} catch (MQClientException e) { // @4 
				result = -1; 
			} 
		}
	 } else { 
	 	result = -1; 
	 } break; 
}

分析什么情况下消息消费进度存储器(OffestStore)在什么情况下会返回0

ConsumerManageProcessor#queryConsumerOffset 
private RemotingCommand queryConsumerOffset(ChannelHandlerContext ctx, Remoti ngCommand request) throws RemotingCommandException { 
	final RemotingCommand response = RemotingCommand.createResponseCommand(QueryConsumerOffsetRespons eHeader.class); 
	final QueryConsumerOffsetResponseHeader responseHeader = (QueryConsumerOffsetResponseHeader) response.readCustomHeader(); 
	final QueryConsumerOffsetRequestHeader requestHeader = (QueryConsumerOffsetRequestHeader) request.decodeCommandCustomHeader(QueryConsumerOffsetRequestHeader.cla ss);
	// 从消费消息进度文件中查询消息消费进度
	long offset = this.brokerController.getConsumerOffsetManager().queryOffset( requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHe ader.getQueueId()); // @1 
	// 如果消息消费进度文件中存储该队列的消息进度,其返回的 offset 必然会大 于等于 0,则直接返回该偏移量该客户端,客户端从该偏移量开始消费
	if (offset >= 0) { // @2 
		responseHeader.setOffset(offset);
		response.setCode(ResponseCode.SUCCESS); 
		response.setRemark(null); 
	} else { // @3 
		// 如果未从消息消费进度文件中查询到其进度,offset 为-1。则首先获取该主 题、消息队列当前在 Broker 服务器中的最小偏移量
		long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHead er.getTopic(), requestHeader.getQueueId()); // @4 
		// 。如果小于等于 0(返回 0 则表示 该队列的文件还未曾删除过)并且其最小偏移量对应的消息存储在内存中而不是存在磁盘 中,则返回偏移量 0,这就意味着 ConsumeFromWhere 中定义的三种枚举类型都不会 生效,直接从 0 开始消费,到这里就能解开其谜团了
		if (minOffset <= 0 && !this.brokerController.getMessageStore().checkInDiskByConsumeOffset (requestHeader.getTopic(), requestHeader.getQueueId(), 0)) { 
			responseHeader.setOffset(0L);
			response.setCode(ResponseCode.SUCCESS); 
			response.setRemark(null); 
		} else { // @6 
			// 如果偏移量小于等于 0,但其消息已经存储在磁盘中,此时返回未找到,最 终 RebalancePushImpl#computePullFromWhere 中得到的偏移量为-1
			response.setCode(ResponseCode.QUERY_NOT_FOUND); 
			response.setRemark("Not found, V3_0_6_SNAPSHOT maybe this group consumer boot first"); 
		} 
	}
	return response; 
}

RocketMQ 主题扩分片后遇到的问题

RocketMQ分片孔融后部分队列中的数据无法消费?

潜在原因:DefaultCluster 集群进行过一次集群扩容,从原来的一台消息服务器( broker-a )额外增加一台 broker 服务器( broker-b ),但扩容的时候并没有把原先的存 在于 broker-a 上的主题、消费组扩容到 broker-b 服务器

解决办法:运维通过命令,在 broker-b 上创建对应的订阅消息,问题解决。

经验教训:集群扩容时,需要同步在集群上的 topic.json、subscriptionGroup.json 文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值