RocketMQ5源码(二)controller模式

前言

本章基于rocketmq5.1.1版本分析5.x的controller模式。

master-slave模式下,controller组件提供了broker自动主从切换能力,主要对标的是4.x的DLedgerCommitLog。

controller支持独立部署,也支持嵌入Nameserver部署,要保证controller具备容错能力,controller需要三副本及以上(raft)。

具体文档可以参考:github.com/apache/rock...

一、案例

controller有两种部署方式。

controller独立部署,比如本地启3个ControllerStartup,启1个NamesrvStartup,启动1-n个broker组。

controller嵌入nameserver部署,比如本地启3个NamesrvStartup,开启enableControllerInNamesrv,nameserver和controller在一个进程内启动。

注:nameserver仍然是无状态的,且nameserver之间无感知,controller仅仅是和nameserver在一个进程中启动。

1、nameserver内嵌controller

相关配置项:

  1. controllerDLegerGroup:一个raft组;
  2. controllerDLegerPeers:raft组成员列表;
  3. controllerDLegerSelfId:当前节点id;
  4. controllerStorePath:raft数据存储位置,默认在user.home/DledgerController;
  5. enableControllerInNamesrv:开启nameserver内嵌controller;

独立部署,去除enableControllerInNamesrv,ControllerStartup -c 配置文件即可。

NamesrvStartup -c namesrv0.conf。

ini 复制代码
# Namesrv config
listenPort = 9876
enableControllerInNamesrv = true
# controller config
controllerDLegerGroup = group1
controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858
controllerDLegerSelfId = n0
controllerStorePath = /tmp/DledgerController

NamesrvStartup -c namesrv1.conf。

ini 复制代码
#Namesrv config
listenPort = 9886
enableControllerInNamesrv = true
#controller config
controllerDLegerGroup = group1
controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858
controllerDLegerSelfId = n1
controllerStorePath = /tmp/DledgerController

同理NamesrvStartup -c namesrv2.conf。

2、broker组

使用controller模式,一个broker组无需像DLedgerCommitLog一样部署3副本组成raft组来保证高可用,部署2个即可。

相关配置项:

  1. enableControllerMode=true,开启controller模式;
  2. controllerAddr,指定controller地址列表;
  3. brokerId和brokerRole,随便设置都行,按照官方案例设置成-1和SLAVE;

BrokerStartup -c broker0.conf。

ini 复制代码
brokerClusterName = MyDefaultCluster
brokerName = broker-a-controller-mode
brokerId = -1
brokerRole = SLAVE
namesrvAddr = 127.0.0.1:9876;127.0.0.1:9886;127.0.0.1:9896
listenPort=30911
storePathRootDir=/tmp/rmqstore/broker-a-00-controller-mode
storePathCommitLog=/tmp/rmqstore/broker-a-00-controller-mode/commitlog
# controller
enableControllerMode = true
controllerAddr = 127.0.0.1:9878;127.0.0.1:9868;127.0.0.1:9858

BrokerStartup -c broker1.conf。

ini 复制代码
brokerClusterName = MyDefaultCluster
brokerName = broker-a-controller-mode
brokerId = -1
brokerRole = SLAVE
namesrvAddr = 127.0.0.1:9876;127.0.0.1:9886;127.0.0.1:9896
listenPort=30921
storePathRootDir=/tmp/rmqstore/broker-a-01-controller-mode
storePathCommitLog=/tmp/rmqstore/broker-a-01-controller-mode/commitlog
# controller
enableControllerMode = true
controllerAddr = 127.0.0.1:9878;127.0.0.1:9868;127.0.0.1:9858

二、Controller

1、概览

ControllerManager

无论那种启动方式,底层都依赖ControllerManager实现。

ControllerManager管理controller的相关组件:

  1. BrokerHeartbeatManager:管理broker心跳;
  2. Controller:实现broker的master-slave切换。目前只有基于dledger(raft)实现的DLedgerController,依赖BrokerHeartbeatManager中broker的心跳信息;
kotlin 复制代码
public class ControllerManager {
    // dledger(raft) controller
    private Controller controller;
    // broker心跳管理
    private BrokerHeartbeatManager heartbeatManager;
}

DLedgerController

DLedgerController的成员变量分为两部分:DLedger组件和业务组件。

php 复制代码
public class DLedgerController implements Controller {
    /** dledger 部分**/
    // dledger raft server
    private final DLedgerServer dLedgerServer;
    private final ControllerConfig controllerConfig;
    private final DLedgerConfig dLedgerConfig;
    // raft角色变更处理
    private final RoleChangeHandler roleHandler;
    // raft状态机 -> replicasInfoManager
    private final DLedgerControllerStateMachine statemachine;

    /** 业务部分 **/
    // 单线程处理raft请求
    private final EventScheduler scheduler;
    private final EventSerializer eventSerializer;
    // broker副本管理
    private final ReplicasInfoManager replicasInfoManager;
    // 定时任务future 利用BrokerValidPredicate 扫描下线master broker 重新选主
    private final ScheduledExecutorService scanInactiveMasterService;
    private ScheduledFuture scanInactiveMasterFuture;
    // 监听broker状态变更
    private List<BrokerLifecycleListener> brokerLifecycleListeners;
    // broker判活函数
    private BrokerValidPredicate brokerAlivePredicate;
    // broker选主策略
    private ElectPolicy electPolicy;
}

DLedger组件

  1. DLedgerServer :raft server实现,在4.x HA已经大致了解过:
    1. 选主:基于peers成员列表controllerDLegerPeers发现同组raft节点,拥有最新raft日志(term+index)且得票过半的成员成为Raft Leader;
    2. 日志复制:接收业务AppendEntry请求,执行raft读写;
  1. RoleChangeHandler :监听当前raft成员的角色变更,DLedgerController.RoleChangeHandler
  2. StateMachine :状态机。当选主完成后,leader可以接收写请求,当raft日志过半写完成后,应用到状态机DLedgerControllerStateMachine

这里与4.x的DLedgerCommitLog不同的是,DLedgerCommitLog的raft日志写完成后,不需要应用raft日志到内存状态机。因为DLedgerCommitLog每条raft日志(DLedgerEntry)就是包了raft头的commitlog,仅仅利用raft过半写。

业务组件

  1. ReplicasInfoManager :broker副本信息管理,实际raft状态机最终就是更新ReplicasInfoManager中的内存数据
  2. BrokerValidPredicate:broker判活函数,根据BrokerHeartbeatManager记录的broker心跳信息,判断broker是否存活;
  3. ElectPolicy:broker选主策略,当broker判活返回false,执行策略选新的master broker,目前只有一种实现DefaultElectPolicy;
  4. scanInactiveMasterFuture :定时扫描BrokerHeartbeatManager中的broker心跳信息,利用BrokerValidPredicate 判活,如果broker下线,执行ElectPolicy重新选master broker;
  5. EventScheduler:raft请求处理线程;

2、brokerId/brokerControllerId

从5.1.1版本开始,controller模式下的brokerId由controller分配,参照docs/cn/controller/persistent_unique_broker_id.md。

broker首次上线和controller协商得到brokerId,与其他数据存储在数据目录的brokerIdentity文件中。

brokerId在controller和broker之间,代表broker组的副本id概念。

在broker角色确定以后,master broker向nameserver注册会使用brokerId=0,slave broker会使用controller分配的brokerId。

3、broker心跳管理

DefaultBrokerHeartbeatManager管理broker心跳。

swift 复制代码
public class DefaultBrokerHeartbeatManager implements BrokerHeartbeatManager {
    // 心跳超时检测
    private ScheduledExecutorService scheduledService;
    // cluster+brokerName+brokerControllerId -> broker心跳信息
    private final Map<BrokerIdentityInfo/* brokerIdentity*/, BrokerLiveInfo> brokerLiveTable;
    // broker失活监听
    private final List<BrokerLifecycleListener> brokerLifecycleListeners;
}

在心跳管理中,每个broker的唯一标识是BrokerIdentityInfo

注意,这里brokerId就是上面提到的brokerControllerId。

arduino 复制代码
public class BrokerIdentityInfo {
    private final String clusterName;
    private final String brokerName;
    private final Long brokerId; // 注意 不是传统brokerId
}

BrokerLiveInfo,broker心跳信息。包含通讯层channel,心跳时间、commitlog写进度等。

arduino 复制代码
public class BrokerLiveInfo {
    private final String brokerName;
    private String brokerAddr;
    // 心跳超时时间
    private long heartbeatTimeoutMillis;
    // netty channel
    private Channel channel;
    // brokerControllerId
    private long brokerId;
    // 上次心跳时间
    private long lastUpdateTimestamp;
    private int epoch;
    // commitlog写进度
    private long maxOffset;
    private long confirmOffset;
    private Integer electionPriority;
}

DefaultBrokerHeartbeatManager#onBrokerHeartbeat

代码过长,当broker发来心跳请求,新增或更新BrokerLiveInfo。

4、broker副本信息管理

ReplicasInfoManager维护broker组副本信息,每个broker组的唯一标识就是brokerName。

  • replicaInfoTable:brokerName-副本成员信息;
  • syncStateSetInfoTable:brokerName-副本syncStateSet和master的brokerId;

这两份内存数据都需要经过raft写来更新,controller重启后需要通过raft日志回放来恢复。

具体的增删改查待后面分析,先了解一下数据模型。

arduino 复制代码
public class ReplicasInfoManager {
    private final Map<String/* brokerName */, BrokerReplicaInfo> replicaInfoTable;
    private final Map<String/* brokerName */, SyncStateInfo> syncStateSetInfoTable;
}

BrokerReplicaInfo维护的是副本组内的成员信息。

brokerIdInfo维护了brokerId(副本id)到broker地址的映射关系。

arduino 复制代码
public class BrokerReplicaInfo {
    private final String clusterName;

    private final String brokerName;

    // Start from 1
    private final AtomicLong nextAssignBrokerId;

    private final Map<Long/*brokerId*/, Pair<String/*ipAddress*/, String/*registerCheckCode*/>> brokerIdInfo;

    public BrokerReplicaInfo(String clusterName, String brokerName) {
        this.clusterName = clusterName;
        this.brokerName = brokerName;
        this.nextAssignBrokerId = new AtomicLong(1);
        this.brokerIdInfo = new ConcurrentHashMap<>();
    }
}

SyncStateInfo维护了两部分数据:

  1. syncStateSet:组中同步进度跟上Master的副本集合(包含Master);
  2. masterBrokerId:当前副本组中的master的brokerId;

每份数据都有对应的版本,称为xxxEpoch。

kotlin 复制代码
public class SyncStateInfo {
    private final String clusterName;
    private final String brokerName;
    // syncStateSet
    private final AtomicInteger syncStateSetEpoch;
    private Set<Long/*brokerId*/> syncStateSet;
    // master
    private final AtomicInteger masterEpoch;
    private Long masterBrokerId;
}

5、raft读写

broker发往leader controller的大部分请求都需要走raft流程,底层还是走dledger.jar提供raft能力,即和DLedgerCommitLog使用同一套api。

最终会调用EventScheduler#appendEvent这个api。

DLedgerController.EventScheduler#appendEvent:

确认当前节点是raft leader的情况下,将业务方法(supplier)封装为ControllerEventHandler,放入内存队列。

EventScheduler单线程处理请求

DLedgerController.ControllerEventHandler#run:

supplier先执行业务方法,返回ControllerResult。

如果读请求或业务方法返回ControllerResult.events非空,走raft读。

默认情况下,controller未开启raft读 ,即broker从controller读的数据不能保证线性一致,在请求入队之前确认自己是leader,就返回当前内存数据。

开启raft读(isProcessReadEvent=true),需要执行一次raft写,并没有实现readIndex和leaseRead这类优化。

DLedgerController.ControllerEventHandler#run:

如果是写请求且业务返回ControllerResult.events非空,需要走raft写。

可以看到raft日志项(DLedgerEntry)的body部分都是json序列化的event。

DLedgerControllerStateMachine#onApply:

当过半节点raft日志写成功,master将这些日志应用到状态机。

ReplicasInfoManager#applyEvent:

目前,controller所有raft状态机数据都存储在ReplicasInfoManager中。

6、RoleChangeHandler

DLedgerController.RoleChangeHandler#handle:controller自身raft角色发生变更,处理相关业务。

如果成为candidate或follower,关闭EventScheduler raft请求处理线程,关闭broker下线扫描定时任务。

如果成为leader:

1)进行一次raft空写,确保所有日志被应用到内存状态机;

2)开启EventScheduler raft请求处理线程;

3)开启定时任务,扫描是否有master broker下线,可触发broker选主;

如果上述处理失败(如raft空写失败),持续重试,直到自己非raft leader。

三、Broker

核心组件

AutoSwitchHAService

DefaultMessageStore构造:

传统master-slave,采用DefaultHAService;

开启controller模式,使用AutoSwitchHAService

AutoSwitchHAService继承原来的DefaultHAService,大部分父类方法和成员变量都能复用。

实现切换broker角色,负责master-slave之间的数据同步。

核心成员变量包括:

  1. epochCache:每个master任期内,同步commitlog的offset范围;
  2. syncStateSet:master角色,组中同步进度跟上Master的Slave副本加上Master的brokerId集合,这里是broker侧真正维护syncStateSet的地方;
  3. syncStateSetChangedListeners:master角色,监听syncStateSet变化,上报给controller;
  4. haClient:slave角色,与master通讯的客户端;
  5. brokerControllerId:controller为当前broker实例分配的brokerId;
scala 复制代码
public class AutoSwitchHAService extends DefaultHAService {
    // slave副本 - 上次追上master的时间
    private final ConcurrentHashMap<Long/*brokerId*/, Long/*lastCaughtUpTimestamp*/> connectionCaughtUpTimeTable = new ConcurrentHashMap<>();
    // 通知controller syncStateSet变化
    private final List<Consumer<Set<Long/*brokerId*/>>> syncStateSetChangedListeners = new ArrayList<>();
    // syncStateSet
    private final Set<Long/*brokerId*/> syncStateSet = new HashSet<>();
    private final Set<Long> remoteSyncStateSet = new HashSet<>();
    //  Indicate whether the syncStateSet is currently in the process of being synchronized to controller.
    private volatile boolean isSynchronizingSyncStateSet = false;
    // 每个master任期内 同步commitlog的offset范围
    private EpochFileCache epochCache;
    // slave角色haClient
    private AutoSwitchHAClient haClient;
    // controller分配的brokerId
    private Long brokerControllerId = null;
}

ReplicasManager

BrokerController#initialize:

controller模式,构造ReplicasManager。

ReplicasManager主要与Controller通讯,根据Controller指令执行AutoSwitchHAService切换broker角色。

arduino 复制代码
public class ReplicasManager {
    // controller模式下的ha服务
    private final AutoSwitchHAService haService;
    // controller地址
    private List<String> controllerAddresses;
    private final ConcurrentMap<String, Boolean> availableControllerAddresses;
    private volatile String controllerLeaderAddress = "";
    // ReplicasManager状态
    private volatile State state = State.INITIAL;
    // replica注册状态
    private volatile RegisterState registerState = RegisterState.INITIAL;
    // 当前broker在controller侧的id
    private Long brokerControllerId;
    // 组内成为master的brokerControllerId
    private Long masterBrokerId;
    // broker的唯一标识
    private BrokerMetadata brokerMetadata;
    private TempBrokerMetadata tempBrokerMetadata;
    // syncStateSet
    private Set<Long> syncStateSet;
    private int syncStateSetEpoch = 0;
    // master broker地址
    private String masterAddress = "";
    private int masterEpoch = 0;
}

EpochEntry

每个broker都维护了一份EpochFileCache

内存中的体现是一个TreeMap ,key是master的任期,value是EpochEntry

EpochEntry代表一个master epoch中commitlog的开始和结束位置

这份内存数据持久化在epochFileCheckpoint文件中。

存储形式是:EpochEntry数量+crc32(所有EpochEntry)+EpochEntry。

其中每个EpochEntry以epoch-startOffset的形式存储。

注:每个EpochEntry的endOffset等于下一个EpochEntry的startOffset。

broker上线主流程

ReplicasManager#startBasicService:BrokerController#start阶段,controller模式broker上线的核心流程

  1. 发现leader controller;
  2. 向leader controller注册;
  3. broker选主,选主成功则正式上线(解除隔离状态);
  4. 开启定时任务,每5s从leader controller获取broker组元数据;
  5. 开始监听AutoSwitchHAService维护的syncStateSet变化,通知controller;

大致状态变更如下:

broker隔离状态isIsolated

BrokerController#initialize:

broker初始化阶段,如果开启controller模式,broker会处于隔离状态,isIsolated=true。

BrokerController#start:

如果broker处于隔离状态isIsolated=true,代表broker还没做好准备工作。

此时broker不向nameserver发送注册请求,无法被client发现,不会对外提供服务。

只有当broker确定自己是master还是slave后,才能设置isIsolated=false,开始对外提供服务。

broker发现leader controller

ReplicasManager#updateControllerMetadata:

循环controller地址列表,发送CONTROLLER_GET_METADATA_INFO请求。

直到找到正常返回的一个leader controller的通讯地址。

DLedgerController#getControllerMetadata:

controller集群中任意节点都能处理这个请求,获取当前DLedger组内的成员状态返回。

broker注册到leader controller

从5.1.1开始,broker不再以自身ip+port作为唯一标识注册到controller(5.0.x-5.1.0都是以ip+port作为唯一标识),而是以一个双方协商得到的一个brokerId作为唯一标识

这个id类似zk集群的myid,和mysql的server_uuid,都在数据目录下,作为一个节点的唯一标识。

只不过zk的myid需要运维配置死,保证不冲突;mysql的server_uuid自动生成。

此外brokerId不是全局唯一,是broker副本组内唯一。

ReplicasManager#register:

  1. confirmNowRegisteringState:由于brokerId协商设计两个中间状态,所以需要通过当前文件系统里的实际情况,恢复注册状态;
  2. getNextBrokerId+createTempMetadataFile:broker分配的brokerId;
  3. applyBrokerId+createMetadataFileAndDeleteTemp:broker应用brokerId;
  4. registerBrokerToController:真正注册broker到leader controller;

分配brokerId

ReplicasManager#getNextBrokerId:

broker侧,发送CONTROLLER_GET_NEXT_BROKER_ID请求。

ReplicasInfoManager#getNextBrokerId:

controller侧,如果broker组还未上线,分配brokerId=1,否则返回下一个brokerId。

BrokerReplicaInfo只是返回当前下一个brokerId。

如果多个broker同时来申请,返回的是同一个brokerId,将在应用brokerId阶段发生冲突

ReplicasManager#createTempMetadataFile:

broker侧,根据ip+当前时间戳生成一个checkCode校验码,将clusterName+brokerName+brokerId+checkCode,写入一个brokerIdentity-temp文件。

应用brokerId

ReplicasManager#applyBrokerId:

Step1,broker侧,携带上一阶段从leader controller得到的brokerId和自己生成的checkCode,请求leader controller应用brokerId(CONTROLLER_APPLY_BROKER_ID)。

ReplicasInfoManager#applyBrokerId:

Step2,controller侧,判断brokerId是否满足以下条件之一:

1)broker副本组首次出现,且brokerId=1;

2)broker副本组中不存在brokerId,即broker副本首次上线;

3)broker副本组中存在brokerId,但是checkCode校验码相同,这是因为controller侧应用id成功,broker侧应用id失败(createMetadataFileAndDeleteTemp);

满足上述条件,才能应用brokerId,提交ApplyBrokerIdEvent到raft。

ReplicasInfoManager#handleApplyBrokerId:

Step3,将ApplyBrokerIdEvent进行raft写,最终应用到状态机:

1)ReplicasInfoManager.replicaInfoTable,副本组信息,加入副本组;

2)ReplicasInfoManager.syncStateSetInfoTable,副本组同步信息,初始化;

ReplicasManager#register:

broker侧,Step4-1,如果leader controller未成功应用brokerId。

可能是由于brokerId被组内其他broker实例占用,删除brokerIdentity-temp文件,状态回滚。

ReplicasManager#createMetadataFileAndDeleteTemp:

broker侧,Step4-2,如果leader controller成功应用brokerId。

创建brokerIdentity 文件,包含clusterName+brokerName+brokerId,删除brokerIdentity-temp文件。

注册

ReplicasManager#registerBrokerToController:broker侧

broker携带应用成功的brokerId向leader controller发送注册请求。

leader controller会返回当前broker组的信息:syncStateSet、成为master的brokerId和通讯地址。

在注册阶段,就可能已经能确定当前broker的角色了。

ReplicasInfoManager#registerBroker:controller侧

1)校验broker组和brokerId的合法性;(必须走完上述brokerId协商流程)

2)如果master存活,将master信息放入结果;

3)如果broker地址发生变化,需要走一次raft写,最终将broker地址更新到replicaInfoTable中;

所以一般情况下,broker重新上线,地址不变,注册流程只是根据brokerId获取了当前master和SyncStateSet。

broker注册状态恢复

ReplicasManager#confirmNowRegisteringState:

当broker发生重启,需要根据brokerIdentity-tempbrokerIdentity文件的存在情况,判断当前注册状态处于哪一步。

broker选主

ReplicasManager#startBasicService:

如果注册阶段,broker就已经发现master broker了,则直接上线,即setFenced(false)设置isIsolated=false

否则需要完成选主,如果选主失败startBasicService每隔5s重试,每次选主前broker需要发送心跳给所有controller

broker心跳包

ReplicasManager#sendHeartbeatToController:心跳包中包含

  1. getLastEpoch:当前最新master的epoch,即最后一个EpochEntry;(选举条件1)
  2. getMaxPhyOffset:commitlog最大物理offset;(选举条件2)
  3. getBrokerElectionPriority:broker选举优先级,默认都是Integer.MAX_VALUE;(选举条件3)
  4. getControllerHeartBeatTimeoutMills:心跳超时时间,默认10s,由broker配置决定,controller根据这个超时时间可判断broker下线;

broker发送选主请求

ReplicasManager#brokerElect:

broker侧,请求leader controller,发送CONTROLLER_ELECT_MASTER请求。

如果broker返回master信息,则切换自己角色为master或slave。

controller选主请求处理

ReplicasInfoManager#electMaster:controller侧选主逻辑。

Step1,如果副本组刚上线,第一个发送CONTROLLER_ELECT_MASTER的broker成为master。

Step2,使用DefaultElectPolicy选主。

Step3-1,如果master未发生变化,直接返回CONTROLLER_MASTER_STILL_EXIST,包含master信息。

Step3-2,如果master发生变化,组装响应数据,提交ElectMasterEvent,需要走一轮raft写。

注意响应数据里的syncStateSet只包含新master,和后面应用到内存的一致。

ReplicasInfoManager#handleElectMaster:

Step4,raft写日志成功,应用到状态机。

如果选出新主,更新masterBrokerId(含epoch),更新syncStateSet只包含新master(含epoch);

如果未选出新主,这往往是controller探测broker下线导致,只会更新masterBrokerId=null(含epoch),后面再看。

ControllerRequestProcessor#handleControllerElectMaster:

Step5,对于选出新主的情况,controller会通知所有组内broker,NOTIFY_BROKER_ROLE_CHANGED。

选主策略

DefaultElectPolicy#elect:

controller优先从syncStateSet中的broker选择master,降级从所有副本中选择master。

但是默认enableElectUncleanMaster=false ,即不会走降级逻辑,只会从syncStateSet同步副本中选master

DefaultElectPolicy#tryElect:

  1. 根据BrokerValidPredicate函数,过滤存活broker集合;
  2. 如果老master仍然存活,直接返回老master;
  3. 如果是通过electMaster命令指定brokerId成为master,直接返回指定brokerId;
  4. 根据broker心跳信息+自定义comparator比较,选择broker,这也是broker需要提前发送心跳的原因;

DefaultBrokerHeartbeatManager#isBrokerActive:

broker存活判断函数,全局都使用这个,判断心跳表中存在对应broker实例,且未心跳超时。

心跳超时时间通过broker侧配置文件指定controllerHeartBeatTimeoutMills,默认10s

DefaultElectPolicy#comparator :根据epoch>maxOffset>brokerElectionPriority比较BrokerLiveInfo。

epoch:broker目前所处master任期(见后面EpochFile);

maxOffset:broker目前commitlog的write pagecache进度;

brokerElectionPriority:broker配置项,越小优先级越高,默认Integer.MAX_VALUE;

综上,默认优先选任期最大的,其次选commitlog写进度最大的。

broker切主

主流程

ReplicasManager#changeToMaster:

1)更新内存SyncStateSet;

2)关闭定时拉取config目录下数据任务(如消费进度、topic配置等);

3)HAService处理

4)broker角色变更(SYNC_MASTER);

5)开启特殊消息调度,如事务消息回查、延迟消息、pop消息;

6)更新master信息;

7)开启定时向leader controller汇报当前SyncStateSet

8)topic配置版本跟随master任期;

9)向nameserver发送一次注册请求,master可对外提供服务;

AutoSwitchHAService#changeToMaster:

由于master-slave同步,不是基于一条一条消息的,是基于一段一段commitlog的buffer的,即同步的数据是没有边界的。这里需要先截断传输了一半的消息,再恢复内存数据。

1)停止通讯层所有连接;

2)truncateInvalidMsg截断错误消息;

3)恢复confirmOffset,这里就是上面截断后的commitlog最大offset;

4)epoch记录截断;

5)增加新的epoch记录

6)等待reput(consumequeue构建)追上commitlog进度;

kotlin 复制代码
@Override
public boolean changeToMaster(int masterEpoch) {
    final int lastEpoch = this.epochCache.lastEpoch();
    if (masterEpoch < lastEpoch) {
        LOGGER.warn("newMasterEpoch {} < lastEpoch {}, fail to change to master", masterEpoch, lastEpoch);
        return false;
    }
    // 1. 通讯层connection清理
    destroyConnections();
    if (this.haClient != null) {
        this.haClient.shutdown();
    }

    // 2. 截断commitlog和consumequeue
    final long truncateOffset = truncateInvalidMsg();

    // 3. 计算confirmOffset,这里会取当前commitlog的最大offset
    this.defaultMessageStore.setConfirmOffset(computeConfirmOffset());

    // 4. 按照上面commitlog计算的offset,截断epoch文件中记录的offset
    if (truncateOffset >= 0) {
        this.epochCache.truncateSuffixByOffset(truncateOffset);
    }

    // 5. 创建新的epoch记录
    final EpochEntry newEpochEntry = new EpochEntry(masterEpoch, this.defaultMessageStore.getMaxPhyOffset());
    if (this.epochCache.lastEpoch() >= masterEpoch) {
        this.epochCache.truncateSuffixByEpoch(masterEpoch);
    }
    this.epochCache.appendEntry(newEpochEntry);

    // 6. 等待reput(consumequeue)追上commitlog
    while (defaultMessageStore.dispatchBehindBytes() > 0) {
        try {
            Thread.sleep(100);
        } catch (Exception ignored) {

        }
    }

    // 7. 开启TransientStorePool,等待 writebuffer commit
    if (defaultMessageStore.isTransientStorePoolEnable()) {
        waitingForAllCommit();
        defaultMessageStore.getTransientStorePool().setRealCommit(true);
    }
    // ...
    return true;
}

截断CommitLog

AutoSwitchHAService#truncateInvalidMsg:截断错误消息。

1)如果reput进度赶上commitlog进度,代表所有消息都完整,直接返回;

2)从reput进度遍历到commitlog进度;(并不是从commitlog头开始遍历);

3)checkMessageAndReturnSize循环处理每个offset的消息,直到发现不完整消息;

4)truncateDirtyFiles按照这个offset截断commitlog和consumequeue;

ini 复制代码
public long truncateInvalidMsg() {
    // reput进度未落后于commitlog,消息完整,不需要截断
    long dispatchBehind = this.defaultMessageStore.dispatchBehindBytes();
    if (dispatchBehind <= 0) {
        return -1;
    }
    boolean doNext = true;
    long reputFromOffset = this.defaultMessageStore.getReputFromOffset();
    do {
        // 根据reput进度向后找最后一条完整的消息
        SelectMappedBufferResult result = this.defaultMessageStore.getCommitLog().getData(reputFromOffset);
        if (result == null) {
            break;
        }
        try {
            reputFromOffset = result.getStartOffset();
            int readSize = 0;
            while (readSize < result.getSize()) {
                DispatchRequest dispatchRequest = this.defaultMessageStore.getCommitLog().checkMessageAndReturnSize(result.getByteBuffer(), false, false);
                if (dispatchRequest.isSuccess()) {
                    // 完整的消息
                    int size = dispatchRequest.getMsgSize();
                    if (size > 0) {
                        reputFromOffset += size;
                        readSize += size;
                    } else {
                        reputFromOffset = this.defaultMessageStore.getCommitLog().rollNextFile(reputFromOffset);
                        break;
                    }
                } else {
                    // 不完整的消息,退出大循环
                    doNext = false;
                    break;
                }
            }
        } finally {
            result.release();
        }
    } while (reputFromOffset < this.defaultMessageStore.getMaxPhyOffset() && doNext);
    // 截断commitlog consumequeue
    this.defaultMessageStore.truncateDirtyFiles(reputFromOffset);
    return reputFromOffset;
}

截断EpochEntry

EpochFileCache#truncateSuffixByOffset:如果某个epoch的startOffset超出commitlog,需要把这个epoch截断删除。

EpochFileCache#truncateSuffixByEpoch:如果某个epoch大于等于目前master epoch,需要截断这个epoch。

AutoSwitchHAService#changeToMaster:

截断历史EpochEntry后,需要加入一条当前任期的EpochEntry记录。

broker切从

ReplicasManager#changeToSlave:

  1. 停止SyncStateSet上报;
  2. 修改broker角色为SLAVE;
  3. 停止特殊消息调度,比如延迟消息、事务消息回查检查、pop消息处理;
  4. 将controller分配给broker的id作为brokerId;
  5. 更新内存master信息;
  6. 开启定时从master拉取config目录下数据,如消费进度;
  7. haService处理;
  8. topic配置版本更新为新epoch;
  9. 向所有nameserver发送注册请求(注意这里能拿到nameserver返回的master的ha地址,可以进行后续主从同步);
kotlin 复制代码
public void changeToSlave(final String newMasterAddress, final int newMasterEpoch, Long newMasterBrokerId) {
    synchronized (this) {
        if (newMasterEpoch > this.masterEpoch) {
            this.masterEpoch = newMasterEpoch;
            if (newMasterBrokerId.equals(this.masterBrokerId)) {
                // if master doesn't change
                this.haService.changeToSlaveWhenMasterNotChange(newMasterAddress, newMasterEpoch);
                this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(newMasterEpoch);
                registerBrokerWhenRoleChange();
                return;
            }
            // 1. 停止syncStateSet上报
            stopCheckSyncStateSet();
            // 2. 修改broker角色
            this.brokerController.getMessageStoreConfig().setBrokerRole(BrokerRole.SLAVE);
            // 3. 停止特殊消息调度,比如延迟消息、事务消息回查检查、pop消息处理
            this.brokerController.changeSpecialServiceStatus(false);
            // 4. 将controller分配给broker的id作为brokerId 向nameserver注册
            this.brokerConfig.setBrokerId(brokerControllerId);
            // 5. 更新master信息
            this.masterAddress = newMasterAddress;
            this.masterBrokerId = newMasterBrokerId;
            // 6. 开启定时拉取config目录下数据,如消费进度
            handleSlaveSynchronize(BrokerRole.SLAVE);
            // 7. haService处理
            this.haService.changeToSlave(newMasterAddress, newMasterEpoch, brokerControllerId);
            // 8. topic配置版本更新为新epoch
            this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(newMasterEpoch);
            // 9. 向所有nameserver发送注册请求 --- 能拿到master的ha地址
            registerBrokerWhenRoleChange();
        }
    }
}

AutoSwitchHAService#changeToSlave:

关闭master才有的HAConnection,开启slave才用的HAClient。

所有commitlog处理在主从同步中处理。

四、主从同步

与传统同步比较

  1. 发现master的方式 相同

master上线后,向nameserver发送心跳,心跳包含自己的ha地址;

slave通过nameserver发现master的ha地址,与master的ha地址建立连接。

  1. 线程模型 相同

slave创建一个HAClient线程负责底层通讯和数据落盘。

master用一个AcceptSocketService线程负责接收slave连接。

master将每个slave连接封装为一个HAConnection

master每个HAConnection包含一个读socket线程和一个写socket线程

  1. 其他文件同步方式 相同

走master非ha地址,每隔x秒同步config目录下的非commitlog相关数据,如topic配置、消费进度等。

  1. commitlog同步流程 不同

传统模式同步流程如下:

1)slave:DefaultHAClient发送自己的offset;

2)master:DefaultHAConnection根据slave的offset,每次传送最多32k大小的commitlog;

3)slave:DefaultHAClient收到数据写commitlog;

controller模式同步流程有些不同,引入了HAConnectionState。

HAConnectionState

在5.x中引入了HAConnectionState连接状态。

arduino 复制代码
public enum HAConnectionState {
    /**
     * Ready to start connection.
     */
    READY,
    /**
     * CommitLog consistency checking.
     */
    HANDSHAKE,
    /**
     * Synchronizing data.
     */
    TRANSFER,
    /**
     * Connection shutdown.
     */
    SHUTDOWN,
}

传统同步没有做流程上的改造,只是定义了哪些行为属于哪个状态,状态如何变更。

  1. 对于slave,DefaultHAClient,READY-刚创建还未与master建立连接,TRANSFER-与master建立连接持续同步commitlog,SHUTDOWN-关闭;
  2. 对于master,DefaultHAConnection,TRANSFER-正常运行与slave进行同步,SHUTDOWN-关闭;

而在controller模式下,TRANSFER前多了HANDSHAKE阶段,在正式同步前交换一些信息,做一些准备工作。

READY阶段

只有slave端存在READY阶段。

AutoSwitchHAClient#run:

  1. 将commitlog/consumequeue按照完整消息截断,逻辑和broker切主中的一致;
  2. 将EpochFile按照截断后的offset截断;
  3. 与master建立连接,如果成功,进入HANDSHAKE;

HANDSHAKE阶段

1.slave发送handshake

slave的handshake数据包含四部分:

  1. 状态:HANDSHAKE;
  2. isSyncFromLastFile:当slave没有commitlog时,master是否从最后一个commitlog文件开始同步。默认false,即slave没有commitlog,master需要从第一个commitlog开始从头同步在传统同步模式下这点不一样,如果slave没有任何commitlog,只能从master最近一份commitlog文件的头部开始同步,无法改变这个行为(4.x的HA分析过)
  3. isAsyncLearner:是否是异步learner,默认false,即一个正常的slave。如果是异步learner,只同步数据,不参与选主(不会进入SyncStateSet);
  4. brokerId:controller分配给broker的id;

AutoSwitchHAClient#sendHandshakeHeader:

2.master接收handshake

AutoSwitchHAConnection.ReadSocketService.HAServerReader#processReadResult:

master收到HANDSHAKE数据包,将slave的三个属性注入AutoSwitchHAConnection。

3.master回复handshake(发送EpochEntry)

AutoSwitchHAConnection.AbstractWriteSocketService#buildHandshakeBuffer:

master回复包含:

  1. 最大offset;
  2. 当前epoch;
  3. 所有EpochEntry;

4.slave接收handshake(截断)

slave收到master的EpochEntry后,执行截断逻辑。

AutoSwitchHAClient#doTruncate:slave无数据的情况,直接进入下一阶段。

AutoSwitchHAClient#doTruncate:slave有数据的情况:

  1. 比对自己和master的EpochEntry,找到截断点offset
  2. 截断commitlog/consumequeue/EpochEntry;
  3. 进入TRANSFER状态;
  4. 汇报自己的offset给master,让master开始传输commitlog;

EpochFileCache#findConsistentPoint:找截断点

  1. 将EpochEntry按照epoch倒序遍历,即从后往前找
  2. 找到epoch和startOffset相同的entry
  3. 返回slave和master中对应entry的endOffset的最小值作为截断点;

TRANSFER阶段

1.slave发送offset

AutoSwitchHAClient#transferFromMaster:

slave:当写通道空闲5s或收到master传输的commitlog导致offset变更,向master汇报自己的offset。

2.master接收offset

AutoSwitchHAConnection.ReadSocketService.HAServerReader#processReadResult:

master收到slave的offset:

  1. 记录当前slave的offset;
  2. 扩展SyncStateSet
  3. 更新confirmOffset
  4. 唤醒写消息等待ha的线程;

3.master传输commitlog

AutoSwitchHAConnection.AbstractWriteSocketService#run:

首次收到slave的offset,计算从哪里开始传输nextTransferFromWhere

如果slaveOffset非0,直接从对应offset开始即可;

如果slaveOffset为0:

  1. 默认isSyncFromLastFile=false,从第一个commitlog的起始offset开始传输;
  2. 可设置isSyncFromLastFile=true,从最后一个commitlog的起始offset开始传输,这种情况和非controller模式一致;

AutoSwitchHAConnection.AbstractWriteSocketService#transferToSlave:

  1. 根据nextTransferFromWhere从commitlog读buffer;
  2. 限制commitlog传输大小size不超出32kb;
  3. 对于一段commitlog,跨两个epoch的情况,需要分两次传输
  4. 构造请求头;
  5. 写请求头和commitlog buffer到slave;

AutoSwitchHAConnection.AbstractWriteSocketService#buildTransferHeaderBuffer:

请求头包含:

  1. 这段commitlog buffer的起始offset
  2. 这段commitlog buffer的epoch和startOffset
  3. 当前master的confirmOffset

4.slave保存commitlog

AutoSwitchHAClient.HAClientReader#processReadResult:slave收到transfer包:

  1. 校验本地offset与数据包offset一致;
  2. 当收到属于新epoch的commitlog,持久化一条新EpochEntry
  3. 如果body存在,写commitlog;(master会在空闲期间,向slave发送只包含header的transfer包)
  4. 更新confirmOffset
  5. offset变更,继续汇报offset给master,进入下一轮transfer;

五、confirmOffset管理

confirmOffset作用

confirmOffset的一个主要的作用是,控制reput线程构建consumequeue

ReputMessageService#doReput:

只有到达confirmOffset的消息,才能够被reput处理,才能对消费者可见。

CommitLog#getConfirmOffset:

在controller模式下,master的confirmOffset往往需要通过computeConfirmOffset计算得到,slave的confirmOffset由master同步而来。

而传统master-slave模式下就是最大物理offset。

confirmOffset变更

AutoSwitchHAClient.HAClientReader#processReadResult:

transfer阶段,slave的confirmOffset从master同步而来,confirmOffset由master决定。

less 复制代码
haService.getDefaultMessageStore().setConfirmOffset(Math.min(confirmOffset, messageStore.getMaxPhyOffset()));

AutoSwitchHAService#computeConfirmOffset:master计算confirmOffset逻辑

  1. 如果SyncStateSet中,存在未与master建立连接的slave,confirmOffset不会变更 。这意味着:直到SyncStateSet收缩踢出slave,才能让confirmOffset变更,才能让新消息放入consumequeue被消费者看到;(ISSUE#6662)
  2. 正常情况下,从所有与master建立连接的slave中,且在SyncStateSet中的slave中,选择slaveOffset最小的成为confirmOffset,这意味着:如果slave在SyncStateSet中,但是同步很慢,会让新消息放入consumequeue变慢,有消费延迟
  3. 如果没有与master建立连接的slave,或SyncStateSet中只有master,那么master自己的最大offset就是confirmOffset;

有多种情况会触发master的confirmOffset变更。

AutoSwitchHAService#updateConfirmOffsetWhenSlaveAck:

case1,transfer阶段,SyncStateSet中的slave汇报offset

AutoSwitchHAService#setSyncStateSet:

case2,由于各种原因造成SyncStateSet变更(需要controller走一轮raft写),需要重新计算confirmOffset;

AutoSwitchHAService#changeToMaster:

case3,刚成为master,此时SyncStateSet中只有master自己,confirmOffset就是master的最大offset。

confirmOffset恢复

confirmOffset在每次变更都会写入内存StoreCheckpoint

DefaultMessageStore#addScheduleTask:checkpoint会每隔1s刷盘。

StoreCheckpoint#flush:confirmOffset跟随一些其他属性一起异步刷盘。

DefaultMessageStore#load:broker重启,初始化阶段,会将confirmOffset重新载入内存。

DefaultMessageStore#start:broker重启,启动阶段,会将confirmOffset注入reput服务,从这个位置开始构建consumequeue。

六、SyncStateSet管理

SyncStateSet初始化

ReplicasInfoManager#electMaster:

回顾controller选主,在新一轮选主结束后SyncStateSet只包含新master

AutoSwitchHAService#setSyncStateSet:

master broker将controller返回的SyncStateSet维护到内存。

SyncStateSet扩张(基于offset)

AutoSwitchHAService#maybeExpandInSyncStateSet:

master收到slave的offset后,如果slave不在SyncStateSet中,可能导致SyncStateSet扩张。

如果slave的offset追上master的confirmOffset,代表slave已经追上broker组内最小slave的offset(如果组内只有master,则已经追上master)。

在slave的offset追上confirmOffset的前提下,slave的offset在当前epoch中 ,才真正能扩张SyncStateSet

SyncStateSet收缩(基于时间)

读空闲

AutoSwitchHAConnection.ReadSocketService#run:

master超过20s未从slave读到数据关闭连接。

AutoSwitchHAService#removeConnection:

移除slave连接前,将slave从SyncStateSet中移除,异步通知controller。

lastCaughtUpTime

master在connectionCaughtUpTimeTable记录了每个slave副本上次追上master的时间。

AutoSwitchHAConnection#updateLastTransferInfo:

每次master传输数据前会记录当前commitlog的最大offset和传输开始时间。

AutoSwitchHAConnection#maybeExpandInSyncStateSet:

当master收到slave回复的offset,超过上次记录的commitlog的最大offset,更新lastCaughtUpTime。

ReplicasManager#checkSyncStateSetAndDoReport:

每隔5s,master会判断SyncStateSet是否能收缩,如果SyncStateSet发生变化通知controller。

AutoSwitchHAService#maybeShrinkSyncStateSet:

如果slave超过15秒没有追上过master ,或者slave根本没有与master建立连接,将slave从SyncStateSet剔除

SyncStateSet变更

参考ISSUE-5663极端情况下消息丢失。

SyncStateSet在broker侧需要三个成员变量来处理中间状态:

  1. isSynchronizingSyncStateSet:broker是否正在向controller同步SyncStateSet,true-已经发送同步请求,还未收到controller响应,false-broker与controller的SyncStateSet一致;
  2. syncStateSet:本地SyncStateSet,如果isSynchronizingSyncStateSet=true,这是上次的SyncStateSet;
  3. remoteSyncStateSet:远程SyncStateSet,broker正在向controller同步的SyncStateSet,还未得到controller确认;

AutoSwitchHAService#getSyncStateSet:

计算confirmOffset时,SyncStateSet如果正在同步,会取本地和远程的SyncStateSet的并集来判断。

broker发送SyncStateSet变更请求

AutoSwitchHAService#markSynchronizingSyncStateSet:

broker设置isSynchronizingSyncStateSet标记为true,代表正在执行SyncStateSet同步。

将新的SyncStateSet先放入remoteSyncStateSet,代表正在向controller同步的SyncStateSet。

ReplicasManager#doReportSyncStateSetChanged:

broker将当前的masterEpoch、syncStateSetEpoch、新SyncStateSet发送给leader controller。

controller处理SyncStateSet变更请求

ReplicasInfoManager#alterSyncStateSet:

  1. 校验参数合法;(masterEpoch正确、旧SyncStateSetEpoch正确、syncStateSet在controller侧存活等等)
  2. 拼接response,包含SyncStateSet的新epoch版本;
  3. 提交raft写;

ReplicasInfoManager#handleAlterSyncStateSet:

raft日志写成功后,应用到内存syncStateSetInfoTable,可供后续反查。

broker处理SyncStateSet真实变更

AutoSwitchHAService#setSyncStateSet:

broker收到controller回复,实际更新syncStateSet

ReplicasManager#schedulingSyncBrokerMetadata:

如果broker与controller通讯异常,需要通过每5s从leader controller拉取SyncStateSet来补偿

七、写消息(quorum write)

ReplicasManager#changeToMaster:

在broker切主时,BrokerRole被设置成了SYNC_MASTER ,但这并不代表同步复制

CommitLog#asyncPutMessage:

5.x提出了quorum write 机制,在SYNC_MASTER 下通过一系列参数和SyncStateSet计算出需要等待slave ack的数量needAckNums

在controller模式下:

  1. 如果SyncStateSet.size小于minInSyncReplicas ,返回IN_SYNC_REPLICAS_NOT_ENOUGH拒绝写入,minInSyncReplicas默认为1,而SyncStateSet至少有master自己,所以默认不会拒绝写入;
  2. 如果allAckInSyncStateSet =true,代表需要所有SyncStateSet中的副本ack,才能响应客户端,默认false
  3. 如果不满足上述情况,走inSyncReplicas 代表等待slave ack数量,默认inSyncReplicas=1,即master写入即响应,即默认行为和ASYNC_MASTER一致

CommitLog#handleHA:

master写commitlog成功,刷盘处理完成(同步或异步刷盘),处理复制逻辑。

默认上面计算到needAckNums=1,只需要master副本,直接返回OK,逻辑同ASYNC_MASTER

如果inSyncReplicas=2,这里就需要等待2个副本ack,即除了master还需要一个slave ack。

如果allAckInSyncStateSet=true,这里需要所有SyncStateSet中的副本ack。

具体逻辑不深入分析。

八、broker故障转移

broker定时发送心跳给controller

BrokerController#scheduleSendHeartbeat:

当broker实例选主结束,isIsolated=true,每隔1s所有controller持续发送心跳。

ReplicasManager#sendHeartbeatToController:

心跳包中,将broker自己配置的超时时间controllerHeartBeatTimeoutMills=10s给controller。

leader controller扫描下线master broker

case1 DLedgerController扫描

DLedgerController#scanInactiveMasterAndTriggerReelect:

leader controller每隔5s扫描下线master broker。

ReplicasInfoManager#scanNeedReelectBrokerSets:

扫描每个副本组,如果满足:

  1. 超过10s(controllerHeartBeatTimeoutMills)未收到master心跳(validPredicate);
  2. 组内存在其他副本存活(10s内收到过心跳);

则加入重新选主的结果集。

case2 BrokerHeartbeatManager扫描

DefaultBrokerHeartbeatManager#scanNotActiveBroker:

在通讯层每隔5s也会判断broker心跳是否超时(10s),如果超时关闭底层通讯channel,触发重新选主。

controller重新选主

ControllerManager#onBrokerInactive:

对于DLedgerController扫描master-broker下线,直接触发重新选主;

对于BrokerHeartbeatManager扫描broker下线,需要先获取副本组信息,判断下线broker是master才重新选主。

ControllerManager#triggerElectMaster:

leader controller选主逻辑同broker上线阶段描述,参考DefaultElectPolicy。

如果选出新主,通知组内其他broker。

ControllerManager#doNotifyBrokerRoleChanged:

通知其他broker产生新主,请求包含:masterAdress、masterBrokerId、masterEpoch、SyncStateSet、SyncStateSetEpoch等。

需要注意的是,leader controller发送的是oneway请求,不保证broker能及时收到组内master变更情况,需要broker有补偿逻辑

broker收到master变更请求

ReplicasManager#changeBrokerRole:

broker正常收到NOTIFY_BROKER_ROLE_CHANGED请求,校验masterEpoch变更,切换为master或slave。

broker定时获取副本组信息(补偿)

leader controller通知master broker变更是个oneway请求,需要broker通过定时反查leader controller补偿。

ReplicasManager#schedulingSyncBrokerMetadata:

broker在上线之后,每隔5s请求leader controller查询当前副本组信息,包括master信息和SyncStateSet信息。

如果masterEpoch发生变更,补偿的是重新选主:

  1. 如果已经选出master,切换为master或slave;
  2. 如果未选出master,调用leader controller推荐自己成为master(默认enableElectUncleanMaster=false,需要当前broker在SyncStateSet中才能成为master);

如果masterEpoch未变更,补偿的是SyncStateSet变更(isSynchronizingSyncStateSet=true这个中间态)。

总结

本文分析了rocketmq5.x controller模式的实现。

broker上线

controller模式下,broker启动后处于隔离状态,即isIsolated=true

需要确定broker实例在broker组内角色后,才能解除隔离状态,向nameserver发送心跳,对外提供服务。

broker上线大致分为三步:

  1. 发现leader controller:请求任意controller获取leader controller地址;
  2. 向leader controller注册:如果当前broker实例还未得到brokerId,需要与leader controller协商得到brokerId,然后将brokerId、brokerName、brokerAddr注册到leader controller;
  3. 选举master broker:如果组内已经存在master,在注册阶段就会返回master broker信息和SyncStateSet;如果组内不存在master,broker需要请求leader controller进行master选举,最终得到master broker信息和SyncStateSet。broker比对自己的brokerId和masterBrokerId,判断切从还是切主。

选举策略

如果broker实例是组内第一个上线的broker,那么直接成为master。

否则走DefaultElectPolicy选主。

默认 SyncStateSet中的副本选master(开启enableElectUncleanMaster=true,支持从所有副本中选master):

  1. 过滤心跳超时(10s)broker;
  2. 如果老master仍然存活,直接返回老master;
  3. 如果是通过electMaster命令指定brokerId成为master,直接返回指定brokerId;
  4. 根据broker心跳信息,按照epoch>maxOffset>brokerElectionPriority的优先级选择;

epoch:broker目前所处master任期;

maxOffset:broker目前commitlog的write pagecache进度;

brokerElectionPriority:broker配置项,越小优先级越高,默认Integer.MAX_VALUE;

主从切换

切主

  1. 根据controller返回的SyncStateSet,更新内存中的SyncStateSet;

  2. 关闭定时拉取config目录下数据任务(如消费进度、topic配置等);

  3. HAService处理

    1. 停止通讯层所有连接;
    2. 截断错误消息commitlog/consumequeue等;
    3. 恢复confirmOffset;
    4. epoch记录截断;
    5. 增加新的epoch记录
    6. 等待reput(consumequeue构建)追上commitlog进度;
  4. broker角色变更(SYNC_MASTER,brokerId=0);

  5. 开启特殊消息调度,如事务消息回查、延迟消息、pop消息;

  6. 更新master信息;

  7. 开启定时向leader controller汇报当前SyncStateSet

  8. topic配置版本跟随master任期;

  9. 向nameserver发送一次注册请求,master可对外提供服务;

切从

  1. 停止SyncStateSet上报;
  2. 修改broker角色为SLAVE;
  3. 停止特殊消息调度,比如延迟消息、事务消息回查检查、pop消息处理;
  4. 将controller分配给broker的id作为brokerId;
  5. 更新内存master信息;
  6. 开启定时从master拉取config目录下数据,如消费进度;
  7. haService处理,关闭通讯连接;
  8. topic配置版本更新为新epoch;
  9. 向所有nameserver发送注册请求(注意这里能拿到nameserver返回的master的ha地址,可以进行后续主从同步);

主从同步

controller模式与传统master-slave同步的区别,主要在同步流程上多了HANDSHAKE阶段。

READY阶段:

  1. slave:截断错误消息commitlog/consumequeue等;
  2. slave:截断epochEntry;
  3. slave:与master建立连接;

HANDSHAKE阶段:

  1. slave:发送一些flag(isSyncFromLastFile、isAsyncLearner)和自己的brokerId;
  2. master:将上述flag注入HAConnection;
  3. master:回复slave当前最大offset、epoch、所有EpochEntry;
  4. slave:根据master的EpochEntry,截断commitlog/consumequeue/epochEntry;

TRANSFER阶段:

  1. slave:持续发送自己的最大offset;
  2. master:更新SyncStateSet、confirmOffset,唤醒等待HA线程;
  3. master:发送commitlog,注意commitlog不能跨epoch传输,请求头包含confirmOffset、当前commitlog所属EpochEntry;
  4. slave:保存confirmOffset,保存commitlog,如果commitlog属于新Epoch,创建新EpochEntry;

ConfirmOffset

confirmOffset代表已经确认的offset,在confirmOffset之后的消息可以被reput线程处理,构建consuemqueue对消费者可见。

confirmOffset由master broker维护,持久化在checkpoint文件中,slave会在同步commitlog阶段从master获取。

confirmOffset是SyncStateSet中所有副本的offset最小值,如果SyncStateSet中只有master,那么是master的最大物理offset。

一般confirmOffset有两种变更情况:

  1. SyncStateSet中的slave由于持续同步,slave offset增加导致confirmOffset增加;
  2. SyncStateSet扩张或收缩;

SyncStateSet

SyncStateSet代表一个broker组中同步进度跟上Master的副本集合(包含Master)。

SyncStateSet由master broker维护,由controller持久化(raft)。

SyncStateSet的作用有很多,包括选举、confirmOffset推进、quorum write等等。

在新一轮master broker选举结束后,SyncStateSet只包含master自己。

当slave的offset追上master的confirmOffset后,slave进入SyncStateSet。

slave长时间(15s)未追上master,或长时间未发送数据包(20s),将被逐出SyncStateSet。

写消息(quorum write)

虽然master broker上线后,角色是SYNC_MASTER,但是实际行为默认同ASYNC_MASTER

在controller模式下:

  1. 如果SyncStateSet.size小于minInSyncReplicas ,返回IN_SYNC_REPLICAS_NOT_ENOUGH拒绝写入,minInSyncReplicas默认为1,而SyncStateSet至少有master自己,所以默认不会拒绝写入;
  2. 如果allAckInSyncStateSet =true,代表需要所有SyncStateSet中的副本ack,才能响应客户端,默认false
  3. 如果不满足上述情况,需要等待inSyncReplicas 个slave ack,默认inSyncReplicas=1,即master写入即响应,即默认行为和ASYNC_MASTER一致

故障转移

broker:上线后,每隔1s 向所有controller发送心跳包,心跳包指定了心跳超时时间为10s

leader controller:每隔5s扫描下线master broker。

leader controller:按照选主策略,如果选主成功,通知组内所有broker,请求包含masterAdress、masterBrokerId、masterEpoch、SyncStateSet、SyncStateSetEpoch。

broker:收到角色变更通知,判断masterEpoch和masterBrokerId,决定切主或切从。

broker:上线后,每隔5s请求leader controller查询当前副本组信息,包括master信息和SyncStateSet信息,用于补偿角色变更通知和SyncStateSet变更通知。

参考资料:

  1. github.com/apache/rock...
  2. RIP-44
  3. ISSUE#6662
  4. ISSUE#5663

欢迎大家评论或私信讨论问题。

本文原创,未经许可不得转载。

欢迎关注公众号【程序猿阿越】。

相关推荐
数据龙傲天8 分钟前
1688商品API接口:电商数据自动化的新引擎
java·大数据·sql·mysql
带带老表学爬虫36 分钟前
java数据类型转换和注释
java·开发语言
千里码aicood43 分钟前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
彭于晏6891 小时前
Android广播
android·java·开发语言
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455661 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
2401_857297912 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
架构师吕师傅2 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
福大大架构师每日一题2 小时前
23.1 k8s监控中标签relabel的应用和原理
java·容器·kubernetes
金灰2 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5