浅析 InnoDB Redo Log
导语:之前的文章(见本文末)已经介绍了 InnoDB 的部分内外存数据结构,我也在学习过程中对 InnoDB 的一些数据对象有了基本认识,后面的文章会从数据流出发来学习 InnoDB。这篇文章主要学习 InnoDB Redo Log 的流程。Redo Log 是 InnoDB 实现数据一致性和持久化存储的关键,本文主要从设计原理和部分源码实现出发,对其中的知识点进行归纳总结。
Redo Log Buffer & Redo Log File
前面两篇文章已经描述过,InnoDB 使用 Redo Log 来保证数据的一致性和可持久性,它采用 WAL 机制,即先写日志再写数据。具体来说,InnoDB 进行写操作时,先将数据操作记录在 log buffer 中,然后将 log buffer 中的数据刷到磁盘 log file 中,后续数据再落到数据 ibd 文件这一步骤由 checkpoint 来保证。其数据流如下图所示。
另外有一个细节值得注意:虽然对于一个事务来讲,还需要再成功写入 binlog 文件,才能认为事务完成,但单从存储层的视角来看,在某个时刻,数据文件加上 Redo Log File 就是此时刻数据库的一个完整快照,像 Xtrabackup 这类的物理备份工具,就是通过拷贝数据文件和 Redo Log File 来进行全量备份的。
Redo Log 元数据及其初始化流程
InnoDB 使用 log_sys 这个对象来管理 Redo Log Buffer,其结构体为 log_t,它在源码中的定义(部分数据被折叠)如下。
log_t 结构体定义 (/innobase/include/log0log.h)
log_sys 主要包括以下元数据信息:
参数名称 | 描述 |
lsn | 日志序列号 |
buf_free | 当前 log buffer 空闲空间的起始位置 |
buf | log buffer 起始位置 |
······ | ······ |
write_lsn | 最新写入的 lsn |
flushed_to_disk_lsn | 已经刷到 redo log file 中的 lsn |
last_checkpoint_lsn | 最近执行检查点的 lsn |
······ | ······ |
在 log_sys 元数据信息中有一个非常重要的概念 — LSN。
LSN 即日志序列号( Log Sequence Number ),它代表 Redo Log 的序号,它是单调递增的,每写入一个 Redo Log 时,LSN 就会递增该 Redo Log 写入的字节数,因此,LSN 就像是时间点一样,记录了每个 Redo Log 产生的时序,并且和 Redo Log 一一对应,我们后面会具体描述 LSN 和 Redo Log 之间是如何转换的。
log_sys 对象是在 InnoDB 启动时,由 log_init() 函数负责初始化,主要是对元数据信息进行赋值操作。其部分代码实现如下:
log_init()函数实现 (/innobase/log/log0log.cc)
log_init() 函数另一个重要的步骤是对 log 对象 log_block 进行初始化。log_block 是 Redo Log 的最小数据管理单元,其大小为 512 Bytes。log_block 并没有单独的结构体来进行管理,其元数据信息存在于自身的前 12 个字节中,这部分信息称为 log_block_header,另外,log_block 还包括 8 个字节的 log_block_tail 信息,因此,每个 log_block 实际可存储空间为 492 Bytes。log_sys 和 log_block 的结构信息如下图。
正如上图所标注的信息,log_block_header 包含如下信息:
参数名称 | 描述 |
log_block_hdr_no | log_block 的编号 |
log_block_data_len | log_block 所占用的大小 |
log_block_first_rec_group | log_block 中第一个日志的偏移量 |
log_block_checkpoint_no | 该 log_block 最后被写入时的 checkpoint 值 |
log_block_init() 函数进行的操作如下:
log_block_init()函数实现 (/innobase/include/log0log.ic)
首先,通过该 log_block 的 lsn 值计算出 log_block 的编号,即 log_block_hdr_no,前面已经提到过,log_block 没有其他的数据结构进行组织,因此该编号唯一标识了 log_block 在 log buffer 中的位置。lsn 到 log_block_hdr_no 的转换由函数 log_block_convert_lsn_to_no() 实现,实现逻辑为:
UNIV_INLINE
ulint
log_block_convert_lsn_to_no(
/*========================*/
lsn_t lsn) /*!< in: lsn of a byte within the block */
{
return(((ulint) (lsn / OS_FILE_LOG_BLOCK_SIZE) & 0x3FFFFFFFUL) + 1);
}
这个逻辑还是比较简单,因为日志都是按 log_block_size(512 Bytes) 存储的,并且 lsn 单调增加,因此 lsn 其实是 log_block_size 的倍数再加上当前偏移量。
计算出log_block编号后,log_block_set_hdr_no() 函数将该编号记录在 log_block 的前 4 个字节中。然后 log_block_set_data_len() 函数设置该 log_block 默认已使用的空间 LOG_BLOCK_HDR_SIZE,即 12 Bytes。
在实际记录 log 的过程中,一次写 log 的大小可能大于 512 Bytes,即连续占用多个 log_block,所以 log_block 中可能包含多个 log 的内容,因此用 log_block_first_rec_group 记录了 log_block 中第一个日志的偏移量。该变量的值也会被设置成 LOG_BLOCK_HDR_SIZE 的大小。
上面讲到的 log_init() 的整个流程是在 InnoDB 存储引擎的初始化过程中实现的。学习一个复杂系统,从它的初始化流程入手会比较有帮助,就像学习一个文件系统,从 xxxfs_init() 入手,你可以很快的了解 super_block 是如何初始化的,inodes 是如何组织的,学习 InnoDB 也是同样的道理。InnoDB 的初始化流程是由 innobase_start_or_create_for_mysql() 这个函数来实现的,它负责初始化 InnoDB 所需要的各种元数据信息,如 Buffer Pool 结构、LRU 链表、各种文件数据结构、日志数据结构、锁信息、设置 InnoDB 的各种参数变量等。除了我们讲到的 log_init() 流程,还包括 fil_init(), buf_pool_init(), fsp_init(), buf_flush_page_cleaner_init() 等。例如 buf_pool_init() 流程所做的工作初始化 Buffer Pool Instance 对象的结构体 buf_pool_t,包括对 buffer_pool_chunks 的初始化,多个 LRU 链表和指针变量的初始化等。
CheckPoint & LSN
前面已经提到,Buffer Pool 中的数据落盘操作,是由 checkpoint 来执行的。checkpoint 负责将内存数据同步到磁盘数据文件中,以确保数据的一致性。checkpoint 执行的情况决定了数据库在进行灾难恢复时所需要的时间,在数据库进行灾难恢复时,只需要恢复从 checkpoint 到最近的 Redo Log 这段日志中的数据,就可以恢复到宕机之前的状态。LSN 记录了 Redo Log 产生的时序,日志写到 log buffer 以及 log buffer 数据落盘到 log file,都是根据 LSN 来往前推进的。而 checkpoint 也是根据 LSN 的时序来进行刷盘操作的。在 MySQL 中执行 show engine innodb status 可以看到:
由于系统写入量较少,当前系统产生的 lsn 和已经刷到 Redo Log File 的 lsn 值是一致的。而 Buffer Pool 中数据页刷盘的进度和执行 checkpoint 的进度都较为落后。
Group Commit
InnoDB 允许多个事务产生的 Redo Log 一起提交,来减少磁盘 I/O。首先我们再来阐述一下 Redo Log 和 Binlog 的区别。Redo Log 是 InnoDB 存储层特有的日志,记录的是物理页数据的变更,而 Binlog 是 MySQL 都有的,记录的逻辑日志。Binlog 只有事务提交时进行一次写入,而 Redo Log 的写入时机则有多种。首先,在事务提交时,若设置了参数 innodb_flush_log_at_trx_commit 为 1,则会进行刷盘操作;其次,当 Redo Log Buffer 空间不足时,系统也会强制进行刷盘操作;另外后台线程也会每秒进行一次刷盘操作。
如果是在多并发事务的情况下,某个事务在事务过程中产生的 log (已写入到 log buffer 中),可能会被其他事务的提交带到 log file 中,也可能被后台线程进行刷盘,这样就达到了 Group Commit 的效果。同时也可以看到,一个事务产生的 log 并不是在事务提交时一次写到 log file 的,而是在事务过程中不断写入到 log file 的。
Reference
MySQL 源码版本 — 5.7.25
《MySQL 内核 InnoDB 存储引擎》
作者:陈俊熹,腾讯云数据库研发工程师,主要负责腾讯云MySQL数据库研发工作。
文章转自公众号:腾讯云数据库