TOC
共识中的Wal需要解决什么问题?
- 解决共识中途宕机,造成的节点状态丢失。
- 解决共识中途宕机,造成的尚未落盘或者尚未同步到其他节点的Log Entry数据丢失
- 解决共识节点重启后,节点数据重放
共识中的Wal需要哪些功能?
- 初始化: Wal初始化,预分配资源
- 单条写: 单条写入Wal,支持同步或者异步刷盘
- 批量写: 批量写入Wal后,一次性刷盘
- 读日志: 根据Index,读取Wal中存放的数据内容
- 删日志: 跟据Index,可以往前删除Wal文件,释放磁盘空间
- 手动刷盘: 当开启的是异步刷盘模式的时候,可以手动执行提前刷盘
设计思路
- 做减法
- 共识的wal并不需要像数据库的wal那样,对读性能也有很高要求
- 需要恢复的数据量,不会多到需要snapshot的方式提速
- 减少一切资源消耗,减少过度使用锁
- 统一对外
- 统一对外开放接口,统一选项和参数
- 只针对数据wal存储,不做数据内部细分和定义
- 自动化
- 自动wal文件清理
架构设计
Segment 是什么?
每一个Segment表示一个数据片段。里面包含多条Log Entry等数据和信息。每一个存在过得Segment都对应一个Wal文件。Segment同时只会存在一个。
Segment有什么作用?
数据写入时候,每一个Entry都Append到Segment中。
当Segment大小已经超过阈值时,sync落盘。同时ID自增,清空Segment数据并创建新的对应的Wal文件。
Entry是什么?
Entry是最小wal数据单元。对于Wal来说,不关心Entry内部的数据结构,由用户自己定义。
Segment如何保证Entry数据的一致性?
每一次EntryAppend的时候,都会先做crc32计算,并一起Append到Segment中。这样,读取的时候,只需要检查crc32是否一致就可以保证数据一致。
示意图
结构体
代码语言:txt复制//Segment 日志段,包含多条Log Entry
type Segment struct {
id uint64 // Segment的ID
currentSize uint64 // 当前文件大小
path string // Segment的对应文件的路径
index uint64 // Segment中最小数据块的index
position []pos // data中的索引
data []byte // 保存Entry数据,格式:checksum|entry_size|entry
}
//索引位置信息
type pos struct {
start int //开始索引位置
end int //结束索引位置
}
Wal文件
每一个ID的Segment对应一个Wal文件,每当Segment写满后,自动销毁并创建新的Segment,同时新建一个wal文件对应。
Wal命名
{SegmentID}-{Index}.wal
- SegmentID: 为当前对应的Segment的ID
- Index: 为当前Segment中最先被写入的Entry对应的Index
fmt.Sprintf("6x-6x.wal", SegmentID, Index);
示意图
结构体
代码语言:txt复制//Wal日志
type Wal struct {
mutex sync.RWMutex
path string //日志路径
opts Options //选项
segment Segment //当前日志对应的Segment
firstIndex uint64 //当前日志中的第一条Entry的Index
lastIndex uint64 //当前日志中的最后一条Entry的Index
MarshalFunc MarshalFunc //Marshal函数支持自定义
}
//定义序列化方法
type MarshalFunc func(data interface{}) []byte
Options配置
代码语言:txt复制//配置选项
type Options struct {
Async bool //是否开启异步模式,默认同步
SegmentSize int //每一个Segment大小,默认64m
CustomMarshalFunc bool //自定义MarshalFunc,默认false,使用Default
AutoPurgeFile bool //自动清理wal文件
SyncPurgeFile bool //是否同步清理文件,默认异步:false
}
var DefaultOption = &Options{
Async: false, //默认同步
SegmentSize: 64 * 1024 * 1024, //默认64M
CustomMarshalFunc: false, //使用默认的MarshalFunc
AutoPurgeFile: false, //默认关闭自动清理wal文件
SyncPurgeFile: false, //默认异步清理文件
}
Create创建Wal
- 首先创建目录
- 初始化Index,创建wal初始文件00000000-000000000.wal
- 预分配文件空间,默认64m
Write 写Wal
- 首先marshal序列化数据,计算长度和crc32值
- 将计算长度 Segment中currentSize,判断是否超过64M,未超过直接写入。超过64M则创建新的Wal文件并写入
- 写入Segment,并记录对应的位置,将索引信息记录到Segment中的position中
Read 读Wal
- 根据index,判断是否在Segment中(大于Segment中的index)
- 如果不在Segment中,从磁盘中Wal文件恢复对应的Segment
- 如果在Segment中,直接从当前Segment中读取
- 找到Segment之后,根据index从position中获取到对应的索引pos,快速定位数据位置。
- 再从Segment的Data中,读取数据。计算并校验crc32值
- unmarshal后,返回数据
Sync 手动同步Wal
- 在异步模式中,当前Segment不会立刻刷盘
- 当前Segment只有达到阈值,才会触发刷盘
- 可以调用Sync()方法,提前刷盘
PurgeFile清理Wal文件
- 根据Index,确定对应的wal文件
- 支持手动清理定位的wal文件以及之前的所有wal文件
- 如果Option中配置了
AutoPurgeFile:true
也支持自动清理
ReplayWal 回放WAL
回放,适用于首次启动共识,从Wal中回放共识的重要数据。
通常情况下,首次启动,当前Segment中数没有数据的。因此,需要将最后一个落盘的Wal文件,恢复到Segment中。
- 恢复最新的wal文件到Segment中,从data数据,计算恢复position索引
- 从恢复后的Segment中,拿出最后一条Entry
- 检查crc32值,unmarshal后返回