源码 | 解析 Redo Log 实现方式
![源码 | 解析 Redo Log 实现方式](https://static001.geekbang.org/infoq/87/87fd6b41e181b5031d4442c3c56546b8.png)
柯煜昌 顾问软件工程师
目前从事 RadonDB 容器化研发,华中科技大学研究生毕业,有多年的数据库内核开发经验。
| 前言
提及 Redo Log(重做日志)与 LSN(log sequece number)时,经常被问及以下问题:
MySQL 的 InnoDB 为什么要有 Redo Log?
LSN 是什么?
LSN 与 Redo Log 之间有什么相互关系?
Redo Log 如何轮换?
……
基于 MySQL 8.0 的源码,以及对 InnoDB 机制一些内部探讨与分享,写了几篇关于 Redo Log 的文章。本篇先讲一下 Redo Log 的日志结构。
什么是页?
讲 Redo Log 之前,先来了解一下 Jeff Dean 对计算机系统中各种存储系统访问时间的总结[1]:
从总结内容可知:内存的访问速度至少是 SSD 的 4 倍、磁盘顺序访问的 80 倍! 磁盘、SSD 顺序读写明显要快于随机读写,而且磁盘、SSD 对频繁的小写均不友好。因此主流的数据库采用一次读写一个块,并且使用 buffer/cache 技术尽量减少读写次数。InnoDB 称这种读写块为页。
写放大怎么办?
对于一次事务来说,写一行数据,对应页中一个记录。但是要实现事务的持久化,不光是要往磁盘中写数据页,还要写 Undo 页。这就是出现了修改一行,需要持久化多个页到磁盘中,因此性能的损失会比较大,这也就是通常所说的写放大问题。因此人们提出了先写日志 WAL(write ahead log) 的方式进行优化,即将 页 中修改的操作,转换为重做日志(Redo Log)。
在事务提交时,不需要保证修改的页持久化到磁盘中,只需保证日志已经持久化存储到磁盘中即可。如果出现掉电或者故障的场景,内存的页虽然丢失,但是可以通过磁盘的页进行 Redo 重做,恢复更改的内存页。
在绝大部分情况下,Redo Log 数据比数据页和 Undo 页要小,而且按顺序写入,性能也比写放大后的好。由此可以看出,数据库使用 Redo 对数据的操作,速度上接近内存,持久性接近磁盘。
| Redo Log 的实现方式
设计思路
InnoDB 的 Redo Log 是一组文件的集合,默认是两个。每个日志文件又由一组 512 Byte 大小的日志块组成。
![](https://static001.geekbang.org/infoq/f1/f11179fe722e4744fdb97bae35a4fd4e.webp?x-oss-process=image/resize,p_80/format,jpg)
图 1. 日志文件结构
每个日志文件前 4 个日志块保留。其中第一个日志文件里的前 4 块保存着 Redo 日志的元数据信息。日志文件大小在初始化就已经确定,日志块逻辑上组成一个环,循环使用。
细心的读者会发现,日志文件前 4 个保留日志块,有 2 个 checkpoint 块,不免会有如下两个疑问:
1. 为什么会有两个 checkpoint 块?
checkpoint 是崩溃恢复过程中应用日志的起点。如果 checkpoint 块写入如果出现故障或者掉电,InnoDB 就无法找到日志的起点。如果两个 checkpoint 轮换写入,遇到写入 checkpoint 块失败,可以在另一个 checkpoint 块上取得上次的 checkpoint LSN 作为起点。
2. 会不会两个 checkpoint 块都写坏?
假设 checkpoint1 掉电损坏,则选择 checkpoint2 块选取前一个 checkpoint LSN 做恢复。按照 InnoDB 的轮换算法,第二次写入 checkpoint 点的位置仍然是 checkpoint1,再次写入掉电仍然只会在 checkpoint1 损坏,两个 checkpoint 块方法仍然是可靠的。
Header 日志块
Header 日志块是描述日志总体信息的块,虽然只有第一个日志文件有内容,但是 InnoDB 每个日志文件都有 Header 日志块。
![](https://static001.geekbang.org/infoq/da/da45d8e147161dbe53777713756a614e.png)
checkpoint 日志块
日志文件中记录检查点信息的日志块有两个,每个 checkpoint 日志块结构如下:
![](https://static001.geekbang.org/infoq/25/258dbac0f4be2b250e69951705a4c63c.png)
普通日志块
记录日志记录信息的日志块,头 12 个字节与最后 4 个字节记录日志的描述信息,其他空间存储日志记录。日志块结构如下:
![](https://static001.geekbang.org/infoq/89/89723fc60b381bb642500cf7e67436e5.png)
示例
一条日志记录可以跨多个日志块,一个日志块可以包含多个日志记录。
![](https://static001.geekbang.org/infoq/67/67a9e7297cd76c684f4d50d93b22461c.webp?x-oss-process=image/resize,p_80/format,jpg)
图 2. 几种日志块示例
*图中 block tailer 表示 checksum。
示例结构说明
上图中,三个日志块的 LOG_BLOCK_HDR_DATA_LEN 值都为 512;
log block1 的 LOG_BLOCK_FIRST_REC_GROUP 值为 12;
log block2 无全新日志,则值为 0;
log block3 值为 12+ 红色部分的长度;
日志块的块号依据 LSN 位置换算。
1. checkpoint 的序号是怎么计算的?
假设当前 checkpoint 的序号为 4,InnoDB 推进检查点时候,写入到 checkpoint 块的 checkpoint 序号为 4,推进检查点之后,当前系统的 checkpoint 序号就加 1 变成 5。新写的日志块的 check point 需要都是 5。
2. 为什么会有 flushbit?
通常情况下,log block 的序号最高位都是 1,为 0 的情况。log buffer 中日志块还未写完,而 log buffer 已经满。此时 log buffer 的日志块都写入到磁盘中,但是最后一个日志块肯定是不完整的。此时 flush bit 为 0,表示该日志块是不完整的。将来 InnoDB 会清空 log buffer,重新将该日志块写完整。
| Redo Log 的切换写入
假设 LSN 起点为 1,每个日志文件长度为 5,下图展示了 LSN 增长时如何切换文件。
![](https://static001.geekbang.org/infoq/0b/0bfc4aea708b98994cab15d122e762ad.webp?x-oss-process=image/resize,p_80/format,jpg)
图 3. 日志切换
很显然,LSN 1~5 在第一个文件,6~10 在第二个文件,LSN 11 在第一个文件 LSN 为 1 所在位置。Redo Log 应该写在哪个文件,是可以依据 LSN 计算出来的。
那么,Redo Log 是如何将顺序写入的结构实现为一个逻辑的环呢?
| 从 LSN 到 Offset
日志在逻辑上是一个环。checkpoint LSN 表示,LSN 之前的修改的 page 已经成功持久化到磁盘中,相关的 Redo Log 的使命已经结束。作为崩溃恢复的起点,它一定是在某个 MTR 的 END LSN 位置。因此位置可能在某日志块边缘,也可能在日志块中。
![](https://static001.geekbang.org/infoq/1b/1b014b47e056565714ecc3ba881451dc.webp?x-oss-process=image/resize,p_80/format,jpg)
图 4. Checkpoint LSN 可能在的位置
通过前面的内容得知,checkpoint 块保存的信息有 checkpoint LSN 与 LOG_CHECKPOINT_OFFSET。checkpoint offset 是 checkpoint LSN 在日志文件组中的偏移位置。因此 LSN 与 offset 计算公式如下:
日志的容量是文件个数乘以日志文件有效空间(文件大小减去四个 logblock)。
在启动时,current_file_lsn
通常是 checkpoint LSN, current_file_real_offset
通常是 checkpoint offset。LSN 比 checkpoint LSN 大,所以delta = lsn - log.current_file_lsn
表示 LSN 与 checkpoint LSN 的距离。这个距离可能会超过 size_capacity ,因此使用了取余操作。如果 LSN 比 checkpoint LSN 小呢?这说明 LSN 在 checkpoint LSN 前面。checkpoint LSN 是起点,也是终点。checkpoint LSN + size_capacity 的位置,也是 checkpoint LSN 所在的位置。所以delta = size_capacity - delta % size_capacity;
与 - delta % size_capacity
是等效的,为避免 offset 计算出现负数的情况,可做如下处理:
这个log_files_size_offset
是将current_file_real_offset
转换成日志文件有效空间的偏移位置,计算公式为:
将 curren_file_real_offset 减掉文件头的 4 个 logblock 大小,无跨文件就减一次,跨几个文件就多减几次。再加上偏移值,转换成 file_real_offset
就得到了真实的位置。
| 总结
本文介绍了 Redo Log 与各个日志块的基本结构,并通过示例说明了 Redo Log 的两个 checkpoint 作用以及 LSN 如何与日志位置对应。
Redo Log 是一个非常重要的组成部分,LSN 通常作为数据库中数据变更的逻辑时钟,与 Redo Log 密切不可分,弄清 Redo Log 的作用与机制,就能轻松理解 LSN、数据库持久化这些概念。
参考
[1]. https://d-k-ivanov.github.io/docs/CheatSheets/Latency_Numbers/
关于 RadonDB
RadonDB 开源社区 是一个面向云原生、容器化的数据库开源社区。为数据库技术爱好者提供围绕主流开源数据库(MySQL、PostgreSQL、Redis、MongoDB、ClickHouse 等)的技术分享平台,并提供企业级 RadonDB 开源产品及服务。
目前 RadonDB 开源数据库系列产品已被 光大银行、浦发硅谷银行、哈密银行、泰康保险、太平保险、安盛保险、阳光保险、百年人寿、安吉物流、安畅物流、蓝月亮、天财商龙、罗克佳华、升哲科技、无锡汇跑体育、北京电信、江苏交通控股、四川航空、昆明航空、国控生物 等上千家企业及社区用户采用。
RadonDB 可基于云平台与 Kubernetes 容器平台交付,不仅提供覆盖多场景的数据库产品解决方案,而且提供专业的集群管理和自动化运维能力,主要功能特性包括:高可用主从切换、数据强一致性、读写分离、一键安装部署、多维指标监控 &告警、弹性扩容 &缩容、横向自由扩展、自动备份 &恢复、同城多活、异地灾备 等。RadonDB 仅需企业及社区用户专注于业务层逻辑开发,无需关注集群高可用选型、管理和运维等复杂问题,帮助企业及社区用户大幅度提升业务开发与价值创新的效率!
GitHub:https://github.com/radondb
微信群: 请搜索添加群助手微信号 radondb
版权声明: 本文为 InfoQ 作者【RadonDB开源社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/279a3b9066e26301a6c78bba8】。文章转载请联系作者。
评论