文章目录
主从同步概述
众所周知,Broker中有两种角色:Master和Slave。Master主要用于处理生产者、消费者的请求和存储数据,Slave从Broker中同步所有的数据到本地,具体体现以下两个作用:
- 保证Broker服务高可用:当Broker宕机时,消费者可以通过连接Slave继续消费,这样可以保证服务的高可用
- 提高服务性能:消费者从Master Broker拉取消息时,发现拉取消息的offset和CommitLog的物理offset相差太多,会转向Slave拉取消息,这样可以减轻Master的压力,从而提高性能。
Broker同步数据的方式有两种:
- 同步复制:指客户端发送消息到Master,Master将消息同步复制到Slave的过程,可以通过设置参数
brokerRole = BrokerRole.SYNC_MASTER
来实现,可靠性高但效率比较低。 - 异步复制:指客户端发送消息到Master,再由异步线程HAService异步同步到Slvae的过程,可以通过设置参数
brokerRole = BrokerRole.ASYNC_MASTER
来实现,效率非常高,但是可靠性比同步复制差。
Broker同步的数据有两种:配置数据和消息数据。配置数据主要包含Topic配置、消费者位点信息,延迟消息位点信息、订阅关系配置等。
主从同步流程
名词解释
服务名 | 功能 |
---|---|
SlaveSynchronize | Slave从Master同步配置数据的服务 |
HAService | Slave从Master同步CommitLog数据 |
HAConnection | Slave连接信息 |
HAConnection.WriteSocketService | 将CommitLog写入网络,发送给Slave |
HAConnection.ReadSocketService | 读取Slave发送的offset请求 |
HAClient | Slave处理与Master通信的客户端封装 |
GroupTransferService | 主从同步通知类,实现同步、异步复制提供新数据通知服务 |
AcceptSocketService | Master接受Slave发送的上报offset请求的服务 |
配置数据同步流程
配置数据的同步包含4中类型:Topic配置(TopicConfigManager)、消费者位点(ConsumerOffsetManager)、延迟位点(ScheduleMessageService)、订阅关系配置(SubscriptionGroupManager),它们都继承自ConfigManager抽象类。
当Slave Broker(brokerRole = BrokerRole.SLAVE
)启动时,会初始化SlaveSynchronize方法,调用org.apache.rocketmq.broker.BrokerController#handleSlaveSynchronize方法,每10s调用一次org.apache.rocketmq.broker.slave.SlaveSynchronize.syncAll方法:
private void handleSlaveSynchronize(BrokerRole role) {
if (role == BrokerRole.SLAVE) {
if (null != slaveSyncFuture) {
// 取消正在运行的slave同步任务,不强制中断
slaveSyncFuture.cancel(false);
}
this.slaveSynchronize.setMasterAddr(null);
slaveSyncFuture = this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.slaveSynchronize.syncAll();
}
catch (Throwable e) {
log.error("ScheduledTask SlaveSynchronize syncAll error.", e);
}
}
}, 1000 * 3, 1000 * 10, TimeUnit.MILLISECONDS);
} else {
//handle the slave synchronise
if (null != slaveSyncFuture) {
slaveSyncFuture.cancel(false);
}
this.slaveSynchronize.setMasterAddr(null);
}
}
syncAll方法一次调用四种配置数据(Topic配置、消费者位点、同步延迟位点、订阅关系配置)的同步方法同步全量数据:
public void syncAll() {
// 同步Topic配置
this.syncTopicConfig();
// 同步消费者位点
this.syncConsumerOffset();
// 同步延迟位点
this.syncDelayOffset();
// 同步订阅关系配置
this.syncSubscriptionGroupConfig();
}
syncAll方法执行的4个方法都是通过调用BrokerOuterAPI中的方法,根据RequestCode.GET_ALL_TOPIC_CONFIG、RequestCode.GET_ALL_CONSUMER_OFFSET、RequestCode.GET_ALL_DELAY_OFFSET、RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG
使用Remoting模块远程调用Master Broker,而Master Broker初始化过程中注册了org.apache.rocketmq.broker.processor.AdminBrokerProcessor处理器接受对应的请求,根据RequestCode
的值进行不同方法的调用(四种配置数据信息获取的方法很简单,不在赘述)。
Slave根据上面说的流程获取到Master中的配置数据信息同步持久化到磁盘中。
CommitLog数据同步流程
CommitLog的数据同步分为同步复制和异步复制两种。同步复制是生产者生产消息后,等待Master Broker将数据同步到Slave Broker后,再返回生产者数据存储状态;异步复制是生产者在生产消息后,不用等待Slave同步,直接返回Master存储结果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oWDHoVxA-1649378441486)(C:\Users\liaozhirong\Desktop\learning\Images\webp.webp)]
异步复制
Master Broker启动时,会初始化和启动DefaultMessageStore存储服务,而DefaultMessageStore存储服务会初始化org.apache.rocketmq.store.ha.HAService服务,HAService服务会启动org.apache.rocketmq.store.ha.HAService。AcceptSocketService服务,监听10912端口,用于接收来自一个或多个Slave的注册请求,当有Slave注册请求进来时,会创建一个HAConnection,同时HAConnection会创建WriteSocketService和ReadSocketService服务并启动,开始主从数据同步。
Slave -> Master
ReadSocketService:Master接收Slave同步数据的请求,并将这些信息保存在HAConnection中。
public void run() {
HAConnection.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
this.selector.select(1000);
// 接收Slave的上报请求
boolean ok = this.processReadEvent();
if (!ok) {
HAConnection.log.error("processReadEvent error");
break;
}
long interval = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastReadTimestamp;
// 判断slave心跳时间是否超过同步存活时间,表示Slave不可用(可能宕机或者网络不可用)
// 终止获取Slave的上报请求循环,删除Slave的连接
if (interval > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaHousekeepingInterval()) {
log.warn("ha housekeeping, found this connection[" + HAConnection.this.clientAddr + "] expired, " + interval);
break;
}
} catch (Exception e) {
HAConnection.log.error(this.getServiceName() + " service has exception.", e);
break;
}
}
// 暂停该Slave的read、write服务,并移除该Slave连接
this.makeStop();
writeSocketService.makeStop();
haService.removeConnection(HAConnection.this);
HAConnection.this.haService.getConnectionCount().decrementAndGet();
SelectionKey sk = this.socketChannel.keyFor(this.selector);
if (sk != null) {
sk.cancel();
}
try {
this.selector.close();
this.socketChannel.close();
} catch (IOException e) {
HAConnection.log.error("", e);
}
HAConnection.log.info(this.getServiceName() + " service end");
}
private boolean processReadEvent() {
// 读取到0的次数
int readSizeZeroTimes = 0;
// 读取到最大子节,flip重置position为0,processPosition=0
if (!this.byteBufferRead.hasRemaining()) {
this.byteBufferRead.flip();
this.processPosition = 0;
}
// byteBufferRead是否可以还可以写入数据
while (this.byteBufferRead.hasRemaining()) {
try {
// 读取数据到缓存中
int readSize = this.socketChannel.read(this.byteBufferRead);
// readSize>0表示读取到子节数据
if (readSize > 0) {
// 设置读取到0的次数为0、最近读取数据的时间
readSizeZeroTimes = 0;
this.lastReadTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
// 读取的位点-已读位点 大于等于 8,表示读取到同步请求的偏移位点信息
if ((this.byteBufferRead.position() - this.processPosition) >= 8) {
// slave上报的最大偏移量占8个子节,正常情况下byteBufferRead的position是8的倍数,但是不能确定出现粘包情况的出现
// 所以pos重新计算,规避该情况的出现,保证获取的位置是正确的
// 获取Slave请求同步的最新偏移位点信息
int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8);
long readOffset = this.byteBufferRead.getLong(pos - 8);
// 设置已读位点 = pos
this.processPosition = pos;
// 设置slave应答同步完成的偏移位点
HAConnection.this.slaveAckOffset = readOffset;
// 设置slave请求同步开始的偏移位点
HAConnection.this.slaveRequestOffset = readOffset;
if (HAConnection.this.slaveRequestOffset < 0) {
log.info("slave[" + HAConnection.this.clientAddr + "] request offset " + readOffset);
}
// 比较更新push2SlaveMaxOffset,并唤醒GroupTransferService服务
// GroupTransferService服务则比较push2SlaveMaxOffset判断是否完成同步复制返回future结果
HAConnection.this.haService.notifyTransferSome(HAConnection.this.slaveAckOffset);
}
// 连续读取三次 0 则结束本次读取,返回true
} else if (readSize == 0) {
if (++readSizeZeroTimes >= 3) {
break;
}
// 读取到负数,发生异常,关闭Slave连接
} else {
log.error("read socket[" + HAConnection.this.clientAddr + "] < 0");
return false;
}
} catch (IOException e) {
log.error("processReadEvent exception", e);