• 分布式系统模式 - Write-Ahead Log
  • 发布于 1个月前
  • 58 热度
    0 评论
在分布式系统中,节点之间复制数据对于确保用户服务连续性至关重要。正如 CAP 定理所总结的,根据在故障情况下数据一致性是否关键,或者是否优先考虑可用性,需要做出设计选择。考虑 CP 的情况,有一种称为状态机复制的技术可以实现容错性,同时也能保证强一致性。在状态机复制中,诸如键值存储之类的存储服务在多台服务器上进行复制,并且用户输入按照相同的顺序在每台服务器上执行。其中的关键实现技术是在多台服务器上复制预写式日志(Write-Ahead Log),以形成复制日志(Replicated Log)。

即使存储数据的服务器出现故障,我们也需要提供强大的持久性保证。一旦服务器同意了执行某个操作,那么即便其发生故障并重启从而丢失所有内存中的状态,也应该能够完成该操作,方式之一就是依赖 WAL(Write-Ahead Log)

在一些地方也被这种技术也被称为Journaling。

将每次状态变更作为命令存储在硬盘上的文件中, 如上图所示, anyway, 你把每一条状态变更叫做命令、实体、记录、条目、日志都可以,注意这里的日志不是那个日志的意思哈,意思大家都懂。

每个服务器进程都维护一个单独的日志,并按顺序追加记录。这种单一的日志通过顺序追加的方式,简化了在系统重启时及新命令被追加时对日志的处理。每个日志条目都会被赋予一个唯一的标识符(比如某种顺序的 id、条目离文件开头的偏移量),这个标识符有助于实施对日志的某些其他操作,比如查找、分段日志、使用低水位标记清理日志等。

日志更新可以通过单一更新队列的方式(后续再讲)来实现。确保记录到日志文件中的内容确实能被实际保存到磁盘上至关重要。各类编程语言所提供的文件处理库均提供某种方法,或者系统系统调用,可迫使操作系统立即将文件修改内容同步到底层物理存储设备上。但是,使用这种刷新(Flush)操作时,需要注意存在一个需要权衡的因素。

每收到一条状态变更都生成一条日志条目写入到磁盘,可以保证所有的状态变更不丢失,这听起来不错,但是鱼与熊掌不可兼得,在状态变更很频繁的情况下,每次刷新数据到磁盘会是一个瓶颈,因为它是一个耗时的操作,所以经常我们权衡之后做一个取舍,将批量日志条目一次刷新到磁盘,相信你能理解这个苦衷。

既然涉及到存储,那么就有可能持久化的数据被损坏的情况,已经每年我都会遇到一两次的比特跳变的情况,每一个比特位从 0 被改到 1,或者从 1 改到 0,这个时候我们可能需要加校验位,使用 CRC 等技术对数据进行校验,甚至纠正。如果总往一个日志文件中追加日志,一段时间后日志文件会变得很大,这个时候就需要分段日志和低水位标记等技术了。

因为网络失败和重试等原因,日志中可能包含重复的条目。我们需要幂等操作去处理。通过复制日志的方式,将日志写入到分布式节点,可以避免某个节点 crash 无法恢复。

etcd 是一个很好的学习分布式技术的开源项目。在 etcd 的实现中,关键部分分布式键值存储系统的持久化就使用了预写式日志(Write-Ahead Log,简称 WAL)。etcd 使用 WAL 来保证即使在节点突然崩溃或者意外终止的情况下,也能安全可靠地恢复数据状态。当 etcd 接收到对键值对的修改请求时,首先将这些改动操作记录到 WAL 中,然后再应用到内存中的数据库状态。这样,即便在数据还未完全同步到磁盘数据库之前发生故障,重启后也能通过回放 WAL 中的日志记录来恢复服务的状态一致性。同时,etcd 还利用 WAL 实现了数据的复制与集群间的同步,进一步增强了系统的容错性和可靠性。

感兴趣的同学可以查看 etcd 的代码实现:
// WAL 是稳定存储的逻辑表现形式。
// WAL 处于读取模式或追加模式,但不能同时处于两种模式。
// 新创建的 WAL 处于追加模式,准备好接收记录的追加。
// 刚刚打开的 WAL 处于读取模式,准备好读取已存在的记录。
// 在读取完所有先前的记录之后,WAL 将准备再次追加记录。
type WAL struct {
    lg *zap.Logger // zap 日志器,用于内部日志记录

    dir string // 存储底层文件的实际目录路径

    // dirFile 是指向 WAL 目录的一个文件描述符,用于在重命名时同步目录
    dirFile *os.File

    metadata []byte           // 每个 WAL 文件头部记录的元数据信息
    state    raftpb.HardState // 在 WAL 文件头部记录的硬状态信息
    start     walpb.Snapshot // 用于开始读取 WAL 的快照信息
    decoder   Decoder        // 用于解码 WAL 记录的解码器
    readClose func() error   // 关闭解码器读取器时调用的关闭函数
    unsafeNoSync bool // 如果设置为 true,则不进行 fsync 操作(即不确保数据立即同步至磁盘)
    mu      sync.Mutex // 互斥锁,用于保护并发访问
    enti    uint64     // 已保存至 WAL 的最后一条记录的索引
    encoder *encoder   // 用于编码记录的编码器

    locks []*fileutil.LockedFile // WAL 持有的已锁定文件列表(文件名按递增顺序排列)
    fp    *filePipeline         // 文件管道,用于管理多个 WAL 文件
}
30 个分布式系统模式,我会在后面的文章中徐徐道来。
这些模式由 Unmesh Joshi 整理,并且由大师 Martin Fowler 监制:https://rpcx.io/r/4c97
用户评论