Overview

WAL - write ahead logging 预写式日志是数据库系统提供原子性和持久化的一系列技术。

这里的日志跟我们平常程序的打印日志不同,是序列化后的二进制形式的命令或者指令

我们在 doc.go 中能看到对 etcd wal 的说明

WAL 结构

// 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

创建 WAL

// 代码做了简化

// 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 文件

初始化完成目录的创建和 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