LotusDB 设计与实现—2 WAL 日志

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: WAL 是 Write Ahead Log 的简称,通常叫做预写日志,是为了预防内存崩溃,保证数据不丢失的常用手段。WAL 是 LSM 存储模型中重要的组件,在 LotusDB 当中的重要性是一样的。
LotusDB 是一个全新的 KV 存储引擎,Github 地址: https://github.com/flower-corp/lotusdb,希望大家多多支持呀,点个 star 或者参与进来!


WAL 是 Write Ahead Log 的简称,通常叫做预写日志,是为了预防内存崩溃,保证数据不丢失的常用手段。WAL 是 LSM 存储模型中重要的组件,在 LotusDB 当中的重要性是一样的。


试想一下,如果没有 WAL,写入的数据直接到内存的话,由于内存是易失性的,崩溃之后数据无法恢复,如果数据写到一半发生了这种情况,会造成数据不一致甚至丢失,在一个系统底层的存储引擎当中,这通常是不可接受的。

日志结构的数据文件一般是追加写的,WAL 也是一样。在 LotusDB 当中写入 k/v 时,会先将数据封装成一条日志项 LogEntry,并将其追加到 WAL,日志项 LogEntry 的结构体定义如下:

// LogEntry is the data will be appended in log file.
type LogEntry struct {
  Key       []byte
  Value     []byte
  ExpiredAt int64 // time.Unix
  Type      EntryType
}


写入前需要将 LogEntry 结构体进行编码,然后再追加到文件中,编码后主要包含 key、value 信息,还有对应的长度 size 信息,以及对整条数据有效性做校验的 crc 值,格式如下:

// EncodeEntry will encode entry into a byte slice.
// The encoded Entry looks like:
// +-------+--------+----------+------------+-----------+-------+---------+
// |  crc  |  type  | key size | value size | expiresAt |  key  |  value  |
// +-------+--------+----------+------------+-----------+-------+---------+
// |------------------------HEADER----------------------|
//         |--------------------------crc check---------------------------|


编码后的数据写到 WAL 时,LotusDB 提供了两种 IO 模式:系统标准 IO 和 mmap,可在打开数据库时通过配置项进行选择,LogFile 结构体定义的 IoSelector 负责实现:

// LogFile is an abstraction of a disk file, entry`s read and write will go through it.
type LogFile struct {
  sync.RWMutex
  Fid        uint32
  WriteAt    int64
  IoSelector ioselector.IOSelector
}


LogFile 是操作文件数据读写的结构体,其中最重要的是 IoSelector,它是一个 interface,负责具体的读写操作,具体的实现有 FileIO 和 MMap。

// IOSelector io selector for fileio and mmap, used by wal and value log right now.
type IOSelector interface {
  // Write a slice to log file at offset.
  // It returns the number of bytes written and an error, if any.
  Write(b []byte, offset int64) (int, error)
  // Read a slice from offset.
  // It returns the number of bytes read and any error encountered.
  Read(b []byte, offset int64) (int, error)
  // Sync commits the current contents of the file to stable storage.
  // Typically, this means flushing the file system's in-memory copy
  // of recently written data to disk.
  Sync() error
  // Close closes the File, rendering it unusable for I/O.
  // It will return an error if it has already been closed.
  Close() error
  // Delete delete the file.
  // Must close it before delete, and will unmap if in MMapSelector.
  Delete() error
}


LotusDB 中的一个 WAL 和 一个 memtable 绑定,从 memtable 的结构体定义就能够体现出来:

type memtable struct {
    sync.RWMutex
    sklIter      *arenaskl.Iterator
    skl          *arenaskl.Skiplist
    wal          *logfile.LogFile
    bytesWritten uint32 // number of bytes written, used for flush wal file.
    opts         memOptions
}


WAL 文件的大小和跟之相绑定的 memtable 的容量相关,由于 memtable 通常会有一个阈值,写满之后就关闭了,WAL 此时也不会接受新的写入,因此 WAL 文件的容量通常不会无限膨胀。 memtable 中的数据被后台线程 flush 到磁盘之后,并且没有其他的错误发生,WAL 就可以被安全的删除了。

在 LotusDB 启动打开 memtable 的时候,会全量加载 WAL 中的数据,逻辑很简单,就是打开 WAL 文件,然后遍历其中的每条数据,将其应用到 memtable 的跳表数据结构当中,通过这样的方式,达到了 WAL 恢复数据的作用,参考代码如下:

for {
    if entry, size, err := wal.ReadLogEntry(offset); err == nil {
      offset += size
      // No need to use atomic updates.
      // This function is only be executed in one goroutine at startup.
      wal.WriteAt += size
      // build memtable...
            // ...
    } else {
      if err == io.EOF || err == logfile.ErrEndOfEntry {
        break
      }
      return nil, err
    }
  }


WAL 刷盘策略

数据写到 WAL 文件中,实际上并没有完全落到磁盘,由于操作系统的实现,可能只是到了 page cache,需要我们手动调用 Flush 才能够真正将数据持久化。

针对 Flush 的策略,LotusDB 提供了两个配置项: 一是在 WriteOptions 中的 Sync,如果在数据写入时传递了这个 Options 并且将 Sync 设置为 true,那么写 WAL 完成后会立即 Flush,这种策略能够保证数据不丢,但性能是最差的。 二是 ColumnFamilyOptions 中的 WalBytesFlush,表示写入累积到配置的字节后进行 Flush,可以理解为每 WalBytesFlush 个字节 Flush 一次。这种情况下,如果系统发生异常,最多丢失 WalBytesFlush 个字节的数据。 如果都不设置,则完全交给操作系统,这是默认的策略,性能是最好的,但是如果系统崩溃,丢失的数据可能是最多的。

LotusDB 也提供了手动进行刷盘的方法,用户可以在适当的时候,调用 Sync 方法进行数据刷盘持久化。


其他和 WAL 相关的配置项:ColumnFamilyOptions: WalMMap 是否使用 mmap 进行写入,默认:false WriteOptions: DisableWal 是否禁用 WAL,默认:false




相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
7月前
|
存储 Oracle 关系型数据库
postgresql数据库|wal日志的开启以及如何管理
postgresql数据库|wal日志的开启以及如何管理
1294 0
|
20天前
|
存储 关系型数据库 数据库
【赵渝强老师】PostgreSQL的WAL预写日志文件
PostgreSQL数据库的物理存储结构包含多种文件,其中WAL(预写日志)用于确保数据完整性和高效恢复。WAL机制允许在不频繁刷新数据至磁盘的情况下,通过先写日志再改数据的方式,减少I/O操作,提高性能。每个WAL文件默认大小为16MB,位于pg_wal目录下,支持手动和自动切换。WAL不仅有助于数据恢复,还能显著降低I/O成本。
|
2月前
|
存储 监控 固态存储
如何监控和优化 WAL 日志文件的存储空间使用?
如何监控和优化 WAL 日志文件的存储空间使用?
|
4月前
|
存储 缓存 分布式计算
详解HBase中的“WAL”(Write-Ahead Log)
【8月更文挑战第31天】
256 0
|
4月前
|
Kubernetes 关系型数据库 API
实时计算 Flink版产品使用问题之连接的PG表长时间无数据写入,WAL日志持续增长,该如何解决
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
7月前
|
SQL 关系型数据库 数据库
实时计算 Flink版产品使用合集之同步PostgreSQL数据时,WAL 日志无限增长,是什么导致的
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
监控 关系型数据库 Shell
[翻译]PG15新特性-加速WAL日志归档
[翻译]PG15新特性-加速WAL日志归档
168 0
|
关系型数据库 PostgreSQL
postgresql 的WAL日志解析工具 pg_waldump
postgresql 的WAL日志解析工具 pg_waldump
1545 0
postgresql 的WAL日志解析工具 pg_waldump
|
弹性计算 关系型数据库 测试技术
为什么高并发小事务, unlogged table不比logged table快多少? - commit wal log
标签 PostgreSQL , unlogged table , logged table , wal writer 背景 unlogged table,这些表的写操作不记录WAL日志。那么这种表的高并发写入一定比logged table快,快很多吗? 实际上一个事务,在事务结束时,也会记录一笔commit或rollback xlog,所以如果是高并发的小事务,commit xlog的
861 0
|
1月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
330 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板