Ratis学习(三): Raft Log管理

介绍

为了记录集群中所有操作的状态变化,确保状态机可以保证一致性。

在 Raft 中,集群中的每个节点都维护一个日志,用于记录节点接收到的指令或命令。这些指令可能来自客户端的请求或其他节点的广播。日志的内容是有序的,并且按照顺序进行追加。每个日志条目都包含一个索引和对应的指令。

Raft 算法通过日志来保持一致性。当节点收到新的指令时,它将指令追加到自己的日志末尾,并向其他节点发送追加日志的请求。一旦大多数节点都确认接收了该指令并将其追加到自己的日志中,该指令被视为已提交。提交的指令将在状态机中执行,以确保集群中的状态保持一致。

如果在集群中的某个节点崩溃或发生故障,Raft 算法使用日志来恢复一致性。当节点重新启动时,它会从日志中读取最新的已提交指令,并将其应用于状态机,以便与其他节点达成一致。

看代码

Raft Log 在ratis中有两种实现类,一种是内存日志,另一种是磁盘分段日志。在日常使用中磁盘分段日志更为常用。Raft Log中的日志主要是RaftServerImpl中append的过程中调用log append到日志中。

SegmentedRaftLog将日志存储在本地磁盘的文件上,以分段文件(segment file)的方式存储,每一个段文件中包含了多个Log Entries。单个段文件有8MB上限,每一条日志大小也有8MB的上限。

当一个段内的日志增多,达到段文件上限8MB之后,会关闭这个段文件,重新开启一个段文件继续写日志。被关闭的段文件不能被修改,只能因为和Leader出现冲突而被truncated

提交新日志:

Leader向RaftLog提交新的日志,实现方法appendImpl。方法首先获取日志文件writeLock然后

  • 提交以TransactionContext作为内容的日志前,允许TransactionContext执行preAppendTransaction钩子方法进行额外逻辑操作
  • RaftLogSequentialOps.Runner提交一个异步任务appendEntry,由Runner保证线性执行
  • 等待异步任务完成,返回给上层StateMachine index。

代码如下:

private long appendImpl(long term, TransactionContext operation) throws StateMachineException {
  checkLogState();
  try(AutoCloseableLock writeLock = writeLock()) {
    final long nextIndex = getNextIndex();

    // This is called here to guarantee strict serialization of callback executions in case
    // the SM wants to attach a logic depending on ordered execution in the log commit order.
    operation = operation.preAppendTransaction();
    // build the log entry after calling the StateMachine
    final LogEntryProto e = operation.initLogEntry(term, nextIndex);

    appendEntry(e);
    return nextIndex;
  }
}

异步的appendEntry写日志操作如下:

  • 将日志提交给内存中的Open Segment缓存,
  • 直接更新Log Entry Cache,将这个刚写入的日志缓存起来。注意这两个步骤不能调换,不然会出现不一致。
protected CompletableFuture<Long> appendEntryImpl(LogEntryProto entry) {
  try(AutoCloseableLock writeLock = writeLock()) {
    validateLogEntry(entry);
    // 找到目前的Segment的内存缓冲
    final LogSegment currentOpenSegment = cache.getOpenSegment();
    if (currentOpenSegment == null) {
      // 如果没有Segment,那么新建对应的Segment文件
      cache.addOpenSegment(entry.getIndex());
      fileLogWorker.startLogSegment(entry.getIndex()); // 向IO Worker提交一个新建Segment文件任务
    } else if (isSegmentFull(currentOpenSegment, entry)) {
      // 当前的Segment写满了,那么刷盘这个Segment,新建一个Segment
      cache.rollOpenSegment(true);
      fileLogWorker.rollLogSegment(currentOpenSegment);
    } 

    // If the entry has state machine data, then the entry should be inserted
    // to statemachine first and then to the cache. Not following the order
    // will leave a spurious entry in the cache.
    CompletableFuture<Long> writeFuture =
      fileLogWorker.writeLogEntry(entry).getFuture();
    cache.appendEntry(entry, LogSegment.Op.WRITE_CACHE_WITHOUT_STATE_MACHINE_CACHE);

    return writeFuture;
  } 
}

读日志并被应用到Statemachine:

在StateMachineUpdater调用get接口获得被commit的日志apply到上层statemachine。由Leader的LogAppender线程读取成功提交,需要被复制到所有Follower的新日志。

读取日志的时候首先从Log Entry Cache中读取,如果缓存中没有才会去磁盘中读

public LogEntryProto get(long index) throws RaftLogIOException {
  checkLogState();
  final LogSegment segment;
  final LogRecord record;
  try (AutoCloseableLock readLock = readLock()) {
    segment = cache.getSegment(index);
    if (segment == null) {
      return null;
    }
    record = segment.getLogRecord(index);
    if (record == null) {
      return null;
    }
    final LogEntryProto entry = segment.getEntryFromCache(record.getTermIndex());
    if (entry != null) {
      return entry;
    }
  }
  // the entry is not in the segment's cache. Load the cache without holding the lock.
  return segment.loadCache(record);
}

各组件之间的通信关键类信息:

LogAppender异步线程负责将Leader的日志发送到Follower上。

在客户端和Leader通信时,GrpcClientProtocolClient负责发起客户端请求,GrpcClientRpc负责适配请求,GrpcClientProtocolService 是服务端测,负责接收请求并将结果返回给客户端的类。

在Leader到Follower的通信时,leader在GrpcClientProtocolService的RequestStreamObserver::onNext里收到请求后,会交给RaftServerImpl::submitClientRequestAsync处理,该函数首先校验当前server是否是leader等,然后leader将请求放入待写队列,并通知leader向follower发请求的线程GrpcLogAppender。GrpcLogAppender负责将请求从leader发送到follower,leader为每个follower维护一个GrpcLogAppender线程。

GrpcLogAppender调用GrpcServerProtocolClient:: appendEntries将Leader已经写成功的请求发给follower,follower在GrpcServerProtocolService的ServerRequestStreamObserver::onNext里接收请求,并交给RaftServerImpl:: appendEntriesAsync处理,appendEntriesAsync需要首先校验Leader的请求,包括该请求的源Server是否是Leader,以及该请求是否已经在Follower写入等,然后将请求写入日志,处理完后回复给leader,leader在GrpcLogAppender的AppendLogResponseHandler::onNext检查follower是否写成功。如果leader和两个follower大多数写成功,leader向client回复成功,client在上文提到的GrpcClientProtocolClient的AsyncStreamObservers::onNext处理回复。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想做一个offer收割机

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值