RocketMQ作为一款优秀的分布式消息中间件,分布式系统的一个很重要的特点就是要保证系统的高可用(HA),RocketMQ则是通过主从同步机制保证系统的高可用。
1、概述
主从同步同步的是啥作为消息中间件,无疑是消息相当于给数据做**”备份“**,主节点服务器Broker宕机后,消费者可以从从节点的服务消费消息,可以保证业务的正常运行。 主从同步的原理图
增加slave从节点的优点:
数据备份:保证了两/多台机器上的数据冗余,特别是在主从同步复制的情况下,一定程度上保证了Master出现不可恢复的故障以后,数据不丢失。 高可用性:即使Master掉线, Consumer会自动重连到对应的Slave机器,不会出现消费停滞的情况。 提高性能:主要表现为可分担Master读的压力,当从Master拉取消息,拉取消息的最大物理偏移与本地存储的最大物理偏移的差值超过一定值,会转向Slave(默认brokerId=1)进行读取,减轻了Master压力。 消费实时:master宕机后消费者可以从slave上消费保证消息的实时性,但是slave不能接收producer发送的消息,slave只能同步master数据(RocketMQ4.5版本之前),4.5版本开始增加多副本机制,根据RAFT算法,master宕机会自动选择其中一个副本节点作为master保证消息可以正常的生产消费。
主从数据同步有两种方式同步复制、异步复制
复制方式 | 优点 | 缺点 | 适应场景 |
---|---|---|---|
同步复制 | slave保证了与master一致的数据副本,如果master宕机,数据依然在slave中找到其数据和master的数据一致 | 由于需要slave确认效率上会有一定的损失 | 数据可靠性要求很高的场景 |
异步复制 | 无需等待slave确认消息是否存储成功效率上要高于同步复制 | 如果master宕机,由于数据同步有延迟导致slave和master存在一定程度的数据不一致问题 | 数据可靠性要求一般的场景 |
我们在前面章节中 RocketMQ存储文件介绍过消息存储相关的文件信息,从节点同步commitlog时同样需要同步相关的配置信息,主题列表信息、消费组信息、消费进度信息等元数据信息。下面从源码的角度具体分析下。
2、元数据复制
2.1、Broker启动时元数据同步
元数据就是基础信息,如主题信息、消费者信息、消费进度信息等。我们分析下broker启动时的业务逻辑处理,broker服务启动时会创建BrokerController对象并将其初始化initialize()分析其方法
//如果Broker是Slave
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
if (this.messageStoreConfig.getHaMasterAddress() != null && this.messageStoreConfig.getHaMasterAddress().length() >= 6) {
this.messageStore.updateHaMasterAddress(this.messageStoreConfig.getHaMasterAddress());
this.updateMasterHAServerAddrPeriodically = false;
} else {
this.updateMasterHAServerAddrPeriodically = true;
}
//启动一个定时的单线程池
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.slaveSynchronize.syncAll();
} catch (Throwable e) {
log.error("ScheduledTask syncAll slave exception", e);
}
}
}, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
} else {//如果是master
//定时打印master与slave的差距
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.printMasterAndSlaveDiff();
} catch (Throwable e) {
log.error("schedule printMasterAndSlaveDiff error.", e);
}
}
}, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
}
当前broker为slave是执行slaveSynchronize.syncAll();,每隔60秒同步master中的元数据信息
public void syncAll() {
//同步主题配置信息
this.syncTopicConfig();
//同步消费者偏移量信息
this.syncConsumerOffset();
//同步延迟消费的偏移量信息
this.syncDelayOffset();
//同步订阅的消息组信息
this.syncSubscriptionGroupConfig();
}
我们通过图分析其执行原理,四种文件的同步方式是相同的,我们分析一个主题信息同步
2.2、syncTopicConfig主题信息同步原理
slave端启动时初始化
private void syncTopicConfig() {
String masterAddrBak = this.masterAddr;
if (masterAddrBak != null) {
try {
//调用getAllTopicConfig从master中拉取TopicConfig配置信息
TopicConfigSerializeWrapper topicWrapper =
this.brokerController.getBrokerOuterAPI().getAllTopicConfig(masterAddrBak);
//比较版本号是否一致,不一致则更新
if (!this.brokerController.getTopicConfigManager().getDataVersion()
.equals(topicWrapper.getDataVersion())) {
//更新TopicConfigManager中的版本号
this.brokerController.getTopicConfigManager().getDataVersion()
.assignNewOne(topicWrapper.getDataVersion());
//清空TopicConfigManager中TopicConfig信息
this.brokerController.getTopicConfigManager().getTopicConfigTable().clear();
//赋值新的信息
this.brokerController.getTopicConfigManager().getTopicConfigTable()
.putAll(topicWrapper.getTopicConfigTable());
//进行持久化
this.brokerController.getTopicConfigManager().persist();
log.info("Update slave topic config from master, {}", masterAddrBak);
}
} catch (Exception e) {
log.error("SyncTopicConfig Exception, {}", masterAddrBak, e);
}
}
}
查看其getAllTopicConfig方法调用master中的broker获取topic配置信息,解码返回的数据封装成TopicConfigSerializeWrapper,里面包含主题的配置信息(topicConfigTable)、拉取的当前数据的版本(dataVersion),slave判断拉取的数据版本相同时就不需要更新topicConfig信息。
public TopicConfigSerializeWrapper getAllTopicConfig(
final String addr) throws RemotingConnectException, RemotingSendRequestException,
RemotingTimeoutException, InterruptedException, MQBrokerException {
//创建request
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, null);
//调用底层通信模块remotingClient进行请求返回response
RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(true, addr), request, 3000);
assert response != null;
switch (response.getCode()) {
case ResponseCode.SUCCESS: {
//若返回成功包装成TopicConfigSerializeWrapper进行返回
return TopicConfigSerializeWrapper.decode(response.getBody(), TopicConfigSerializeWrapper.class);
}
default:
break;
}
throw new MQBrokerException(response.getCode(), response.getRemark());
}
调用master端的请求数据
private RemotingCommand getAllTopicConfig(ChannelHandlerContext ctx, RemotingCommand request) {
final RemotingCommand response = RemotingCommand.createResponseCommand(GetAllTopicConfigResponseHeader.class);
//对获取的TopicConfig信息进行编码
String content = this.brokerController.getTopicConfigManager().encode();
if (content != null && content.length() > 0) {
try {
//二进制数据返回
response.setBody(content.get