snapshot作用
snapshot是raft算法的四大特性之Log Compaction。
snapshot的主要作用,对本节点来说:
- Log Compaction,在完成Snapshot完成之后,这个时间之前的日志都可以被删除了,这样可以减少日志占用的资源
- 启动加速,启动阶段变为加载Snapshot和追加之后日志两个阶段,而不需要重新执行历史上所有的操作.
对整个raft-group来说,leader和follower会有InstallSnapshot操作,目前,主要有两个地方会触发InstallSnapshot的发送: - Raft中的Follower落后Leader太多,Follower所需要的Raft Log在Leader中已经删除,会触发Leader向Follower发送snapshot
- 当一个Raft Group增加一个peer时,这个peer通过Leader发送snapshot进行数据的初始化
braft实现
braft将log compaction分成了三大块:
- snapshot save:创建快照
- snapshot load:加载快照
- install snapshot:复制快照
不同业务的Snapshot千差万别,因为SnapshotStorage并没有抽象具体读写Snapshot的接口,而是抽象出SnapshotReader和SnapshotWriter,交由用户扩展具体的snapshot创建和加载逻辑。
Snapshot创建流程:
- SnapshotStorage::create创建一个临时的Snapshot,并返回一个SnapshotWriter
- SnapshotWriter将状态数据写入到临时snapshot中
- SnapshotStorage::close来将这个snapshot转为合法的snapshot
- 通过NodeOptions.snapshot_uri指定快照的存储路径
Snapshot读取流程:
- SnapshotStorage::open打开最近的一个Snapshot,并返回一个SnapshotReader
- SnapshotReader将状态数据从snapshot中恢复出来
- SnapshotStorage::close清理资源
libraft内提供了基于文件列表的LocalSnapshotWriter和LocalSnapshotReader默认实现,具体使用方式为:
- 在fsm的on_snapshot_save回调中,将状态数据写入到本地文件中,然后调用SnapshotWriter::add_file将相应文件加入snapshot meta。
- 在fsm的on_snapshot_load回调中,调用SnapshotReader::list_files获取本地文件列表,按照on_snapshot_save的方式进行解析,恢复状态数据。
braft中一个raft-group中的所有副本做log compaction是相互独立的,通常将NodeOptions.snapshot_interval_s=-1,然后每个副本自己控制snapshot。
主要类说明:
类 | 描述 | 主要流程 |
---|---|---|
SnapshotStorage | 快照读写的抽象接口,用户可以继承该类实现自己的快照读写 | LocalSnapshotStorage,通过create方法创建一个快照,通过copy_from方法开始拷贝快照 |
SnapshotReader | 快照元信息读取的抽象 | LocalSnapshotReader,通过get_path方法来获取该snapshot存放的路径,通过list_files来获取snapshot对应的文件,通过get_file_meta接口获取文件元信息 |
SnapshotWriter | 快照写操作的抽象 | LocalSnapshotWriter,使用add_file接口告知libRaft业务状态机该snapshot对应的文件名及meta信息 |
SnapshotCopier | 快照拷贝的抽象 | LocalSnapshotCopier,负责拷贝快照数据,load_meta_table(元数据拷贝)->filter(过滤规)->copy_file(数据拷贝) |
RemoteFileCopier | 封装rpc请求 | send_next_rpc发起异步请求,copy_to_iobuf拷贝元数据,copy_to_file拷贝数据 |
FileSystemAdaptor | 底层存储接口 | 默认实现是本地文件系统接口PosixFileSystemAdaptor,可以定制,如根据rocksdb实现RocksdbFileSystemAdaptor |
主要流程
braft的状态机提供了on_snapshot_save、on_snapshot_load的回调接口通知用户快照创建和加载,这两个操作是在状态机队列里串行执行的,所以需要高效实现,不然就会阻塞状态机。
snapshot save:创建快照
快照创建由Node::snapshot接口触发,然后相继持久化元数据和数据,braft状态机会持久化包括last_included_index,last_included_term和此snapshot对应的peer configuration的元数据,保存(last_included_term,last_included_index)在加载快照的时候完成后就会从这个log_index开始同步日志;在on_snapshot_save回调中业务完成自己snapshot数据的持久化。
主要函数调用:
- Node::snapshot(Closure* done):定时或手动触发创建快照
- SnapshotExecutor::do_snapshot(Closure* done):
- SnapshotStorage::create():返回SnapshotWriter
- FSMCaller::on_snapshot_save(SaveSnapshotClosure* done)
- FSMCaller::do_snapshot_save(SaveSnapshotClosure* done):保存snapshot元数据
- _fsm::on_snapshot_save(writer, done):执行用户状态机的逻辑
- SnapshotExecutor::on_snapshot_save_done():异步执行
- SnapshotWriter::save_meta()保存快照元数据
- SnapshotStorage::close(writer)关闭资源,更新内存
用户状态机实现on_snapshot_save接口如下:
SnapshotWriter是braft快照写操作的抽象,业务通过SnapshotWriter::add_file接口保存快照的元信息。
执行snapshot save后,braft即可将更老的snapshot删除,并将last_included_index之前的log安全地清除掉,snapshot的元信息存放在指定快照目录的__raft_snapshot_meta文件里。
snapshot load:加载快照
snapshot load发生的场景:
- 节点重启:通过加载snapshot来恢复业务状态机的状态,首先通过on_snapshot_load接口回调业务状态机,完成snapshot的加载,然后在将此snapshot之后的committed log replay给业务状态机,完成状态恢复;
- 日志落后太多:follower日志落后Leader太多时(follower next_log_index < leader first_log_index),leader会发起install_snapshot给Follower加速数据补齐,注意此时在此时在on_snapshot_load回调中需要现清理本地数据。
- 新peer加入:这是上一中场景的特殊情况。
on_snapshot_load接口如下:
SnapshotReader是braft快照读操作的抽象,业务状态机通过get_path方法来获取该snapshot存放的路径,通过list_files来获取snapshot对应的文件,之后即可进行快照文件的读写。
snapshot load完成后,braft从last_included_index之后的log进行回放(replay)。
install snapshot:安装快照
Install Snapshot包括两个阶段,即leader节点上的读以及follower节点上的写。
安装快照时首先Leader向Follower发起RaftService::install_snapshot rpc请求,请求内容如下:
Follower收到请求后开启异步任务,通过FileService::get_file rpc接口来拉取快照数据,
请求GetFileRequest/GetFileResponse内容如下:
拉取完数据后Flollower执行snapshot load加载快照到业务状态机,然后回复Leader InstallSnapshotResponse。完成install snapshot,snapshot install之后,Leader即可从该snapshot的last_included_index之后log开始replication给Follower。
BaikalDB基于RocksDB的Snapshot实现
BaikalDB采用Rocksdb作为单机引擎,raft状态机中应用raft-log直接将数据持写入Rocksdb,可以直接将RocksDB的数据作为snapshot,通过定制RocksdbFileSystemAdaptor实现Snapshot,具体实现如下:
snapshot save:创建快照
在这个阶段不做任何操作,只在SnapshotWriter中add两个文件名,然后结束。BaikalDB创建了meta和data两个ColumnFamily分别保存数据库的元数据和数据。
直接使用RocksDB里的数据作为snapshot,而RocksDB在持续不断的写入数据,因此在snapshot load时需要比较snapshot_index和applied_index,确保事务恢复正确状态。
snapshot load:加载快照
snapshot load发生在场景包括节点重启或者Follower落后日志太多的时候:
节点重启时:
节点重启时重启RocksDB,同时读取raft log_index < std::max(snapshot_index, _applied_index)的 未提交事务的log进行raplay恢复事务状态。
日志落后太多时
日志落后太多时会从Leader拉取一份完整的snapshot,因为是从Leasder拉取的一份完整的数据,此时需要先清除本地所有数据,使用RocksDB的DeleteRange接口,然后将拉取的sst文件通过IngestExternalFile接口加载到Rocksdb。
RocksdbFileSystemAdaptor实现
工具类 | 父类 | 功能 |
---|---|---|
RocksdbFileSystemAdaptor | braft::FileSystemAdaptor | 负责整个快照文件的维护 |
PosixFileAdaptor | braft::FileAdaptor | 负责braft自身快照元数据的读写 |
SstWriterAdaptor | braft::FileAdaptor | 负责follower写sst文件 |
RocksdbReaderAdaptor | braft::FileAdaptor | 负责leader读取rocksdb数据 |
主要接口:
Leader端
leader执行install_snapshot流程:
open_snapshot:该接口用来完成一些snapshot读取前的准备工作,进行并发控制。可能同一个group内有多个Follower需要拉取同一个snapshot,由于braft没有区分读取请求来自哪个Follower,因此需要在open_snapshot接口中对同一个snapshot进行并发限制,同一时刻只允许一个Follower读取该snapshot,检查到已经有其他snapshot install任务打开该snapshot时,返回打开失败。
open:读取时调用RocksdbFileSystemAdaptor::open接口返回RocksdbReaderAdaptor,该类是有状态的FileAdaptor,维护每次读取的进度,并持有RocksDB Iterator直至数据读取全部结束。
read:read操作通过指定offset来决定要从哪里开始读取数据,size参数指定了本次要读取的数据量,通过RocksDB Iterator遍历kv数据分批发送给Follower,直到eof,kv数据以Fixed-Length-Bytes的方式编码,即size+data。
close: close操作清理open操作维护的内存结构,如释放RocksDB Iterator等。
close_snapshot:close_snapshot操作在完成snapshot或出错后调用,在此接口中清除open_snapshot时保存的状态。
Follower端
在Follower端,在收到InstallSnapshotRequest后,调用RocksdbFileSystemAdaptor::open接口返回一个SstWriterAdaptor,利用rocksdb::SstFileWriter生成sst文件。
在SstWriterAdaptor::write接口中,将从Leader读取的数据,根据Fixed-Length-Bytes解析出kv数据,调用SstFileWrtier的Put接口写入sst文件。
最后,在close时,调用SstFileWriter的Finish接口生成完整的sst文件,此sst文件即可在后续snapshot load时ingest到RocksDB中,完成snapshot数据加载。