最近在调研etcd raft来作为一致性协议raft的实现库,因此对所得进行了梳理。
etcd raft介绍
etcd raft是目前使用最广泛的raft库,如果想深入了解raft请直接阅读论文 “In Search of an Understandable Consensus Algorithm”(https://raft.github.io/raft.pdf), etcd raft在etcd, Kubernetes, Docker Swarm, Cloud Foundry Diego, CockroachDB, TiDB, Project Calico, Flannel等分布式系统中都有应用,在生成环境得到了验证。 传统raft库的实现都是单体设计(集成了存储层、消息序列化、网络层等), etcd raft继承了简约的设计理念,只实现了最核心的raft算法, 这样更加的灵活。etcd将网络、日志存储、快照等功能分开,通过独立的模块实现,用户可以在需要时调用。etcd自身实现了自己的一套raft配套库:etcd-wal(用于存储日志),snap(用于存储快照),MemoryStorage(用于存储当前日志、快照、状态等信息以供raft核心程序使用)。
etcd wal介绍
WAL是write ahead log的缩写,etcd使用wal模块来完成raft日志的持久化存储,etcd对wal的所有实现都放在wal目录中。
wal数据结构
type WAL struct {
lg *zap.Logger
dir string // the living directory of the underlay files
// dirFile is a fd for the wal directory for syncing on Rename
dirFile *os.File
metadata []byte // metadata recorded at the head of each WAL
state raftpb.HardState // hardstate recorded at the head of WAL
start walpb.Snapshot // snapshot to start reading
decoder *decoder // decoder to decode records
readClose func() error // closer for decode reader
mu sync.Mutex
enti uint64 // index of the last entry saved to the wal
encoder *encoder // encoder to encode records
locks []*fileutil.LockedFile // the locked files the WAL holds (the name is increasing)
fp *filePipeline
}
上述为wal的数据结构,通过用wal.go文件中的Create()方法来获取wal的实例。wal首先会创建一个临时目录并初始化相关变量,并创建和初始化第一个wal文件,等所有的操作都初始化完成后直接更改临时目录的名字完成wal实例的初始化。
文件组织
wal的所有日志放在一个指定目录下,日志的文件名以 .wal 作为结尾,格式为
每个文件的大小默认为64M,当文件大于64M时,wal会自动生成新的日志文件用于存储日志。每个日志文件都会使用flock锁定文件,参数为LOCK_EX,这是一把独有锁,同一时间只能有一个进程可以操作这个日志文件,所以当wal占有这个文件时,通过进程是无法删除这个文件的。
日志逻辑组织
wal日志可以存储多种类型的数据,具体如下。
类型 | 描述 |
---|---|
metadataType | 文件元信息,每个wal文件都会有一个元信息日志 |
entryType | raft日志 |
stateType | 状态信息 |
crcType | crc类型 |
snapshotType | 快照 |
- crcType 每个新的日志文件的第一条记录都会是crcType类型的记录,crcType也只会在每个日志文件的开始时写入,用于记录上一个文件最后的crc是多少
- metadataType 每个新的日志文件中metadataType紧跟在crcType记录后面,每个日志文件只会出现一次
- stateType
这种日志类型会在两种情况下加入:
- 自动切分日志文件时,新的日志文件中,紧跟在metadataType后面会存入一条stateType的日志
- 当raft核心程序ready中返回hard state时也会存储该类型的日志
- snapshotType wal日志中只会存储snapshot的term和index,具体的数据存储在专门的snapshot中,每次存储快照都会在wal日志中存储一个wal的快照。当存储快照时,会将快照之前index的日志文件都释放掉。wal中存储的snapshot主要用于检查快照是否正确。
日志读写
wal通过封装的encoder和decoder模块来实现日志读写。
写日志
encoder模块把会增量的计算crc和数据一起写入到wal文件中。 下面为encoder数据结构
type encoder struct {
mu sync.Mutex
bw *ioutil.PageWriter
crc hash.Hash32
buf []byte //缓存空间,默认为1M,降低数据分配的压力
uint64buf []byte
}
wal通过encoder实现写日志,在这个模块中会完成crc的计算。wal为了更好的管理数据,日志中的每条数据都会以8字节对齐(wal会自动对齐字节)。 日志写入的流程如下。
图中的crc是增量计算,以之前的所有日志数据为增量基础。 wal只关注写入日志,不会校验日志的index是否重复,但是如果重启这个Node的话,系统会自动过滤掉中间混杂的日志。
日志切分
wal实现了日志自动切分,当日志数据大于默认的64M时就会生成新的文件写入日志,日志的切分通过wal.go文件中的cut方法来实现。cut方法只会在调用wal中的Save方法才会触发调用,新文件的第一条记录就是上一个wal文件最后的crc。
日志compact
wal没有实现日志的自动compact,系统只提供了MemoryStorage的日志compact方法(需要用户主动调用)。
file_pipeline模块
wal新建新的文件时都是先新建一个tmp文件,当所有操作都完成后再重命名这个文件。wal使用file_pipeline这个模块在后台启动一个协程时刻准备一个临时文件以供使用,从而避免临时创建文件的开销。
etcd snap介绍
etcd raft自带了go.etcd.io/etcd/etcdserver/api/snap模块来实现快照的存储。
文件组织
在snap模块中一个快照用一个后缀名为.snap的文件存储,文件格式为
详细介绍
系统可以有多个快照,snap模块使用Snapshotter结构统一管理快照。
type Snapshotter struct {
lg *zap.Logger
dir string
}
上面snapshotter的结构代码,snapshotter主要用于存储和读取快照。
快照具体存储的内容需要用户来指定,例如在raft的官方例子中直接将当时的kv数据Marshal之后存储到快照中。
func (s *kvstore) getSnapshot() ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return json.Marshal(s.kvStore)
}
何时打快照
在etcd-raft中用户可以选择何时打快照,在etcd的官方案例中打快照的方法是maybeTriggerSnapshot(),这个方法在节点的Ready()方法返回时调用,当前提交的index值与上一次大快照的index值大于10000时会打新的快照。
etcd MemoryStorage介绍
MemoryStorage用于存储raft节点临时的数据,包括entrys、快照等。用户将数据存储到memoryStorage中,raft节点也会使用这些数据。包括entrys的传递、快照的发送等都是从memoryStorage中发送。
// MemoryStorage implements the Storage interface backed by an
// in-memory array.
type MemoryStorage struct {
// Protects access to all fields. Most methods of MemoryStorage are
// run on the raft goroutine, but Append() is run on an application
// goroutine.
sync.Mutex
hardState pb.HardState
snapshot pb.Snapshot
// ents[i] has raft log position i+snapshot.Metadata.Index
ents []pb.Entry
}
memoryStorage会存储最新的entrys(包括哪些没有commit)、快照和状态,用户在收到其它节点发送的相关数据时需要将数据存储到memorystorage中。
尾记
转载请注明原地址,冯杰的博客:http://Lfengjie.github.io 谢谢!