etcd-raft-存储分析

最近在调研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 作为结尾,格式为-.wal,seq和index的格式都为%016x,例如:0000000000000001-0000000000000001.wal。index代表这个文件中第一条raft日志的index,seq是这个文件的序列号(依次递增)。

每个文件的大小默认为64M,当文件大于64M时,wal会自动生成新的日志文件用于存储日志。每个日志文件都会使用flock锁定文件,参数为LOCK_EX,这是一把独有锁,同一时间只能有一个进程可以操作这个日志文件,所以当wal占有这个文件时,通过进程是无法删除这个文件的。

日志逻辑组织

wal日志可以存储多种类型的数据,具体如下。

类型 描述
metadataType 文件元信息,每个wal文件都会有一个元信息日志
entryType raft日志
stateType 状态信息
crcType crc类型
snapshotType 快照

日志逻辑架构

  • crcType 每个新的日志文件的第一条记录都会是crcType类型的记录,crcType也只会在每个日志文件的开始时写入,用于记录上一个文件最后的crc是多少
  • metadataType 每个新的日志文件中metadataType紧跟在crcType记录后面,每个日志文件只会出现一次
  • stateType 这种日志类型会在两种情况下加入:
    1. 自动切分日志文件时,新的日志文件中,紧跟在metadataType后面会存入一条stateType的日志
    2. 当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会自动对齐字节)。 日志写入的流程如下。

encoder写日志流程

图中的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, term和index分别代表快照日志所处的term和index。 每个快照具体存储结构如下图:

快照逻辑架构

详细介绍

系统可以有多个快照,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 谢谢!

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦