共计 5444 个字符,预计需要花费 14 分钟才能阅读完成。
柯煜昌 参谋软件工程师
目前从事 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]:
Latency Comparison Numbers
--------------------------
L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns 14x L1 cache
Mutex lock/unlock 25 ns
Main memory reference 100 ns 20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy 3,000 ns 3 us
Send 1K bytes over 1 Gbps network 10,000 ns 10 us
Read 4K randomly from SSD* 150,000 ns 150 us ~1GB/sec SSD
Read 1 MB sequentially from memory 250,000 ns 250 us
Round trip within same datacenter 500,000 ns 500 us
Read 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memory
Disk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtrip
Read 1 MB sequentially from disk 20,000,000 ns 20,000 us 20 ms 80x memory, 20X SSD
Send packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms
Notes
-----
1 ns = 10^-9 seconds
1 us = 10^-6 seconds = 1,000 ns
1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns
Credit
------
By Jeff Dean: http://research.google.com/people/jeff/
Originally by Peter Norvig: http://norvig.com/21-days.html#answers
从总结内容可知:内存的访问速度至多是 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 大小的日志块组成。
<center> 图 1. 日志文件构造 </center>
每个日志文件前 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 日志块。
宏 | 偏移 | 长度 | 含意 |
---|---|---|---|
LOG_HEADER_FORMAT | 0 | 4 | 格局 |
LOG_HEADER_PAD1 | 4 | 4 | 补齐长度,预留字段 |
LOG_HEADER_START_LSN | 8 | 8 | 起始 LSN,最后为固定值 4*k. 如果发现有删除 Redo 文件的动作,则可能是零碎表空间第一个页 page LSN 计算。 |
LOG_HEADER_CREATOR | 16 | 32 | 日志文件名称 |
LOG_HEADER_FLAGS | 48 | 4 | 非凡用处 |
其余空间,通常为 0 | 其余空间,通常为 0 | 其余空间,通常为 0 | 其余空间,通常为 0 |
CHECKSUM | 508 | 4 | 日志块的 checksum checksum 用来验证 block 的是否残缺和正确。 |
checkpoint 日志块
日志文件中记录检查点信息的日志块有两个,每个 checkpoint 日志块构造如下:
宏 | 偏移 | 长度 | 含意 |
---|---|---|---|
LOG_CHECKPOINT_NO | 0 | 8 | checkpoint 序号 |
LOG_CHECKPOINT_LSN | 8 | 16 | checkpoint LSN |
LOG_CHECKPOINT_OFFSET | 16 | 8 | checkpoint 的文件偏移 |
其余空间,通常为 0 | 其余空间,通常为 0 | 其余空间,通常为 0 | 其余空间,通常为 0 |
CHECKSUM | 508 | 4 | 日志块的 checksum |
一般日志块
记录日志记录信息的日志块,头 12 个字节与最初 4 个字节记录日志的形容信息,其余空间存储日志记录。日志块构造如下:
偏移 | 长度 | 含意 | |
---|---|---|---|
LOG_BLOCK_HDR_NO | 0 | 4 | 日志块的序号 最高比特位是 flushbit |
LOG_BLOCK_HDR_DATA_LEN | 4 | 2 | 块内日志长度 蕴含头部信息与 checksum,最高位批示是否加密 |
LOG_BLOCK_FIRST_REC_GROUP | 6 | 2 | 第一条全新日志开始地位 |
LOG_BLOCK_CHECKPOINT_NO | 8 | 4 | 本次 checkpoint 的序号 |
其余空间,用以存储日志记录 | 其余空间,用以存储日志记录 | 其余空间,用以存储日志记录 | 其余空间,用以存储日志记录 |
CHECKSUM | 508 | 4 | 日志块的 checksum |
示例
一条日志记录能够跨多个日志块,一个日志块能够蕴含多个日志记录。
<center> 图 2. 几种日志块示例 </center>
* 图中 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 增长时如何切换文件。
<center> 图 3. 日志切换 </center>
很显然,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 地位。因而地位可能在某日志块边缘,也可能在日志块中。
<center> 图 4. Checkpoint LSN 可能在的地位 </center>
通过后面的内容得悉,checkpoint 块保留的信息有 checkpoint LSN 与 LOG_CHECKPOINT_OFFSET。checkpoint offset 是 checkpoint LSN 在日志文件组中的偏移地位。因而 LSN 与 offset 计算公式如下:
size_capacity = log.n_files * (log.file_size - LOG_FILE_HDR_SIZE);
日志的容量是文件个数乘以日志文件无效空间(文件大小减去四个 logblock)。
if (lsn >= log.current_file_lsn) {
delta = lsn - log.current_file_lsn;
delta = delta % size_capacity;
} else {
/* Special case because lsn and offset are unsigned. */
delta = log.current_file_lsn - lsn;
delta = size_capacity - delta % size_capacity;
}
在启动时,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 计算呈现正数的状况,可做如下解决:
size_offset = log_files_size_offset(log, log.current_file_real_offset);
size_offset = (size_offset + delta) % size_capacity;
return (log_files_real_offset(log, size_offset));
这个 log_files_size_offset
是将current_file_real_offset
转换成日志文件无效空间的偏移地位,计算公式为:
current_file_real_offset - LOG_FILE_HDR_SIZE*(1 + current_file_real_offset/log.file_size)
将 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/…