一款Wal的设计方案

2022-07-14 11:52:41 浏览数 (2)

TOC

共识中的Wal需要解决什么问题?

  1. 解决共识中途宕机,造成的节点状态丢失。
  2. 解决共识中途宕机,造成的尚未落盘或者尚未同步到其他节点的Log Entry数据丢失
  3. 解决共识节点重启后,节点数据重放

共识中的Wal需要哪些功能?

  • 初始化: Wal初始化,预分配资源
  • 单条写: 单条写入Wal,支持同步或者异步刷盘
  • 批量写: 批量写入Wal后,一次性刷盘
  • 读日志: 根据Index,读取Wal中存放的数据内容
  • 删日志: 跟据Index,可以往前删除Wal文件,释放磁盘空间
  • 手动刷盘: 当开启的是异步刷盘模式的时候,可以手动执行提前刷盘

设计思路

  1. 做减法
    • 共识的wal并不需要像数据库的wal那样,对读性能也有很高要求
    • 需要恢复的数据量,不会多到需要snapshot的方式提速
    • 减少一切资源消耗,减少过度使用锁
  2. 统一对外
    • 统一对外开放接口,统一选项和参数
    • 只针对数据wal存储,不做数据内部细分和定义
  3. 自动化
    • 自动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是否一致就可以保证数据一致。

示意图

imageimage

结构体

代码语言: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
代码语言:txt复制
fmt.Sprintf("6x-6x.wal", SegmentID, Index);

示意图

imageimage

结构体

代码语言: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

  1. 首先创建目录
  2. 初始化Index,创建wal初始文件00000000-000000000.wal
  3. 预分配文件空间,默认64m

Write 写Wal

  1. 首先marshal序列化数据,计算长度和crc32值
  2. 将计算长度 Segment中currentSize,判断是否超过64M,未超过直接写入。超过64M则创建新的Wal文件并写入
  3. 写入Segment,并记录对应的位置,将索引信息记录到Segment中的position中

Read 读Wal

  1. 根据index,判断是否在Segment中(大于Segment中的index)
    • 如果不在Segment中,从磁盘中Wal文件恢复对应的Segment
    • 如果在Segment中,直接从当前Segment中读取
  2. 找到Segment之后,根据index从position中获取到对应的索引pos,快速定位数据位置。
  3. 再从Segment的Data中,读取数据。计算并校验crc32值
  4. unmarshal后,返回数据

Sync 手动同步Wal

  • 在异步模式中,当前Segment不会立刻刷盘
  • 当前Segment只有达到阈值,才会触发刷盘
  • 可以调用Sync()方法,提前刷盘

PurgeFile清理Wal文件

  • 根据Index,确定对应的wal文件
  • 支持手动清理定位的wal文件以及之前的所有wal文件
  • 如果Option中配置了AutoPurgeFile:true 也支持自动清理

ReplayWal 回放WAL

回放,适用于首次启动共识,从Wal中回放共识的重要数据。

通常情况下,首次启动,当前Segment中数没有数据的。因此,需要将最后一个落盘的Wal文件,恢复到Segment中。

  1. 恢复最新的wal文件到Segment中,从data数据,计算恢复position索引
  2. 从恢复后的Segment中,拿出最后一条Entry
  3. 检查crc32值,unmarshal后返回

0 人点赞