Write-ahead logging(WAL)
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 写入流程
设计的宗旨:
-
由于多个 HBase 客户端可以对某一台 HBase Region Server 发起并发的业务数据写入请求,因此 WAL 也要支持并发的多线程日志写入。————确保日志写入的线程安全、高并发。
-
对于单个 HBase 客户端,它在 WAL 中的日志顺序,应该与这个客户端发起的业务数据写入请求的顺序一致。
-
为了保证高可靠,日志不仅要写入文件系统的内存缓存,而且应该尽快、强制刷到磁盘上(即 WAL 的 Sync 操作)。但是 Sync 太频繁,性能会变差。所以:
(1) Sync 应当在多个后台线程中异步执行
(2) 频繁的多个 Sync,可以合并为一次 Sync ————适当放松对可靠性的要求,提高性能。
在上图的例子中,“Region Server RPC 服务线程 1” 做了 3 个 Row 的 Append 操作,和一个强制刷磁盘的Sync操作。
当客户端启动一个操作来修改数据,每一个修改都封装到 KV 对象实例中,并通过 RPC 调用发送到含有匹配 Region 的 HRegionServer。一旦对象到达,它们就会被发送到管理相应行的 HRegion 实例,将数据写到 WAL。
一个 HRegionServer 的多个 Region 实例共享同一份 HLog,并管理某一段 RowKey 的范围。
3.1.2 WAL调用链分析
client 端先把 put/delete 等 api 操作封装成 List,然后使用 protobuf 协议使用 rpc 服务发送到对应的 HRegionServer,HRegionServer 调用 execRegionServerService 解析发送过来的 protobuf 协议二进制包,通过 serviceName 找到相应的 service 并调用 callMethod 方法执行:
put、delete 等“写”操作会使用 MultiRowMutationService 这个 service 来作用,在 service 中将会调用 mutateRows 去处理 List,真正调用 mutateRows 的是 MultiRowMutationService 的一个实现类 MultiRowMutationEndpoint, MultiRowMutationEndpoint 类实现了 Hbase 的行事务
在 HRegion 类中,调用 processRowsWithLocks 方法, processRowsWithLocks 方法是整个写 WAL 操作最核心的方法:把写 WAL、刷 WAL 流程都在这里流转。
- 获取锁并禁止通过信号中断。
- 通过 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 的问题解答
-
WAL 和 Redis 的 AOF 有什么区别?Redis 为什么不使用 WAL ?
- AOF:(Append only file,Redis 数据库最大程度保证数据持久化的技术)
- 区别就在于: WAL 是写日志,再执行命令, AOF 是先执行命令,再写日志。
-
- 避免了对操作命令的前置检查
- 数据库设计定位不同导致 Redis 没有使用 WAL:Redis 身为内存数据库,应该保持高性能为主,而且 大多数并不会使用内存数据库进行数据的持久化,在服务宕机后,没有的数据仍可以从数据库里面重新加载,因此不需要强制先记录日志然后在执行命令
-
分段日志( Segmented Log )或者最低水位线( Low-Water Mark ) 是什么? 举个例子?
- 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);
}
- WAL 是怎么写进去的?
在 Hbase 中是通过 LMAX Disruptor(一个高性能的线程间通信库)进行写入的。
加入我们
我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。
扫码发现职位&投递简历
官网投递:https://job.toutiao.com/s/FyL7DRg