LevelDB 之 WAL 实现

介绍 LevelDB 的 WAL 的实现,以及如何实现故障恢复。从流程概览中分离出来。

此外,还介绍了下对文件的封装。

目录:

  1. LevelDB 之 Memtable 实现
  2. LevelDB 之 SSTable 实现
  3. LevelDB 之 Version
  4. LevelDB 之 Compaction
  5. LevelDB 之 WAL
  6. LevelDB 之流程概览

WAL 格式

在介绍故障恢复之前,先介绍 LevelDB 的 WAL 格式。

如下所示,WAL 的写入以 Block 为单位。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum RecordType {
// Zero is reserved for preallocated files
kZeroType = 0,

kFullType = 1,

// For fragments
kFirstType = 2,
kMiddleType = 3,
kLastType = 4
};

static const int kBlockSize = 32768;

AddRecord 会写入一条记录,对应一个 Slice。写入的过程是:

  1. 如果这个 Block 剩余的空间,不能放下一个 header 了,就新建一个 Block。
  2. 否则比较 Slice 的大小,和 Block 的剩余空间
    • 如果能放下,就写到这个 Block 里面,类型为 kFullType。
    • 否则,就要写到多个 Block 里面,根据写入部分在 Slice 中的位置,类型分别是 kFirstType、kLastType、kMiddleType。只有在 Slice 非常大的时候,才会有 kMiddleType。

具体的写入,是通过 EmitPhysicalRecord 来做的。它是调用 PosixWritableFile 的 Append 接口来写入的。PosixWritableFile 这个类实现了 WritableFile 这个接口,具体实现可以看下面的介绍。写入最后,会调用 Flush,也就是 ::write,但是不会调用 Sync 去强制刷盘。这里的刷盘,实际上是实现在外部的,由 options.sync 控制。如果外部不刷盘,那么 WAL 就可能会丢,从而造成数据丢失。

1
2
3
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
...
if (status.ok() && options.sync) {

对文件的封装

WritableFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LEVELDB_EXPORT WritableFile {
public:
WritableFile() = default;

WritableFile(const WritableFile&) = delete;
WritableFile& operator=(const WritableFile&) = delete;

virtual ~WritableFile();

virtual Status Append(const Slice& data) = 0;
virtual Status Close() = 0;
virtual Status Flush() = 0;
virtual Status Sync() = 0;
};

PosixWritableFile

这个类中有个 kWritableFileBufferSize 的 buffer,默认大小是 65536。写入的时候,会先写到这个 buffer 中。如果 buffer 满了,就需要 FlushBuffer 并清空 buffer。FlushBuffer 实际上就是调用 write 方法。这里不需要用 pwrite,因为是顺序写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
Status Flush() override { return FlushBuffer(); }

Status FlushBuffer() {
Status status = WriteUnbuffered(buf_, pos_);
pos_ = 0;
return status;
}

Status WriteUnbuffered(const char* data, size_t size) {
while (size > 0) {
ssize_t write_result = ::write(fd_, data, size);
if (write_result < 0) {
if (errno == EINTR) {
continue; // Retry
}
return PosixError(filename_, errno);
}
data += write_result;
size -= write_result;
}
return Status::OK();
}
...

然后,如果剩余的内容能够填入到新的 buffer 中,就写进去。否则直接调用 WriteUnbuffered 直接写到文件里面。

此外,另有 SyncFd 方法,用来确保数据已经写到持久化介质里面,而不是在 os 的缓存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
static Status SyncFd(int fd, const std::string& fd_path) {
#if HAVE_FULLFSYNC
// On macOS and iOS, fsync() doesn't guarantee durability past power
// failures. fcntl(F_FULLFSYNC) is required for that purpose. Some
// filesystems don't support fcntl(F_FULLFSYNC), and require a fallback to
// fsync().
if (::fcntl(fd, F_FULLFSYNC) == 0) {
return Status::OK();
}
#endif // HAVE_FULLFSYNC

#if HAVE_FDATASYNC
bool sync_success = ::fdatasync(fd) == 0;
#else
bool sync_success = ::fsync(fd) == 0;
#endif // HAVE_FDATASYNC

if (sync_success) {
return Status::OK();
}
return PosixError(fd_path, errno);
}
...

相比 fdatasync,fsync 更重,因为它可能要写两次盘。一次是写数据,另一次是写元数据。而 fdatasync 只同步文件的数据部分,除非元数据的变化是必要的(例如文件大小变化),否则不会同步元数据。

故障恢复

Manifest 损坏/丢失