前言
本章基于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
相关配置项:
- controllerDLegerGroup:一个raft组;
- controllerDLegerPeers:raft组成员列表;
- controllerDLegerSelfId:当前节点id;
- controllerStorePath:raft数据存储位置,默认在user.home/DledgerController;
- 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个即可。
相关配置项:
- enableControllerMode=true,开启controller模式;
- controllerAddr,指定controller地址列表;
- 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的相关组件:
- BrokerHeartbeatManager:管理broker心跳;
- 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组件
- DLedgerServer :raft server实现,在4.x HA已经大致了解过:
-
- 选主:基于peers成员列表controllerDLegerPeers发现同组raft节点,拥有最新raft日志(term+index)且得票过半的成员成为Raft Leader;
- 日志复制:接收业务AppendEntry请求,执行raft读写;
- RoleChangeHandler :监听当前raft成员的角色变更,DLedgerController.RoleChangeHandler;
- StateMachine :状态机。当选主完成后,leader可以接收写请求,当raft日志过半写完成后,应用到状态机DLedgerControllerStateMachine;
这里与4.x的DLedgerCommitLog不同的是,DLedgerCommitLog的raft日志写完成后,不需要应用raft日志到内存状态机。因为DLedgerCommitLog每条raft日志(DLedgerEntry)就是包了raft头的commitlog,仅仅利用raft过半写。
业务组件
- ReplicasInfoManager :broker副本信息管理,实际raft状态机最终就是更新ReplicasInfoManager中的内存数据;
- BrokerValidPredicate:broker判活函数,根据BrokerHeartbeatManager记录的broker心跳信息,判断broker是否存活;
- ElectPolicy:broker选主策略,当broker判活返回false,执行策略选新的master broker,目前只有一种实现DefaultElectPolicy;
- scanInactiveMasterFuture :定时扫描BrokerHeartbeatManager中的broker心跳信息,利用BrokerValidPredicate 判活,如果broker下线,执行ElectPolicy重新选master broker;
- 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维护了两部分数据:
- syncStateSet:组中同步进度跟上Master的副本集合(包含Master);
- 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之间的数据同步。
核心成员变量包括:
- epochCache:每个master任期内,同步commitlog的offset范围;
- syncStateSet:master角色,组中同步进度跟上Master的Slave副本加上Master的brokerId集合,这里是broker侧真正维护syncStateSet的地方;
- syncStateSetChangedListeners:master角色,监听syncStateSet变化,上报给controller;
- haClient:slave角色,与master通讯的客户端;
- 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上线的核心流程
- 发现leader controller;
- 向leader controller注册;
- broker选主,选主成功则正式上线(解除隔离状态);
- 开启定时任务,每5s从leader controller获取broker组元数据;
- 开始监听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:
- confirmNowRegisteringState:由于brokerId协商设计两个中间状态,所以需要通过当前文件系统里的实际情况,恢复注册状态;
- getNextBrokerId+createTempMetadataFile:broker分配的brokerId;
- applyBrokerId+createMetadataFileAndDeleteTemp:broker应用brokerId;
- 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-temp 和brokerIdentity文件的存在情况,判断当前注册状态处于哪一步。
broker选主
ReplicasManager#startBasicService:
如果注册阶段,broker就已经发现master broker了,则直接上线,即setFenced(false)设置isIsolated=false。
否则需要完成选主,如果选主失败startBasicService每隔5s重试,每次选主前broker需要发送心跳给所有controller。
broker心跳包
ReplicasManager#sendHeartbeatToController:心跳包中包含
- getLastEpoch:当前最新master的epoch,即最后一个EpochEntry;(选举条件1)
- getMaxPhyOffset:commitlog最大物理offset;(选举条件2)
- getBrokerElectionPriority:broker选举优先级,默认都是Integer.MAX_VALUE;(选举条件3)
- 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:
- 根据BrokerValidPredicate函数,过滤存活broker集合;
- 如果老master仍然存活,直接返回老master;
- 如果是通过electMaster命令指定brokerId成为master,直接返回指定brokerId;
- 根据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:
- 停止SyncStateSet上报;
- 修改broker角色为SLAVE;
- 停止特殊消息调度,比如延迟消息、事务消息回查检查、pop消息处理;
- 将controller分配给broker的id作为brokerId;
- 更新内存master信息;
- 开启定时从master拉取config目录下数据,如消费进度;
- haService处理;
- topic配置版本更新为新epoch;
- 向所有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处理在主从同步中处理。
四、主从同步
与传统同步比较
- 发现master的方式 相同
master上线后,向nameserver发送心跳,心跳包含自己的ha地址;
slave通过nameserver发现master的ha地址,与master的ha地址建立连接。
- 线程模型 相同
slave创建一个HAClient线程负责底层通讯和数据落盘。
master用一个AcceptSocketService线程负责接收slave连接。
master将每个slave连接封装为一个HAConnection。
master每个HAConnection包含一个读socket线程和一个写socket线程。
- 其他文件同步方式 相同
走master非ha地址,每隔x秒同步config目录下的非commitlog相关数据,如topic配置、消费进度等。
- 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,
}
传统同步没有做流程上的改造,只是定义了哪些行为属于哪个状态,状态如何变更。
- 对于slave,DefaultHAClient,READY-刚创建还未与master建立连接,TRANSFER-与master建立连接持续同步commitlog,SHUTDOWN-关闭;
- 对于master,DefaultHAConnection,TRANSFER-正常运行与slave进行同步,SHUTDOWN-关闭;
而在controller模式下,TRANSFER前多了HANDSHAKE阶段,在正式同步前交换一些信息,做一些准备工作。
READY阶段
只有slave端存在READY阶段。
AutoSwitchHAClient#run:
- 将commitlog/consumequeue按照完整消息截断,逻辑和broker切主中的一致;
- 将EpochFile按照截断后的offset截断;
- 与master建立连接,如果成功,进入HANDSHAKE;
HANDSHAKE阶段
1.slave发送handshake
slave的handshake数据包含四部分:
- 状态:HANDSHAKE;
- isSyncFromLastFile:当slave没有commitlog时,master是否从最后一个commitlog文件开始同步。默认false,即slave没有commitlog,master需要从第一个commitlog开始从头同步 。在传统同步模式下这点不一样,如果slave没有任何commitlog,只能从master最近一份commitlog文件的头部开始同步,无法改变这个行为(4.x的HA分析过) 。
- isAsyncLearner:是否是异步learner,默认false,即一个正常的slave。如果是异步learner,只同步数据,不参与选主(不会进入SyncStateSet);
- 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回复包含:
- 最大offset;
- 当前epoch;
- 所有EpochEntry;
4.slave接收handshake(截断)
slave收到master的EpochEntry后,执行截断逻辑。
AutoSwitchHAClient#doTruncate:slave无数据的情况,直接进入下一阶段。
AutoSwitchHAClient#doTruncate:slave有数据的情况:
- 比对自己和master的EpochEntry,找到截断点offset;
- 截断commitlog/consumequeue/EpochEntry;
- 进入TRANSFER状态;
- 汇报自己的offset给master,让master开始传输commitlog;
EpochFileCache#findConsistentPoint:找截断点
- 将EpochEntry按照epoch倒序遍历,即从后往前找;
- 找到epoch和startOffset相同的entry;
- 返回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:
- 记录当前slave的offset;
- 扩展SyncStateSet;
- 更新confirmOffset;
- 唤醒写消息等待ha的线程;
3.master传输commitlog
AutoSwitchHAConnection.AbstractWriteSocketService#run:
首次收到slave的offset,计算从哪里开始传输nextTransferFromWhere。
如果slaveOffset非0,直接从对应offset开始即可;
如果slaveOffset为0:
- 默认isSyncFromLastFile=false,从第一个commitlog的起始offset开始传输;
- 可设置isSyncFromLastFile=true,从最后一个commitlog的起始offset开始传输,这种情况和非controller模式一致;
AutoSwitchHAConnection.AbstractWriteSocketService#transferToSlave:
- 根据nextTransferFromWhere从commitlog读buffer;
- 限制commitlog传输大小size不超出32kb;
- 对于一段commitlog,跨两个epoch的情况,需要分两次传输;
- 构造请求头;
- 写请求头和commitlog buffer到slave;
AutoSwitchHAConnection.AbstractWriteSocketService#buildTransferHeaderBuffer:
请求头包含:
- 这段commitlog buffer的起始offset
- 这段commitlog buffer的epoch和startOffset
- 当前master的confirmOffset
4.slave保存commitlog
AutoSwitchHAClient.HAClientReader#processReadResult:slave收到transfer包:
- 校验本地offset与数据包offset一致;
- 当收到属于新epoch的commitlog,持久化一条新EpochEntry;
- 如果body存在,写commitlog;(master会在空闲期间,向slave发送只包含header的transfer包)
- 更新confirmOffset;
- 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逻辑
- 如果SyncStateSet中,存在未与master建立连接的slave,confirmOffset不会变更 。这意味着:直到SyncStateSet收缩踢出slave,才能让confirmOffset变更,才能让新消息放入consumequeue被消费者看到;(ISSUE#6662)
- 正常情况下,从所有与master建立连接的slave中,且在SyncStateSet中的slave中,选择slaveOffset最小的成为confirmOffset,这意味着:如果slave在SyncStateSet中,但是同步很慢,会让新消息放入consumequeue变慢,有消费延迟;
- 如果没有与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侧需要三个成员变量来处理中间状态:
- isSynchronizingSyncStateSet:broker是否正在向controller同步SyncStateSet,true-已经发送同步请求,还未收到controller响应,false-broker与controller的SyncStateSet一致;
- syncStateSet:本地SyncStateSet,如果isSynchronizingSyncStateSet=true,这是上次的SyncStateSet;
- 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:
- 校验参数合法;(masterEpoch正确、旧SyncStateSetEpoch正确、syncStateSet在controller侧存活等等)
- 拼接response,包含SyncStateSet的新epoch版本;
- 提交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模式下:
- 如果SyncStateSet.size小于minInSyncReplicas ,返回IN_SYNC_REPLICAS_NOT_ENOUGH拒绝写入,minInSyncReplicas默认为1,而SyncStateSet至少有master自己,所以默认不会拒绝写入;
- 如果allAckInSyncStateSet =true,代表需要所有SyncStateSet中的副本ack,才能响应客户端,默认false;
- 如果不满足上述情况,走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:
扫描每个副本组,如果满足:
- 超过10s(controllerHeartBeatTimeoutMills)未收到master心跳(validPredicate);
- 组内存在其他副本存活(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发生变更,补偿的是重新选主:
- 如果已经选出master,切换为master或slave;
- 如果未选出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上线大致分为三步:
- 发现leader controller:请求任意controller获取leader controller地址;
- 向leader controller注册:如果当前broker实例还未得到brokerId,需要与leader controller协商得到brokerId,然后将brokerId、brokerName、brokerAddr注册到leader controller;
- 选举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):
- 过滤心跳超时(10s)broker;
- 如果老master仍然存活,直接返回老master;
- 如果是通过electMaster命令指定brokerId成为master,直接返回指定brokerId;
- 根据broker心跳信息,按照epoch>maxOffset>brokerElectionPriority的优先级选择;
epoch:broker目前所处master任期;
maxOffset:broker目前commitlog的write pagecache进度;
brokerElectionPriority:broker配置项,越小优先级越高,默认Integer.MAX_VALUE;
主从切换
切主
-
根据controller返回的SyncStateSet,更新内存中的SyncStateSet;
-
关闭定时拉取config目录下数据任务(如消费进度、topic配置等);
-
HAService处理;
- 停止通讯层所有连接;
- 截断错误消息commitlog/consumequeue等;
- 恢复confirmOffset;
- epoch记录截断;
- 增加新的epoch记录;
- 等待reput(consumequeue构建)追上commitlog进度;
-
broker角色变更(SYNC_MASTER,brokerId=0);
-
开启特殊消息调度,如事务消息回查、延迟消息、pop消息;
-
更新master信息;
-
开启定时向leader controller汇报当前SyncStateSet;
-
topic配置版本跟随master任期;
-
向nameserver发送一次注册请求,master可对外提供服务;
切从
- 停止SyncStateSet上报;
- 修改broker角色为SLAVE;
- 停止特殊消息调度,比如延迟消息、事务消息回查检查、pop消息处理;
- 将controller分配给broker的id作为brokerId;
- 更新内存master信息;
- 开启定时从master拉取config目录下数据,如消费进度;
- haService处理,关闭通讯连接;
- topic配置版本更新为新epoch;
- 向所有nameserver发送注册请求(注意这里能拿到nameserver返回的master的ha地址,可以进行后续主从同步);
主从同步
controller模式与传统master-slave同步的区别,主要在同步流程上多了HANDSHAKE阶段。
READY阶段:
- slave:截断错误消息commitlog/consumequeue等;
- slave:截断epochEntry;
- slave:与master建立连接;
HANDSHAKE阶段:
- slave:发送一些flag(isSyncFromLastFile、isAsyncLearner)和自己的brokerId;
- master:将上述flag注入HAConnection;
- master:回复slave当前最大offset、epoch、所有EpochEntry;
- slave:根据master的EpochEntry,截断commitlog/consumequeue/epochEntry;
TRANSFER阶段:
- slave:持续发送自己的最大offset;
- master:更新SyncStateSet、confirmOffset,唤醒等待HA线程;
- master:发送commitlog,注意commitlog不能跨epoch传输,请求头包含confirmOffset、当前commitlog所属EpochEntry;
- 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有两种变更情况:
- SyncStateSet中的slave由于持续同步,slave offset增加导致confirmOffset增加;
- 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模式下:
- 如果SyncStateSet.size小于minInSyncReplicas ,返回IN_SYNC_REPLICAS_NOT_ENOUGH拒绝写入,minInSyncReplicas默认为1,而SyncStateSet至少有master自己,所以默认不会拒绝写入;
- 如果allAckInSyncStateSet =true,代表需要所有SyncStateSet中的副本ack,才能响应客户端,默认false;
- 如果不满足上述情况,需要等待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变更通知。
参考资料:
欢迎大家评论或私信讨论问题。
本文原创,未经许可不得转载。
欢迎关注公众号【程序猿阿越】。