Nacos 2.x 源码深度解析 (十六):JRaft 日志复制 —— 同步规则与快照管理

《Nacos 2.x源码深度解析》专栏目录
一、架构通信篇:
《Nacos 2.x 源码深度解析 (一):架构整体全貌 —— 核心模块划分与版本演进》
《Nacos 2.x 源码深度解析 (二):通信协议迭代 —— HTTP长轮询到gRPC演进》
二、配置中心篇
《Nacos 2.x 源码深度解析 (三):配置中心客户端 —— 启动加载与自动装配》
《Nacos 2.x 源码深度解析 (四):配置中心服务端 —— 事件总线与数据持久化》
《Nacos 2.x 源码深度解析 (五):gRPC 推送链路 —— 配置变更下发与动态刷新》
《Nacos 2.x 源码深度解析 (六):三级缓存体系 —— 降级兜底与故障自愈机制》
三、服务注册发现篇
《Nacos 2.x 源码深度解析 (七):服务注册流程 —— 客户端上报与服务端存储》
《Nacos 2.x 源码深度解析 (八):服务订阅机制 —— 从首次订阅到gRPC双向流变更通知》
四、grpc连接内核篇
《Nacos 2.x 源码深度解析 (九):双向流设计 —— 连接创建复用与销毁》
《Nacos 2.x 源码深度解析 (十):心跳保活策略 —— 断线检测与重连源码》
《Nacos 2.x 源码深度解析 (十一):RPC 请求调度 —— 收发模型与线程池处理》
五、集群一致性篇
《Nacos 2.x 源码深度解析 (十二):集群基础交互 —— 节点感知与基础数据同步》
《Nacos 2.x 源码深度解析 (十三):Distro 协议 ——AP 模式异步数据同步原理》
《Nacos 2.x 源码深度解析 (十四):Distro 容错处理 —— 数据校验与冲突修复》
《Nacos 2.x 源码深度解析 (十五):JRaft 架构 ——CP 集群 Leader 选举机制》
《Nacos 2.x 源码深度解析 (十六):JRaft 日志复制 —— 同步规则与快照管理》
在上一篇文章中,我们深入分析了 JRaft 协议的 Leader 选举机制,理解了 Multi-Raft Group 的创建与各 Group 独立选举 Leader 的运行方式。
但 Leader 选举完成后,JRaft 如何确保所有节点上的数据变更按相同的顺序被提交和回放?onApply() 中 iter.done() 为 null 和不为 null 时日志来源有何不同,Follower 为何要跳过 ReadRequest 日志?业务状态机的 onApply 回调背后,NacosClosure.run() 何时触发 Response 返回?快照机制又是如何通过 onSnapshotSave/onSnapshotLoad 在三层适配下实现从 Nacos 抽象快照到 JRaft 底层存储的桥接?本文将聚焦 JRaft 的日志复制与快照管理机制,从 NacosStateMachine.onApply 的日志回放入口出发,深入日志来源分流、状态机执行与 NacosClosure 回调,再到快照的触发、保存和加载流程,完整剖析 JRaft 协议中数据同步与灾备恢复的核心实现。
一、日志复制总览
在进入 onApply 的细节之前,有必要先理解日志复制在 Raft 协议中的定位,以及 JRaft 实现的核心类和实体。
1.1 日志复制在 Raft 协议中的定位
Raft 协议保证"如果两个节点上同一 term、同一 index 的日志内容相同,则两个节点的状态机执行顺序一致"。这是一致性的核心约束。Leader 将客户端请求封装为日志条目,复制到所有 Follower,当日志被多数节点确认后提交并回放到状态机。一条日志条目包含 term(任期号)、index(索引号)和 data(数据)三个核心字段。
日志复制与 Leader 选举、快照管理构成了 Raft 协议的三大核心机制:
| 机制 | 职责 | 运行频率 |
|---|---|---|
| Leader 选举 | 选出集群中唯一的 Leader | 偶发(Leader 崩溃时触发) |
| 日志复制 | 将数据变更从 Leader 同步到所有 Follower | 每次写入操作 |
| 快照管理 | 压缩日志,加速节点恢复 | 周期性(默认 30 分钟) |
在上一篇文章中,我们详细分析了 Leader 选举。本文将继续深入日志复制和快照管理的内部实现。
1.2 关键类速览
NacosStateMachine:继承 SOFAJRaft 的StateMachineAdapter,负责日志提交后的状态机执行。onApply(Iterator)是本文分析的核心方法。NacosClosure:实现 SOFAJRaft 的Closure接口,包装了日志提交完成后的回调逻辑。构造时传入Message和Closure,run(Status)时将Response传递回调用方。WriteRequest/ReadRequest:Raft 日志的两种 Protobuf 消息类型。WriteRequest触发processor.onApply()做数据变更,ReadRequest触发processor.onRequest()做数据读取。SnapshotOperation:Nacos 抽象的快照操作接口,定义了onSnapshotSave(Writer, BiConsumer)和onSnapshotLoad(Reader)两个方法。JSnapshotOperation:JRaft 适配层的 package-private 接口,桥接SnapshotOperation和 JRaft 的SnapshotWriter/SnapshotReader。NacosClosure.NacosStatus:NacosClosure内部定义的Status子类,扩展了Response和Throwable字段,用于将状态机执行的返回结果传递给外部。
二、NacosStateMachine.onApply:日志回放的统一入口
NacosStateMachine.onApply 是 JRaft 日志复制的核心方法——无论是 Leader 提交的日志还是 Follower 复制的日志,最终都会汇聚到这里执行状态机回放。本节的时序图展示了 Leader 和 Follower 两个视角下 onApply 的调用路径差异:

2.1 onApply 的双来源分流
onApply 方法接收 SOFAJRaft 的 Iterator 作为参数,其中包含了从 Raft 日志存储中读取的一系列待提交日志条目。方法的核心逻辑在于区分日志来源:
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onApply
@Override
public void onApply(Iterator iter) {
int index = 0;
int applied = 0;
Message message;
NacosClosure closure = null;
try {
while (iter.hasNext()) {
Status status = Status.OK();
try {
if (iter.done() != null) {
closure = (NacosClosure) iter.done();
message = closure.getMessage();
} else {
final ByteBuffer data = iter.getData();
message = ProtoMessageUtil.parse(data.array());
if (message instanceof ReadRequest) {
applied++; index++; iter.next();
continue;
}
}
LoggerUtils.printIfDebugEnabled(Loggers.RAFT, "receive log : {}", message);
if (message instanceof WriteRequest) {
Response response = processor.onApply((WriteRequest) message);
postProcessor(response, closure);
}
if (message instanceof ReadRequest) {
Response response = processor.onRequest((ReadRequest) message);
postProcessor(response, closure);
}
} catch (Throwable e) {
index++;
status.setError(RaftError.UNKNOWN, e.toString());
Optional.ofNullable(closure).ifPresent(closure1 -> closure1.setThrowable(e));
throw e;
} finally {
Optional.ofNullable(closure).ifPresent(closure1 -> closure1.run(status));
}
applied++;
index++;
iter.next();
}
} catch (Throwable t) {
Loggers.RAFT.error("processor : {}, stateMachine meet critical error: {}.", processor, t);
iter.setErrorAndRollback(index - applied,
new Status(RaftError.ESTATEMACHINE, "StateMachine meet critical error: %s.",
ExceptionUtil.getStackTrace(t)));
}
}
Leader 路径(iter.done() != null):当 Iterator 的 done() 方法返回非 null 时,表示当前日志条目是本节点作为 Leader 通过 node.apply(task) 提交的本地任务。iter.done() 返回的是注册 Task 时设置的 NacosClosure 对象,它携带了原始的 Message 对象。此时直接从 closure.getMessage() 获取 WriteRequest 或 ReadRequest,无需反序列化——因为消息本身在 applyOperation 时已经以 Protobuf 格式准备好。
Follower 路径(iter.done() == null):当 iter.done() 返回 null 时,表示当前日志条目是从 Leader 复制到 Follower 的副本。这时 iter.getData() 返回的是日志存储中的原始字节数据,需要通过 ProtoMessageUtil.parse(data.array()) 反序列化为 Message 对象。
2.2 Follower 跳过 ReadRequest 的设计意图
Follower 路径中有一个特别的分支判断:
if (message instanceof ReadRequest) {
applied++; index++; iter.next();
continue;
}
当 Follower 从 Leader 复制的日志条目是一个 ReadRequest 时,Follower 直接跳过处理。原因在于 Raft 的线性一致性读取协议:读取请求在 Leader 端通过 readIndex 确认已提交日志索引后在本地执行,不需要复制到 Follower。Follower 虽然在日志复制过程中收到了 ReadRequest 的副本,但由于它不需要在 Follower 上执行(Follower 也不应该返回数据给客户端),因此直接 continue 是最合理的行为——既避免不必要的处理器调用,也保证 Follower 不会因为 ReadRequest 的执行结果与 Leader 不一致而产生歧义。
注意:Follower 路径上 iter.done() == null 时,源码注释明确指出 'iter.done() == null' means current node is follower, ignore read operation。Follower 遇到 ReadRequest 日志条目时直接 continue 跳过,因为线性一致性读取只需要在 Leader 端通过 ReadIndex 机制确认提交索引后执行,不需要在 Follower 上回放。
2.3 迭代器的批处理与索引追踪
onApply 方法通过 while (iter.hasNext()) 循环批量处理 Iterator 中缓存的多条日志。index 变量追踪当前处理的日志条目总数,applied 变量追踪成功提交的日志条目数。当 catch (Throwable) 捕获到异常后,通过 iter.setErrorAndRollback(index - applied, status) 计算需要回滚的日志条目数(index - applied),将这些未成功提交的条目回滚到上一个已提交的索引。这个"全部成功才前进"的策略保证了状态机的状态在任何时刻都不会处于"部分应用"的不一致状态。
三、NacosClosure 回调与状态机执行
NacosClosure 是连接日志提交与业务回调的桥梁。本节的时序图展示从 applyOperation 到 closure.run(status) 的完整链路:

下面的闭环示意图展示了 NacosClosure 从创建、注册、执行到回调的完整生命周期:

3.1 日志提交的入口:applyOperation
当 JRaftServer.commit() 判断当前节点是 Leader 后,调用 applyOperation() 将请求提交为 Raft 日志:
com.alibaba.nacos.core.distributed.raft.JRaftServer#applyOperation
public void applyOperation(Node node, Message data, FailoverClosure closure) {
final Task task = new Task();
task.setDone(new NacosClosure(data, status -> {
// 直接获取 NacosStatus,将 Throwable 和 Response 同时传递,
// 由 NacosClosure.NacosStatus 的内部状态决定调用方如何消费
NacosClosure.NacosStatus nacosStatus = (NacosClosure.NacosStatus) status;
closure.setThrowable(nacosStatus.getThrowable());
closure.setResponse(nacosStatus.getResponse());
closure.run(nacosStatus);
}));
// 添加请求类型字段前缀:Follower 路径通过 iter.getData()
// 读取日志时,需要前 2 字节来区分 ReadRequest 和 WriteRequest
byte[] requestTypeFieldBytes = new byte[2];
requestTypeFieldBytes[0] = ProtoMessageUtil.REQUEST_TYPE_FIELD_TAG;
if (data instanceof ReadRequest) {
requestTypeFieldBytes[1] = ProtoMessageUtil.REQUEST_TYPE_READ;
} else {
requestTypeFieldBytes[1] = ProtoMessageUtil.REQUEST_TYPE_WRITE;
}
byte[] dataBytes = data.toByteArray();
task.setData((ByteBuffer) ByteBuffer.allocate(requestTypeFieldBytes.length + dataBytes.length)
.put(requestTypeFieldBytes).put(dataBytes).position(0));
node.apply(task);
}
applyOperation 的执行流程分三步。第一步创建 NacosClosure:构造参数为原始 Message 和一个匿名的 Closure,该匿名 Closure 在回调时将 Status 强制转换为 NacosClosure.NacosStatus,然后将 Throwable 和 Response 同时注入 FailoverClosure,由 FailoverClosure 内部根据 NacosStatus.getThrowable() 是否非空决定走正常响应还是异常回调。第二步编码数据:先为 requestTypeFieldBytes 分配 2 字节——第 0 字节为 ProtoMessageUtil.REQUEST_TYPE_FIELD_TAG,第 1 字节标识 ReadRequest 或 WriteRequest——再将 Message 序列化为字节数组追加其后,整体包装为 ByteBuffer 后调用 position(0) 重置读写位置到起点,保证后续 Follower 路径通过 iter.getData() 能从头部完整读取字节数据。第三步提交日志:node.apply(task) 将 Task 提交到 JRaft 的日志写入引擎。node.apply() 是异步方法,将日志写入本地存储后立即返回,不阻塞调用线程。
3.2 NacosClosure 的内部结构
NacosClosure 的构造器和 run() 方法实现了回调的上下衔接:
com.alibaba.nacos.core.distributed.raft.NacosClosure
public class NacosClosure implements Closure {
private Message message;
private Closure closure;
private NacosStatus nacosStatus = new NacosStatus();
public NacosClosure(Message message, Closure closure) {
this.message = message;
this.closure = closure;
}
@Override
public void run(Status status) {
nacosStatus.setStatus(status);
closure.run(nacosStatus);
clear();
}
public void setResponse(Response response) {
this.nacosStatus.setResponse(response);
}
public void setThrowable(Throwable throwable) {
this.nacosStatus.setThrowable(throwable);
}
public Message getMessage() {
return message;
}
}
NacosClosure 的核心设计思想是双层封装。外层 Closure 由 applyOperation 创建,传入原始 Message 和业务回调 Closure。当 onApply 的 finally 块调用 closure.run(status) 时,NacosClosure.run() 执行三个操作:将 Status 注入 NacosStatus、执行业务回调 closure.run(nacosStatus)、clear() 清理引用(帮助 GC 回收)。
NacosStatus 是 NacosClosure 的内部静态类,继承 SOFAJRaft 的 Status,额外扩展了 Response 和 Throwable 字段。onApply 的 finally 块中,closure.run(status) 传入的是外部 Status,而 NacosStatus.setStatus(status) 将其保存后,业务回调通过 nacosStatus.isOk() 判断执行结果,通过 nacosStatus.getResponse() 获取状态机返回的数据。
3.3 postProcessor 与 finally 的执行顺序
onApply 方法中 postProcessor 和 finally 块的协作关系值得注意:
try {
// ... 执行 processor.onApply() ...
Response response = processor.onApply((WriteRequest) message);
postProcessor(response, closure); // 将 Response 注入 NacosClosure
} catch (Throwable e) {
status.setError(RaftError.UNKNOWN, e.toString());
Optional.ofNullable(closure).ifPresent(closure1 -> closure1.setThrowable(e));
throw e;
} finally {
Optional.ofNullable(closure).ifPresent(closure1 -> closure1.run(status)); // 触发回调
}
postProcessor 在正常路径上将 processor.onApply() 的返回注入 NacosClosure 的 NacosStatus。finally 块始终执行 closure.run(status)。两者配合形成了两条完整路径:
正常路径:postProcessor 注入 Response → finally 触发回调 → 业务回调通过 nacosStatus.getResponse() 获取数据。
异常路径:catch 块注入 Throwable → finally 触发回调 → 业务回调通过 nacosStatus.getThrowable() 获取异常。
这种"先注入、后回调"的设计避免了业务回调在状态机执行前被错误触发,也保证了即使状态机执行抛出异常,调用方也能得到回调通知而不会永久阻塞。
3.4 异常回滚机制
当 onApply 的 while 循环中抛出异常时,外层 catch 块执行回滚:
catch (Throwable t) {
Loggers.RAFT.error("processor : {}, stateMachine meet critical error: {}.", processor, t);
iter.setErrorAndRollback(index - applied,
new Status(RaftError.ESTATEMACHINE, "StateMachine meet critical error: %s.",
ExceptionUtil.getStackTrace(t)));
}
setErrorAndRollback 的第一个参数 index - applied 计算当前批次中未成功提交的日志条目数(即从上次成功提交到当前异常位置之间的条目数)。SOFAJRaft 底层收到 setErrorAndRollback 后,会将这些日志标记为"已失败"并跳过,状态机回滚到上一个已提交的日志索引。这种"异常即回滚"的保守策略避免了状态机在部分应用后进入不一致状态——即使只有一个日志条目执行失败,整个批次中剩余的未应用条目也会被回滚。
四、快照机制的三层适配架构
Raft 状态机中的日志会无限增长,需要快照机制来压缩已应用的日志条目。Nacos 的快照机制采用了三层适配架构,从底层的 JRaft 快照存储到顶层的业务快照操作,每一层各司其职。
4.1 快照在 Raft 协议中的作用
快照的核心作用有两个:压缩日志和加速恢复。一方面,已应用的历史日志在快照生成后可以被安全删除,避免日志无限增长占用磁盘空间。另一方面,新节点加入集群或节点崩溃重启时,通过加载最近的快照可以直接恢复到快照时的状态,不需要从第一条日志开始逐条回放。
Nacos 中快照的默认间隔为 1800 秒(30 分钟),通过配置项 nacos.core.protocol.raft.snapshot_interval_secs 调整,常量定义在 RaftSysConstants 中:
com.alibaba.nacos.core.distributed.raft.RaftSysConstants
public static final int DEFAULT_RAFT_SNAPSHOT_INTERVAL_SECS = 30 * 60; // 1800秒 = 30分钟
public static final String RAFT_SNAPSHOT_INTERVAL_SECS = "snapshot_interval_secs";
如果业务模块未实现快照处理器,快照间隔会被设置为 0(即禁用快照),这在 createMultiRaftGroup 中有明确处理:
com.alibaba.nacos.core.distributed.raft.JRaftServer#createMultiRaftGroup
int doSnapshotInterval = ConvertUtils.toInt(raftConfig.getVal(RaftSysConstants.RAFT_SNAPSHOT_INTERVAL_SECS),
RaftSysConstants.DEFAULT_RAFT_SNAPSHOT_INTERVAL_SECS);
doSnapshotInterval = CollectionUtils.isEmpty(processor.loadSnapshotOperate()) ? 0 : doSnapshotInterval;
copy.setSnapshotIntervalSecs(doSnapshotInterval);
4.2 三层适配架构
快照机制的分层设计可以用下面的时序图来展现:

下面的分层示意图展示了从 SOFAJRaft 引擎层到业务层的三层适配关系:

三层适配架构在 NacosStateMachine 构造函数中完成适配:
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#NacosStateMachine
NacosStateMachine(JRaftServer server, RequestProcessor4CP processor) {
this.server = server;
this.processor = processor;
this.groupId = processor.group();
adapterToJRaftSnapshot(processor.loadSnapshotOperate());
}
adapterToJRaftSnapshot 将业务层的 SnapshotOperation 集合适配为内层的 JSnapshotOperation 集合:
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#adapterToJRaftSnapshot
private void adapterToJRaftSnapshot(Collection<SnapshotOperation> userOperates) {
List<JSnapshotOperation> tmp = new ArrayList<>();
for (SnapshotOperation item : userOperates) {
if (item == null) {
Loggers.RAFT.error("Existing SnapshotOperation for null");
continue;
}
tmp.add(new JSnapshotOperation() {
@Override
public void onSnapshotSave(SnapshotWriter writer, Closure done) {
final Writer wCtx = new Writer(writer.getPath());
final BiConsumer<Boolean, Throwable> callFinally = (result, t) -> {
Boolean[] results = new Boolean[wCtx.listFiles().size()];
int[] index = new int[] {0};
wCtx.listFiles().forEach((file, meta) -> {
try {
results[index[0]++] = writer.addFile(file, buildMetadata(meta));
} catch (Exception e) {
throw new ConsistencyException(e);
}
});
final Status status = result
&& Arrays.stream(results).allMatch(Boolean.TRUE::equals) ? Status.OK()
: new Status(RaftError.EIO, "Fail to compress snapshot at %s, error is %s",
writer.getPath(), t == null ? "" : t.getMessage());
done.run(status);
};
item.onSnapshotSave(wCtx, callFinally);
}
@Override
public boolean onSnapshotLoad(SnapshotReader reader) {
final Map<String, LocalFileMeta> metaMap = new HashMap<>(reader.listFiles().size());
for (String fileName : reader.listFiles()) {
// ... 解析 LocalFileMeta ...
metaMap.put(fileName, fileMeta);
}
final Reader rCtx = new Reader(reader.getPath(), metaMap);
return item.onSnapshotLoad(rCtx);
}
@Override
public String info() {
return item.toString();
}
});
}
this.operations = Collections.unmodifiableList(tmp);
}
三层架构的各层职责如下:
底层(SOFAJRaft 引擎):StateMachineAdapter 定义 onSnapshotSave(SnapshotWriter, Closure) 和 onSnapshotLoad(SnapshotReader) 两个方法。SnapshotWriter 和 SnapshotReader 是 JRaft 底层快照存储的抽象,直接操作 Raft 日志目录下的快照文件。addFile(file, meta) 将快照文件注册到 JRaft 的元数据管理中。
适配层(JSnapshotOperation):adapterToJRaftSnapshot 方法将 SnapshotOperation 包装为 JSnapshotOperation。适配层完成两个核心转换:保存时,将 Nacos 抽象 Writer 中产生的文件通过 writer.addFile(file, meta) 注册到 JRaft 的 SnapshotWriter;加载时,将 JRaft 的 SnapshotReader.listFiles() 解析为 Nacos 的 Reader。这种适配解耦了两层的类型依赖——SnapshotOperation 不需要知道 JRaft 的 SnapshotWriter 和 SnapshotReader 的存在。
顶层(SnapshotOperation + 业务实现):各业务模块实现 SnapshotOperation 接口,将各自的数据持久化为文件。PersistentClientOperationServiceImpl 将持久化实例快照为 persistent_instance.zip,DistributedDatabaseOperateImpl 将 Derby 数据库快照为 derby_data.zip,元数据模块将服务和实例元数据分别快照为 service_metadata.zip 和 instance_metadata.zip。
4.3 onSnapshotSave 与 onSnapshotLoad 的回调
NacosStateMachine.onSnapshotSave 和 onSnapshotLoad 的实现非常简洁——遍历 operations 列表并委托给每个 JSnapshotOperation:
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onSnapshotSave
@Override
public void onSnapshotSave(SnapshotWriter writer, Closure done) {
for (JSnapshotOperation operation : operations) {
try {
operation.onSnapshotSave(writer, done);
} catch (Throwable t) {
Loggers.RAFT.error("There was an error saving the snapshot , error : {}, operation : {}", t,
operation.info());
throw t;
}
}
}
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onSnapshotLoad
@Override
public boolean onSnapshotLoad(SnapshotReader reader) {
for (JSnapshotOperation operation : operations) {
try {
if (!operation.onSnapshotLoad(reader)) {
Loggers.RAFT.error("Snapshot load failed on : {}", operation.info());
return false;
}
} catch (Throwable t) {
Loggers.RAFT.error("Snapshot load failed on : {}, has error : {}", operation.info(), t);
return false;
}
}
return true;
}
onSnapshotSave 在遍历中如果某个 operation 抛出异常,会直接抛出让 SOFAJRaft 感知快照失败。onSnapshotLoad 在遍历时如果某个 operation 返回 false 或抛出异常,立即返回 false 终止整个加载——因为部分加载成功会导致状态不一致,不如全部失败让系统走日志回放恢复。
4.4 各业务模块的快照实现
Nacos 中注册了快照操作的业务模块及其快照文件如下:
持久化客户端快照(PersistentClientOperationServiceImpl 的内部类 PersistentInstanceSnapshotOperation):快照文件 persistent_instance.zip。writeSnapshot() 先将 PersistentIpPortClientManager 中所有客户端通过 dumpSnapshot() 序列化为 ConcurrentHashMap<String, ClientSyncData> 的字节流,然后使用 DiskUtils.compressIntoZipFile("instance", inputStream, outputFile, checksum) 压缩为 ZIP 文件。校验码使用 CRC64,写入 LocalFileMeta 的 CHECK_SUM_KEY 中。readSnapshot() 解压 ZIP 后反序列化,通过 loadSnapshot() 对比新旧 clientIds,增删/更新 IpPortBasedClient。
Derby 数据库快照(DerbySnapshotOperation):快照文件 derby_data.zip。保存时通过 Derby 的备份 SQL 命令 SYSCS_BACKUP_DATABASE 将数据库文件拷贝到临时目录,然后压缩为 ZIP。加载时解压 ZIP 并还原 Derby 数据库目录,清理旧的日志文件,发布 DerbyLoadEvent 触发后续的 DumpService 恢复。
元数据快照:ServiceMetadataSnapshotOperation(service_metadata.zip)和 InstanceMetadataSnapshotOperation(instance_metadata.zip),分别持久化服务元数据和实例元数据。
SwitchDomain 快照(SwitchDomainSnapshotOperation):快照文件 naming_persistent.zip,内建 naming_persistent 目录,保存 SwitchDomain 配置。
全文小结
本文聚焦 JRaft 的日志复制与快照管理机制,从 onApply 日志回放、NacosClosure 回调、异常回滚到快照的三层适配架构,完整分析了 JRaft 协议中数据同步与灾备恢复的核心实现。
在日志复制方面,NacosStateMachine.onApply 是日志提交后的统一回放入口。Leader 和 Follower 通过 iter.done() 区分日志来源——Leader 路径直接从 NacosClosure 获取 Message 对象,Follower 路径通过 ProtoMessageUtil.parse() 反序列化原始日志数据且跳过 ReadRequest。这种分流设计保证了同一组日志条目在所有节点以相同顺序执行,为 Raft 协议的强一致性提供了状态机层面的保障。在回调机制方面,NacosClosure 作为 Task.done 在 node.apply(task) 时注册,postProcessor 在正常路径上注入 Response,finally 块确保无论如何都会执行回调——这种"先注入后回调"的设计保证了调用方不会因状态机异常而永久阻塞。在异常容错方面,setErrorAndRollback 在状态机执行异常时回滚从上次成功应用到异常位置之间的所有未提交日志,避免了"部分应用"导致的不一致。在快照管理方面,adapterToJRaftSnapshot 通过三层适配架构将 Nacos 的 SnapshotOperation 桥接到 JRaft 的 onSnapshotSave/onSnapshotLoad 回调——底层由 SOFAJRaft 的 SnapshotWriter/SnapshotReader 管理快照存储,适配层完成类型转换,顶层由各业务模块独立实现快照文件的保存和加载(持久化实例的 persistent_instance.zip、Derby 数据库的 derby_data.zip、元数据的 .zip 文件),使各 Raft Group 能够独立管理自己的快照生命周期。
原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。
更多推荐




所有评论(0)