Write-ahead logging(WAL)

标签: write ahead logging | 发表时间:2022-09-01 18:36 | 作者:字节跳动技术团队
出处:https://juejin.cn/backend

1、什么是 WAL

WAL( Write-ahead logging预写日志)是数据库用于实现事务原子性和持久性的技术。

所有的修改在提交之前,系统将其先可靠的写入到 WAL 管理的 Log 文件中,再通过 Log 文件中记录的日志执行真正的操作。如果遵循这种过程,则无需在每个事务的提交时 等待系统落盘,因为无论何时何种情况,都可以使用日志进行数据库的恢复。

Log 文件通常包括 redo 和 undo 的信息, Repeating history during Redo,在数据库 crash 之后重启时,通过重新执行数据库在 crash 之前的行为,使数据库恢复到它 crash 之前那一刻的状态,然后 undo 掉 crash 时还在执行的事务; Logging changes during Undo,将 undo 事务时对数据库所做的变更记录日志,保证在重复重启时不会重做。

2、WAL 的优点和缺点

优点

  • 使用 WAL 可以显著的降低磁盘的写次数

当事务提交时,仅需要将所关联的日志文件刷入磁盘,而被事务所改变的数据文件无需刷出,日志文件按顺序进行写入,其同步的代价要远低于刷写数据文件。

  • 可实现在线备份以及时间点恢复

通过归档的 WAL 数据,可以回转数据至任何一个可用 WAL 数据的操作时间

  • 操作数据的顺序读写

缺点

  • 性能降低

对于非常大的事务,WAL 的性能将会降低。例如,对于 Hbase 的 WAL 写入操作涉及到 HDFS 的 flush 操作,这个操作需要进行 2 次网络连接(第一次连接 NameNode 确定要写入的 DataNode 队列,第二次连接DataNode发送数据),比较耗时,会严重影响 HBase 的写入性能,因此采用了异步提交的写入模式

当并发较大时,WAL 的性能会明显下降,这是因为对于同一台 RegionServer,所有的 WAL 记录在 flush 之前都放在同一个队列中,队列的并发性能对 WAL 的写性能会有较大影响,虽然 WAL 是一个高性能选项,但是非常大或运行时间非常长的事务会引入额外的开销

3、WAL 的实际使用案例

从常见的分布式 KV 数据库: HBase 出发,对 WAL 日志的实现原理以及源码调用进行不同角度的分析

HBase

解决的问题:

HBase的Write Ahead Log(WAL)提供了一种高并发、持久化的日志保存与回放机制,每一个业务数据的写入操作(PUT / DELETE)执行前,都会记账在 WAL 中,如果出现 HBase 服务器宕机,则可以从 WAL 中回放执行之前没有完成的操作

HBase 使用 WAL 机制所写入的日志文件叫做:HLog 日志

3.1.1 HLog 写入流程

设计的宗旨:

  1. 由于多个 HBase 客户端可以对某一台 HBase Region Server 发起并发的业务数据写入请求,因此 WAL 也要支持并发的多线程日志写入。————确保日志写入的线程安全、高并发。

  2. 对于单个 HBase 客户端,它在 WAL 中的日志顺序,应该与这个客户端发起的业务数据写入请求的顺序一致。

  3. 为了保证高可靠,日志不仅要写入文件系统的内存缓存,而且应该尽快、强制刷到磁盘上(即 WAL 的 Sync 操作)。但是 Sync 太频繁,性能会变差。所以:

    (1) Sync 应当在多个后台线程中异步执行

    (2) 频繁的多个 Sync,可以合并为一次 Sync ————适当放松对可靠性的要求,提高性能。

在上图的例子中,“Region Server RPC 服务线程 1” 做了 3 个 Row 的 Append 操作,和一个强制刷磁盘的Sync操作。

当客户端启动一个操作来修改数据,每一个修改都封装到 KV 对象实例中,并通过 RPC 调用发送到含有匹配 Region 的 HRegionServer。一旦对象到达,它们就会被发送到管理相应行的 HRegion 实例,将数据写到 WAL。

UML 图 (1).jpg

一个 HRegionServer 的多个 Region 实例共享同一份 HLog,并管理某一段 RowKey 的范围。

3.1.2 WAL调用链分析

UML 图 (2).jpg

client 端先把 put/delete 等 api 操作封装成 List,然后使用 protobuf 协议使用 rpc 服务发送到对应的 HRegionServer,HRegionServer 调用 execRegionServerService 解析发送过来的 protobuf 协议二进制包,通过 serviceName 找到相应的 service 并调用 callMethod 方法执行:

put、delete 等“写”操作会使用 MultiRowMutationService 这个 service 来作用,在 service 中将会调用 mutateRows 去处理 List,真正调用 mutateRows 的是 MultiRowMutationService 的一个实现类 MultiRowMutationEndpointMultiRowMutationEndpoint 类实现了 Hbase 的行事务

在 HRegion 类中,调用 processRowsWithLocks 方法, processRowsWithLocks 方法是整个写 WAL 操作最核心的方法:把写 WAL、刷 WAL 流程都在这里流转。

  1. 获取锁并禁止通过信号中断。

  1. 通过 LMAX Disruptor(一种无锁高并发队列)的写入方式写入 Hlog。

核心思想:Sync 操作是为了确保之前的 Append 操作(包括涉及的业务数据)一定可靠地记录到了磁盘上的日志中,然后 HBase 才能做后续相对不可靠的复杂操作,比如写入 MemStore。

3.1.3 利用 WAL 机制的 Hbase 容错处理

WAL 记载了每一个 RegionServer 对应的 HLog,某一个 RegionServer 挂掉了,都会迁移到其它的机器上处理,重新操作,进行恢复,当 RegionServer 意外终止的时候,Master 会通过 Zookeeper 感知到,Master 首先会处理遗留下来的 HLog 文件,根据其中不同 HRegion 的 Log 数据进行拆分,分别放到相应的 Region 目录下,然后再将实效的 Region 重新分配,领取到这些日志的 Region 发现有历史的 HLog 需要处理,因此会 Replay HLog 的数据到 Memstore 之中,然后 flush 数据到 StoreFiles,完成数据的恢复。

3.1.4 HLog 的清理机制

利用 HLog.cleanOldLogs 可以清除旧的日志。首先取得 StoreFile 中的最大的 SequenceNumber,之后检查是否存在一个 HLog 所有的条目的 SequenceNumber 均低于这个值,如果存在,将删除这个 HLog

4、 关于 WAL 的问题解答

  1. WAL Redis AOF 有什么区别?Redis 为什么不使用 WAL

    1. AOF:(Append only file,Redis 数据库最大程度保证数据持久化的技术)
  • 区别就在于: WAL 是写日志,再执行命令, AOF 是先执行命令,再写日志。
    • 避免了对操作命令的前置检查
    • 数据库设计定位不同导致 Redis 没有使用 WAL:Redis 身为内存数据库,应该保持高性能为主,而且 大多数并不会使用内存数据库进行数据的持久化,在服务宕机后,没有的数据仍可以从数据库里面重新加载,因此不需要强制先记录日志然后在执行命令
  1. 分段日志( Segmented Log )或者最低水位线( Low-Water Mark ) 是什么? 举个例子?

    1. Segmented Log:在 WAL 机制的使用过程中,随着系统的运行,事务的操作日志会越来越大,当需要读取时将会变得非常缓慢,Segmented Log 将一个大日志拆分为多个小日志,以方便操作。单个日志分成多个段,到达指定规模的上限之后,日志文件会滚动。
  public Long writeEntry(WALEntry entry) {
    maybeRoll();
    return openSegment.writeEntry(entry);
}
private void maybeRoll() { //日志滚动
    if (openSegment.size() >= config.getMaxLogSize()) {
        openSegment.flush(); //达到限制大小进行日志落盘
        sortedSavedSegments.add(openSegment);
        long lastId = openSegment.getLastLogEntryId();
        openSegment = WALSegment.open(lastId, config.getWalDir());//滚动到新的部分
    }
}

有了日志分段,还要有一种简单的方式将逻辑日志偏移同日志分段文件做一个映射。

  • 每个日志分段名都是生成的,采用 前缀基本偏移的方式
  public static String createFileName(Long startIndex) {
    return logPrefix + "_" + startIndex + logSuffix;
}

读操作有两步,对于给定的偏移,确定日志分段,从后续的日志段中读取所有的日志记录

  public List<WALEntry> readFrom(Long startIndex) {
    List<WALSegment> segments = getAllSegmentsContainingLogGreaterThan(startIndex);
    return readWalEntriesFrom(startIndex, segments);
}
private List<WALSegment> getAllSegmentsContainingLogGreaterThan(Long startIndex) {
    List<WALSegment> segments = new ArrayList<>();
    for (int i = sortedSavedSegments.size() - 1; i >= 0; i--){
        //得到所有起始偏移量小于startIndex的segment文件
        WALSegment walSegment = sortedSavedSegments.get(i);
        segments.add(walSegment);
    }
    if (openSegment.getBaseOffset() <= startIndex) {
        segments.add(openSegment);
    }
    return segments;
}

Low-water Mark:

最低水位线是指在 WAL 预写日志这种设计模式中,标记在这个位置之前的日志可以被丢弃,使用 Segmented Log,虽然可以一次可以处理更小的文件,但如果不检查,磁盘总存储量仍然会无限增长,对于 WAL 的清理机制而言,存储引擎会周期地打快照,通过键值对的方式持久化到磁盘上,然后日志管理器就会得到低水位标记,可以丢弃旧的日志了。

  public SnapShot takeSnapshot() {
    Long snapShotTakenAtLogIndex = wal.getSequenceNumber();
    return new SnapShot(serializeState(kv), snapShotTakenAtLogIndex);
}
  1. WAL 是怎么写进去的?

在 Hbase 中是通过 LMAX Disruptor(一个高性能的线程间通信库)进行写入的。

加入我们

我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。

扫码发现职位&投递简历

官网投递:https://job.toutiao.com/s/FyL7DRg

相关 [write ahead logging] 推荐:

Write-ahead logging(WAL)

- - 掘金 后端
WAL( Write-ahead logging预写日志)是数据库用于实现事务原子性和持久性的技术. 所有的修改在提交之前,系统将其先可靠的写入到 WAL 管理的 Log 文件中,再通过 Log 文件中记录的日志执行真正的操作. 如果遵循这种过程,则无需在每个事务的提交时 等待系统落盘,因为无论何时何种情况,都可以使用日志进行数据库的恢复.

logging的使用分析

- jeff - limodou的学习生活
昨天,Lord Leatherface问我为什么使用了suds后,输出日志变成了两条. 于是我仔细地研究了logging的日志处理机制,并且再次考虑了uliweb的日志处理,最终对uliweb的日志进行了重构,并且弄清楚了为什么会有两条日志的现象,因此以本文作一个记录. 对于logging我想大家用得应该不少,那么我先提几个问题:.

Python日志库logging总结

- - IT瘾-tuicool
在部署项目时,不可能直接将所有的信息都输出到控制台中,我们可以将这些信息记录到日志文件中,这样不仅方便我们查看程序运行时的情况,也可以在项目出现故障时根据运行时产生的日志快速定位问题出现的位置. Python 标准库 logging 用作记录日志,默认分为六种日志级别(括号为级别对应的数值),NOTSET(0)、DEBUG(10)、INFO(20)、WARNING(30)、ERROR(40)、CRITICAL(50).

Redo write触发的四种情况

- - CSDN博客推荐文章
1、当LGWR空闲的时候,会每隔3秒检查一次是否有从redo buffer写入redelog中的数据,如果有,一个后台进程就会自动的执行将其写入. 2、当有进程要从redo buffer中分配空间时,会先计算redo buffer中已经占用的空间,如果该空间大于_log_io_size这个参数值,并且此时的LGWR处于空闲状态,便会被激活执行后台写.

编程每一天(Write Code Every Day)

- - 博客园_知识库
   英文原文: Write Code Every Day.   去年秋天,我的个人项目似乎走到了尽头:我一直没能取得必要的进展,在不牺牲我的主业( 可汗学院的全职工作) 的情况下,我无法完成更多的个人编程事项.   我个人项目上的工作始终存在着几个严重问题. 我把这些工作主要放在周末,但有时也可能是周末的晚上.

55最佳实践系列:Logging最佳实践

- - 博客园_旁观者
@郑昀汇总 创建日期:2012/10. ASAP (As Soon As Possible)原则. 当你意识到靠现有的日志无法定位问题时,. 当现象难以在你的开发环境重现时,. 请不要执著于枯坐肉眼看代码,因为:一)不一定是你代码逻辑问题,可能是脏数据造成的,是老业务数据造成的,是分布式环境造成的,是其他子系统造成的;二)线上业务处于不稳定中,条件不允许问题定位无限期.

Write or die:不能停笔的写作应用

- tossking - 爱范儿 · Beats of Bits
作家 Alisa Bowman 在文章《如何写得快》中,认为如果要写得快,就“不能停笔”:. 一开始就不要停下,除非你脑中在没有词可以涌出. 不要因为拼写错误而停下,不要因为语法而停下,不要因为反复思量而停下. Write or Die(WoD),就是一个让你“不能停笔”的应用. 如果停下笔来,它就会惩罚你,直到你重新提笔写作为止.

how to write extension for firefox -- 如何给firefox编写插件

- - CSDN博客Web前端推荐文章
1.1 创建一个dev profile. 1.3 创建或者修改一些preference. 1.4 启动firefox. 使用-no-remoet可以使你同时用自己平时使用的profile浏览网页,和你开发的profile互补干扰. Note 1:当你修改install.rdf中有关firefox的compatile version时,有时会提示你版本不兼容当前的firefox,不要理会,.

HBase如何合理设置客户端Write Buffer - 大圆那些事 - 博客园

- -
大圆那些事| 转载请以超链接形式标明文章原始出处和作者信息. HBase客户端API提供了Write Buffer的方式,即批量提交一批Put对象到HBase服务端. 本文将结合HBase相关源码,对其进行深入介绍,分析如何在实际项目中合理设置和使用它. 什么时候需要Write Buffer. 默认情况下,一次Put操作即要与Region Server执行一次RPC操作,其执行过程可以被拆分为以下三个部分:.