WAL - write ahead logging 预写式日志是数据库系统提供原子性和持久化的一系列技术。
这里的日志跟我们平常程序的打印日志不同,是序列化后的二进制形式的命令或者指令
我们在 doc.go
中能看到对 etcd wal 的说明
w.SaveSnapshot
来记录以便于重启时能从 snapshot 中恢复$seq-$index.wal
第一个 WAL 文件必须时 0000000000000000-0000000000000000.wal
,wal 文件在超过 64 MB 后(entry 会先被写入,然后再判断是否超过 64MB,所以 wal 文件是会超过 64MB 的),会被进行分片。
// WAL is a logical representation of the stable storage.
// WAL is either in read mode or append mode but not both.
// A newly created WAL is in append mode, and ready for appending records.
// A just opened WAL is in read mode, and ready for reading records.
// The WAL will be ready for appending after reading out all the previous records.
type WAL struct {
...
}
上面的这个 struct 就是 wal 包对外提供的接口中最重要的表现部分了,后续都是通过对这个 struct 来对 WAL 日志进行写入,暂时不需要理解 struct 中各个字段的含义
从注释可以知道,WAL 文件只能处于 read 或者 append 模式二者的其中之一,WAL 要先将已有的 records 全部读完之后才能进行 append
// 代码做了简化
// Create creates a WAL ready for appending records. The given metadata is
// recorded at the head of each WAL file, and can be retrieved with ReadAll.
func Create(lg *zap.Logger, dirpath string, metadata []byte) (*WAL, error) {
// 首先不允许在已经存在的 dirpath 上创建 WAL
if Exist(dirpath) {
return nil, os.ErrExist
}
// 创建 tmp 目录,再这个目录下进行初始化之后,通过 rename 达到原子操作的效果
tmpdirpath := filepath.Clean(dirpath) + ".tmp"
...
// 创建第一个 wal 文件 0000000000000000-0000000000000000.wal
p := filepath.Join(tmpdirpath, walName(0, 0))
// 对 wal 文件上锁,转化为 LockFile
f, err := fileutil.LockFile(p, os.O_WRONLY|os.O_CREATE, fileutil.PrivateFileMode)
// 定位到文件末尾
_, err = f.Seek(0, io.SeekEnd)
// 预分配文件大小,默认为 64 MB
fileutil.Preallocate(f.File, SegmentSizeBytes, true)
// 创建 WAL 对象
w := &WAL{
lg: lg,
dir: dirpath,
metadata: metadata,
}
// 创建 encoder,后续都是通过 encoder 写入 file
w.encoder, err = newFileEncoder(f.File, 0)
w.locks = append(w.locks, f)
// 分别写入三条数据,crc metadata snapshot
err = w.saveCrc(0)
err = w.encoder.encode(&walpb.Record{Type: metadataType, Data: metadata})
err = w.SaveSnapshot(walpb.Snapshot{})
// - rename 将 data-dir/member/wal.tmp 目录改成了 data-dir/member/wal
// - 创建 filePipeline,这个 filePipeline 会默默地在后台创建一个 temp 文件,预先分配空间,
// 然后等待 filePipeline.Open() 调用之后,它会再次创建一个新的 temp 文件
// - 当发生 cut 之后,新文件就是从这个 filePipeline 里面获取的
w, err = w.renameWAL(tmpdirpath)
// directory was renamed; sync parent dir to persist rename
pdir, perr := fileutil.OpenDir(filepath.Dir(w.dir)
perr = fileutil.Fsync(pdir)
}
初始化完成后 0000000000000000-0000000000000000.wal
的文件结构如下
+--------------------+
| +----------------+ |
| | type: crc | |
| +----------------+ |
| | crc: 0 | |
| +----------------+ |
| | data: | |
| +----------------+ |
+--------------------+
| +----------------+ |
| | type: metadata | |
| +----------------+ |
| | crc: 0 | | +-----------+
| +----------------+ | | NodeID |
| | data: ----------------> +-----------+
| +----------------+ | | ClusterID |
+--------------------+ +-----------+
| +----------------+ |
| | type: snapshot | |
| +----------------+ |
+-------+ | | crc: 0 | |
| Index | | +----------------+ |
+-------+ <-------+ data: | |
| Term | | +----------------+ |
+-------+ +--------------------+
初始化完成目录的创建和 WAL 结构体的准备之后,等待数据写入,总共有下面 5 中写入方式,对外提供的是上面两种
func (w *WAL) Save(st raftpb.HardState, ents []raftpb.Entry) error
func (w *WAL) SaveSnapshot(e walpb.Snapshot) error
func (w *WAL) saveCrc(prevCrc uint32) error
func (w *WAL) saveEntry(e *raftpb.Entry) error
func (w *WAL) saveState(s *raftpb.HardState) error