选举
流程按照官方样例梳理,example中的election模块
初始化
- 创建节点配置信息NodeOptions
- 创建状态机ElectionOnlyStateMachine,绑定至NodeOptions
- 创建配置文件Configuration,解析配置的服务列表initialServerAddressList,绑定至NodeOptions
- 根据dataPath设置日志存储路径
- 根据dataPath设置元数据存储路径
- 按照NodeOptions配置的ServerAddress创建服务节点PeerId
- 根据PeerId配置的端口号创建RpcServer服务
- 注册raft处理器至RpcServer:RaftRpcServerFactory.addRaftRequestProcessors(rpcServer)
- 创建raft group服务框架RaftGroupService,入参groupId、serverId、nodeOptions、rpcServer
- 启动raftGroupService
启动raftGroupService
- 将server地址信息添加至NodeManager
- RaftServiceFactory创建并初始化raft节点,入参:groupId、serverId、nodeOptions
- 创建raft节点
- 根据groupId、PeerId创建NodeImpl
- 根据当前node实例创建配置上下文ConfigurationCtx
- 初始化Node节点
- 启动rpcServer.start
初始化Node节点
- 绑定NodeOptions的ServiceFactory配置至Node
- 绑定NodeOptions至Node
- 绑定NodeOptions的raftOptions配置至Node
- 如果启用性能统计则创建NodeMetrics,Metrics 监控
- 创建TimerManager管理器
- 初始化TimerManager线程池大小:cpu * 3。最大20
- 创建投票任务RepeatedTimer,超时时间默认1秒:options.electionTimeoutMs
- 创建选举任务electionTimer,实例类型及配置与投票任务相同
- 创建stepDownTimer,超时时间:electionTimeoutMs * 2
- 创建快照任务snapshotTimer,超时时间默认1小时:snapshotIntervalSecs * 1000
- 创建配置文件管理器ConfigurationManager
- 创建applyDisruptor,disruptorBufferSize默认16384(16k),生产类型为_MULTI,队列满时等待策略为阻塞类型_BlockingWaitStrategy,事件handle句柄LogEntryAndClosureHandler,异常handle句柄LogExceptionHandler
- 创建finite state machine FSMCaller
- 初始化日志存储initLogStorage
- 初始化元数据存储initMetaStorage
- 初始化有限状态机initFSMCaller
- 创建投票箱BallotBox
- 创建投票箱参数BallotBoxOptions
- 设置投票箱waiter参数为fsmCaller
- 设置投票箱closureQueue参数为closureQueue
- 初始化投票箱
- 初始化快照存储initSnapshotStorage
- 日志管理器校验一致性
- 创建replicatorGroup
- 根据replicatorGroup创建BoltRaftClientService
- 创建raft参数ReplicatorGroupOptions
- 初始化BoltRaftClientService
- 初始化replicatorGroup
- 创建readOnlyService
- 创建ReadOnlyServiceOptions参数
- 初始化readOnlyService
- 设置当前节点状态为STATE_FOLLOWER
- 启动快照任务snapshotTimer
- 如果当前节点conf不为空则执行stepDown
- 添加当前节点至NodeManager
- 获取写锁,判断当前group的conf配置中是否只有一个节点(一般至少3个节点),如果是则该节点必须是Leader,立即触发一次electSelf任务选举自己为leader
stepDown
- 如果当前节点是candidate状态则停止投票任务stopVoteTimer
- 如果是transfering或者leader状态则停止stepDown下台任务
- resetLeaderId重置leaderId
- 状态置为follow,confCtx重置,更新上次leader时间戳updateLastLeaderTimestamp
- 启动选举任务electionTimer.start
启动rpcServer
添加leader状态监听
- ElectionOnlyStateMachine状态器中添加监听
- ElectionOnlyStateMachine继承至StateMachineAdapter,在收到leader启动或stop状态时回调监听列表
超时进行选举handleElectionTimeout
- 如果当前不是follow状态返回
- 如果当前时间-上次leader时间小于超时时间直接返回
- 重置leaderId
- 预投票preVote
- 初始化预投票上下文prevVoteCtx
- 遍历配置文件中的PeerId列表封装为UnfoundPeerId添加至peers
- 设置quorum为当前peers数量/2+1(即过半),oldQuorum同样逻辑
- 遍历向其他节点发送PreVote预投票请求,创建OnPreVoteRpcDone对象绑定连接对应的PeerId,创建RequestVoteRequest请求,请求成功或超时回调OnPreVoteRpcDone
- 预投票请求回调OnPreVoteRpcDone,metrics记录花费时间,获取预投票响应RequestVoteResponse,如果响应投票通过,则标识该节点投票通过并递减Quorum,如果通过数过半则选举自己electSelf为leader
投票处理RequestVoteRequestProcessor
- 如果PeerId解析正常则处理请求processRequest0
- 如果是预投票,则服务端处理预投票请求RaftServerService.handlePreVoteRequest
- 否则处理_request-vote请求_handleRequestVoteRequest
- 调用实现子类NodeImpl.handlePreVoteRequest实现处理预投票请求
- 解析候选id失败直接返回candidateId(即请求中serverId参数)
- 如果当前节点是leader并且处于有效期内(未超时),直接返回投票不通过
- 如果请求term小于当前term直接返回失败。当前节点刚刚选举成为leader时可能没有启动复制任务,校验复制任务checkReplicator
- 如果请求term等于当前term+1(大于当前term一个批次)。与上个情况类似,同样需要校验复制任务是否正常checkReplicator
- 如果请求的最新日志requestLastLogId大于等于当前最新日志lastLogId。则投票通过标识granted为true
Read And Write
流程按照官方样例梳理,example中的counter模块
Server
- 创建CounterServer服务,配置与选举类似
- 创建RpcServer
- 注册内置处理器以及业务处理器GetValueRequestProcessor、IncrementAndGetRequestProcessor
- 创建状态机CounterStateMachine
- 创建RaftGroupService服务
- 启动服务raftGroupService
启动raftGroupService流程在选举中已梳理。不再赘述,直接查看服务端业务处理模块,递增也就是写入逻辑IncrementAndGetRequestProcessor
Write
- 获取当前状态机判断是否是leader,如果不是则返回转发请求响应,如果本地已经获取到了leader Node属性,则返回响应中附带leader信息
- 创建Value响应对象ValueResponse
- 创建递增闭包IncrementAndAddClosure
- 创建Task任务,设置Done动作为递增闭包Closure,序列化request请求绑定至data
- counterServer获取当前raft节点node应用apply任务
- 当前节点实现NodeImpl执行apply任务
- 创建日志LogEntry
- 创建泛型为LogEntryAndClosure的EventTranslator事件转换器
- 死循环使用RingBuffer尝试发布事件,重试次数为3
- 发布事件至Disruptor,事件处理句柄会实时消费事件处理事件
- 事件消费LogEntryAndClosureHandler,如果事件数量大于等于执行batch配置applyBatch(默认32)或者endOfBatch为true,则执行任务
- 批量执行任务executeApplyingTasks
- 如果当前节点不是leader,根据状态设置异常信息,如果是_STATE_TRANSFERRING状态则说明当前节点处于忙状态,状态设置为EBUSY_,将任务批量提交至线程池_CLOSURE_EXECUTOR_执行,线程池默认core为cpu数,max为CPU数*100,keepAlive超时为1分钟,队列为SynchronousQueue同步队列
- 如果当前expectedTerm与当前currTerm不一致,批量提交任务执行,状态为_EPERM_
- 将任务追加至投票箱ballotBox中的闭包队列closureQueue.appendPendingClosure,ClosureQueueImpl实现的队列实际类型是一个链表LinkedList
- 日志管理器logManager批量添加条目appendEntries,入参LeaderStableClosure
- 将LeaderStableClosure闭包封装为StableClosureEvent事件发布至磁盘RingBuffer diskQueue
- 稳定的闭包事件处理句柄StableClosureEventHandler消费事件
- AppendBatcher批量追加写入日志,AppenderBatcher刷新flush
- 调用RocksDBLogStorage日志存储批量写入日志appendEntries(entries为LogEntry包装的Task.getData,当前样例中也就是客户端请求中的delta对应的是i)
- 遍历LeaderStableClosure闭包执行,状态为OK
class LeaderStableClosure extends LogManager.StableClosure {
...
@Override
public void run(final Status status) {
if (status.isOk()) {
NodeImpl.this.ballotBox.commitAt(this.firstLogIndex, this.firstLogIndex + this.nEntries - 1,
NodeImpl.this.serverId);
} else {
LOG.error("Node {} append [{}, {}] failed.", getNodeId(), this.firstLogIndex, this.firstLogIndex
+ this.nEntries -1);
}
}
}
- 投票箱提交ballotBox.commitAt,提交完成后回调状态机onCommitted方法,发布事件至taskQueue(RingBuffer)
- 申请任务句柄处理ApplyTaskHandler,执行申请任务runApplyTask
- 执行提交doCommitted
- 如果任务是Task闭包onTaskCommitted则回调onCommitted方法
- 遍历closureQueue队列,执行对应的done动作,状态为OK
- 执行申请任务doApplyTasks,回调状态机onApply方法入参会遍历得到的task任务
- 执行CounterStateMachine状态机申请任务,按照请求中的delta数据递增value。业务逻辑执行完成
- 返回提交的最大下标maxCommittedIndex
- 如果批量提交至最后节点endOfBatch为true,则AppendBatcher批量刷新flush日志,逻辑同上
读取value请求处理逻辑GetValueRequestProcessor
Read
public Object handleRequest(final BizContext bizCtx, final GetValueRequest request) throws Exception {
if (!this.counterServer.getFsm().isLeader()) {
return this.counterServer.redirect();
}
final ValueResponse response = new ValueResponse();
response.setSuccess(true);
response.setValue(this.counterServer.getFsm().getValue());
return response;
}
- 如果当前不是leader节点,转发请求
- counterServer获取状态机中的value封装为ValueResponse返回
Client
- 创建配置Configuration解析集群配置
- 路由表根据groupId更新配置RouteTable
- 创建BoltCliClientService客户端服务
- 路由表根据groupId刷新leader配置RouteTable.refreshLeader
- 路由表根据groupId选择leader,RouteTable.getInstance().selectLeader(groupId);
- 创建IncrementAndGetRequest递增请求
- 客户端服务发送递增请求cliClientService
线性一致读
ReadIndex Read
public void readFromQuorum(final String key, AsyncContext asyncContext) {
final byte[] reqContext = new byte[4];
Bits.putInt(reqContext, 0, requestId.incrementAndGet());
this.node.readIndex(reqContext, new ReadIndexClosure() {
@Override
public void run(Status status, long index, byte[] reqCtx) {
if (status.isOk()) {
try {
asyncContext.sendResponse(new ValueCommand(fsm.getValue(key)));
} catch (final KeyNotFoundException e) {
asyncContext.sendResponse(GetCommandProcessor.createKeyNotFoundResponse());
}
} else {
asyncContext.sendResponse(new BooleanCommand(false, status.getErrorMsg()));
}
}
});
}
- 直接调用node读取数据,不需要判断leader之类的操作。调用Node实现NodeImpl.readIndex方法读取数据
- 只读服务添加请求readOnlyService.addRequest,将请求上下文requestContext与ReadIndexClosure封装为ReadIndexEvent事件提交至readIndexQueue(RingBuffer)队列
- ReadIndex事件处理句柄消费消息ReadIndexEventHandler
- 消费句柄将事件缓存至本地events列表,达到阈值或确认是批次中最后一个消息则批量执行事件executeReadIndexEvents
- 根据groupId与当前serverId创建ReadIndex请求ReadIndexRequest
- 遍历事件将请求上下文RequestContext、将requestContext、ReadIndexClosure闭包与开始时间封装为ReadIndexState添加至请求ReadIndexRequest
- 当前节点处理ReadIndex请求handleReadIndexRequest,入参为request请求,及将状态与request封装为ReadIndexResponseClosure闭包
- 假定当前为follow状态,将请求与leaderId封装为ReadIndexRequest请求发送至leader
- ReadIndex请求处理器ReadIndexRequestProcessor,调用服务handleReadIndexRequest方法处理,此时已经是leader在处理,所以走readLeader分支处理
- 如果只有一个节点,那么直接响应RpcResponseClosure闭包执行返回
- 如果存在多个节点,ReadIndexResponse设置当前最新已提交的日志lastCommittedIndex
- 默认走ReadIndex Read优化读请求,否则走Lease Read优化读请求
- 将ReadIndexResponse、Quorum(过半数量)、peers数量、RpcResponseClosure闭包封装为ReadIndexHeartbeatResponseClosure使用replicatorGroup服务向其他节点发送心跳请求
- 使用BoltRaftClientService服务发送空包请求AppendEntriesRequest至其他节点,超时时间设置为正常超时的1/2,如果小于等于0则使用默认超时5秒。
- 节点实现类NodeImpl处理请求handleAppendEntriesRequest,校验请求中prevLogTerm与当前日志中读取请求指定的prevLogIndex下标处日志的term是否一致,不一致则响应失败,如果是空entries则说明是心跳请求,返回当前term以及当前最新日志index
if (entriesCount == 0) {
final AppendEntriesResponse.Builder respBuilder = AppendEntriesResponse.newBuilder()
.setSuccess(true)
.setTerm(this.currTerm)
.setLastLogIndex(this.logManager.getLastLogIndex());
doUnlock = false;
this.writeLock.unlock();
this.ballotBox.setLastCommittedIndex(Math.min(request.getCommittedIndex(), prevLogIndex));
return respBuilder.build();
}
- leader回调ReadIndexHeartbeatResponseClosure闭包,状态为成功则递增ackSuccess,过半响应则回调上层管道RpcResponseClosureAdapter。继续回调RpcRequestClosure.run,Lease Read此处回调sendResponse,因为在下一管道中填充了响应对象。
- 回调ReadIndexResponseClosure闭包
- 创建ReadIndexStatus状态,遍历ReadIndexState状态列表设置响应的日志index,如果当前最新应用的日志index(lastAppliedIndex)大于响应的日志index(其他节点的最新日志下标lastLogIndex),则通知成功
- 获取用户任务中定义的闭包ReadIndexClosure,设置index为ReadIndexState状态的index,回调用户定义的闭包ReadIndexClosure,ReadIndexClosure 回调成功,可以从状态机读取最新数据返回,如果你的状态实现有版本概念,可以根据传入的日志 index 编号做读取。
Lease Read
默认情况下,jraft 提供的线性一致读是基于 RAFT 协议的 ReadIndex 实现的,性能已经可以接受,在一些更高性能的场景下,并且可以保证集群内机器的 CPU 时钟同步,那么可以采用 Clock + Heartbeat 的 Lease Read 优化,这个可以通过服务端设置 RaftOptions
的 ReadOnlyOption
为 ReadOnlyLeaseBased
来实现。
switch (readOnlyOpt) {
case ReadOnlySafe:
...
case ReadOnlyLeaseBased:
respBuilder.setSuccess(true);
closure.setResponse(respBuilder.build());
closure.run(Status.OK());
break;
}
- Lease Read处理逻辑也是由ReadIndexRequestProcessor处理器处理。但是在节点实现处理中走了Lease Read对应的逻辑NodeImpl.readLeader
- 如果_ReadOnlyLeaseBased类型则判断当前leader是否有效_isLeaderLeaseValid,如果无效则走_ReadOnlySafe处理逻辑_
- checkLeaderLease校验leader至少有效期,当前时间-上次leader当选时间小于选举超时周期*leaderLeaseTimeRatio百分比(默认为90%,即乘以0.9)。如果是则说明当前leader仍然处于有效期。
- 选择(当前时间-节点通信的上次时间小于等于选举周期0.9默认)其中的最小值,如果存在当前时间-上次时间小于等于选举周期0.9的节点数量过半,则使用其中的最小值作为当前上次leader选举时间
- 再次校验leader是否处于有效期checkLeaderLease,如果不是则走_ReadOnlySafe处理逻辑_
- 调用上层闭包ReadIndexResponseClosure设置当前最新提交的日志下标lastCommittedIndex后执行
- 调用上层闭包RpcRequestClosure发送响应消息至follow客户端
- 响应已经回到follow端,follow节点回调上层闭包ReadIndexResponseClosure设置返回的result值,执行闭包。ReadIndexResponseClosure执行逻辑同ReadIndex Read
如果通过时间计算判断当前leader处于有效期内就不再向各个节点发送心跳请求heartbeat,整体减少了心跳请求,所以性能会优于ReadIndex Read模式。官方给出的测试效果为性能提升15%左右
总结
读取数据时为什么如果不是leader要转发请求?
- 状态机之间数据复制任务是异步的。存在中间态,如果不转发至leader直接响应本地follow节点状态机或者日志中的数据是有可能读取到陈旧的数据也就是线性一致读问题。所以请求打到非leader节点是有可能会返回错误的响应或直接响应查询失败。官网中也有提到服务提供了CP而牺牲了A,也就是保证了一致性与分区容错性,放弃了高可用性
- 两种方法确认当前节点是master简单的讲:a.向其他节点发送心跳确认。b.记录被选为leader的时间,在选举超时时间内,当前节点一定依然是leader,例如:选举超时时间是150-300ms内的随机数,选举成功后的150ms内当前leader节点一定依然是leader。当然前提要求是各个服务器的时间是同步的。为了避免时间同步问题jraft提供了一些误差配置,例如:可以对150ms进行可配置的减小一些。