InnoDB-redo日志

105 阅读8分钟
  1. 为什么需要redo log?
  2. redo log的日志文件格式
  3. 如何提高redo log的写入性能和效率
  4. 为什么redo log要与binglog保持一致性同步?MySQL如何保证一致性同步?
  5. 什么是checkpoints机制,为什么需要checkpoints?解决什么问题?
  6. MySQL宕机恢复的流程是怎么样的,redo log在其中发挥了什么作用?

TODO:这个地方贴一个思维导图。

1. 为什么需要redo log?

MySQL为了保证数据的持久性引入了redo log。MySQL将数据存储在磁盘中,而将数据写入磁盘是一个性能很差的操作(相比于CPU和内存直接进行交互),所以MySQL引入了Buffer Pool来调节CPU和磁盘交互的矛盾。

即CPU先将数据写到内存中的Buffer Pool中,然后Buffer Pool再定期将数据刷到磁盘上。

随之带来的问题,如果写到Buffer Pool中后,数据还没有刷到磁盘上系统宕机了,这部分数据就丢失了,如何处理?


一个简单的做法是在事务提交完成之前,把该事务修改的所有页面都刷新到磁盘上,但是这个策略性能较差存在两个问题:

  • Inoodb是以页为单位和磁盘进行I/O的,一次事务提交时有可能仅仅修改了某个页面的一个字节,却要把整个页都刷新到磁盘上,浪费资源。
  • 一个事务有可能修改许多页面,并且这些页面在物理上并不连续,刷新会造成随机I/O。

所以MySQL引入了redo log,也叫重做日志(MySQL读写过程中的操作流水日志),在MySQL数据读写的过程中,除了将数据写到BufferPool中,还要将修改的记录写到redo log中,这样即使中途宕机了,仍然可以通过redo log中找到写入的数据是什么。

并且,redo log中只记录事务对数据页做了哪些修改(比如 将第0号表空间第100号页面中偏移量为1000处更新为2),所以占用的空间非常小,并且是顺序I/O。

2. redo log的日志文件格式

redo log的日志文件格式可以从三个层面来理解,即逻辑层 + 物理层 + 文件层。

逻辑层

REDO日志可以理解为由多个不同Type的多个REDO记录首尾相连而成,有全局唯一递增的偏移量sn。设计者根据事务对数据库不同的修改场景,定义了多种类型的redo日志,用type字段来区分。

而一条条的REDO记录其实是以组的形式写入的,因为一条语句在执行的过程中会修改多个页面,可能会产生多条REDO日志(比如索引页分裂)。所以这多条REDO日志的写入必须保证原子性,所以这多条REDO日志就被分成了一个组,也叫Mini-Transaction(MTR)。

在每组的最后一条redo日志后面加上一条特殊类型的redo日志,标志该组结束。

一个MTR包含一组redo日志,在崩溃恢复时,需要把这一组redo日志作为一个不可分割的整体来处理,要么全部恢复,要么一条也不恢复。

物理层

由于redo日志也是要写磁盘的,所以在物理层面有一个redo log buffer的缓存。由于磁盘是块设备,所以在log buffer层的redo日志也是按照块来组织的,即log buffer的空间被划分成若干个连续的redo log block,mtr的一组日志是顺序写到每一个块中的,块的大小是固定的,除了存放mtr外还会有一个固定长度的header和tailer用来存储一些元信息,所以一个mtr有可能跨越好几个block。

在物理层面,用LSN(long sequecnce number)来记录当前总共写入的日志量。每一组mtr生成的redo日志都有一个唯一的lsn与其对应;lsn越小,说明redo日志产生得越早。

那么log buffer中的数据什么时候被刷盘呢?

  • log buffer空间不足时,默认是日志量已经占满总容量50%,就要把这些日志刷新到磁盘
  • 事务提交时,虽然事务提交时可以不把修改过的Buffer Pool页面立即刷新到磁盘,但是为了保证持久性,必须要把页面修改时所对应的redo日志刷新到磁盘。
  • 后台有一个线程,大约以每秒一次的频率将log buffer中的redo日志刷新到磁盘
  • 正常关闭服务器时
  • 做checkpoint时

文件层

那么在真正磁盘层,redo log是如何组织呢?默认情况下是通过文件组的形式组织的,默认有2个文件,ib_logfile0,ib_logfile1...,写入的时候是顺序循环写的,即写到最后一个文件后会从第一个文件重新开始写。而每个log file内部也是按照块去组织的,将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入到日志文件中。

3. 如何提高redo log的写入性能和效率?

REDO必须要事务提交前保证落盘(这是是指fsync前还是后呢?),否则一旦断电会存在数据丢失的可能。包括REDO内容的产生,REDO写入log buffer,从log buffer写入操作系统page cache,以及REDO刷盘,之后还要唤醒等待的用户线程完成Commit。

  • REDO的产生是通过MTR来保证原子性的,首先会把这个原子操作需要写的所有REDO日志先写到一个动态分配的内存空间m_log中,当原子操作结束后,会将m_log中的数据拷贝到log buffer。

  • 写入log buffer在高并发环境中会有非常多的mtr要拷贝,从性能角度考虑这里没有用锁互斥来解决。从MYSQL8.0开始,设计了一套无锁的写log机制,其核心思路是允许不同的mtr,同时并发写Log Buffer的不同位置。不同mtr会首先调用log_buffer_reserve函数,这个函数里会用自己的REDO长度,原子地对全局变量量lsn做fetch_add,得到自己在log buffer中的独享的空间,从而并发写入。

  • 写入操作系统的pagecache,InnoDB种有单独的log_write来做这件事情。由于log buffer种的数据是不同mtr并发写入的,这个过程中log buffer是有空洞的,因此log_write需要感知当前log buffer中连续日志的末尾,将连续的日志通过操作系统pwrite系统调用写入操作系统的page cache。具体的策略:xx。

  • 唤醒用户线程。这个就是通过innodb_flush_log_at_trx_commit中来控制的,1就是REDO刷盘才会唤醒用户线程,完成事务提交;2的话就是写入PageCache就认为了完成写入。

4. 什么是checkpoints?为什么需要checkpoints?

为什么需要checkpoints:

  • 磁盘空间是有限的,redo日志不可能无限的增大;
  • 宕机恢复的时候要重放redo日志,此时需要告诉MySQL从哪里开始重放redo日志,否则从头开始重放会增大MySQL宕机恢复的时间。

由于redo日志只是为了在系统崩溃后恢复脏页用的,如果BufferPool中的脏页已经刷新到磁盘中了,那么即使系统崩溃,这部分redo日志也不需要了。所以Innodb引入了log_checkpointer线程周期性的打checkpoints。策略是:

  • 我们都知道某数据页被修改后,其对应的页会加到BufferPool中的脏页中去,那我们恢复的起点就是当前BufferPool的脏页链表中,拥有最小的那个lsn作为恢复的起点,当然还存在一种可能就是该数据页加入到BufferPool中了但是还没有加入到脏页链表中,所以checkpoint要取两者的最小值。而这些信息最终会写到日志文件组的第一个block中。

5. redo宕机恢复流程

在宕机恢复时,首先要确定:

  • 重放redo日志的起点,这个起点就是打checkpoints时写到第一个block中的信息,直接从这里面读取即可。
  • 重放redo日志的终点。在文件层中redolog也是以block形式存储的,而block头中就存储了当前block被占用的空间大小,所以只需要从起点往后扫,直到某个block没被占满为止。

通过hash表来加速宕机恢复,将对同一个页面进行修改的redo日志都放在一个槽中,这样可以一次性将一个页修复好,从而避免很多的随机IO。

实际InnoDB的宕机恢复是需要redolog,binlog以及undolog共同参与的,ref:sq.sf.163.com/blog/articl…

参考:

  1. catkang.github.io/2020/02/27/…
  2. juejin.cn/post/686025…
  3. www.51cto.com/article/680…