likes
comments
collection
share

InnoDB之redo log写入和恢复

作者站长头像
站长
· 阅读数 22

1. 前言

InnoDB使用Buffer Pool来加速数据读写,提升性能的同时也带来了一些问题,为了避免页面频繁刷盘和磁盘随机写,InnoDB引入了WAL机制,先顺序写少量的redo log,再由后台线程去异步刷脏页,尽可能提升SQL的执行效率。 写redo log的好处是:相较于一个完整的页,redo log占用的空间极小,而且它是顺序写的,比随机写的效率更高。 根据不同的修改场景,InnoDB设计了几十种不同类型的redo log。这里面包含极其简单的物理日志,只是单纯记录了要把哪个页面的哪些数据做修改;还包含复杂的逻辑日志,它只保留了必要数据,需要调用一些提前准备好的系统函数才能恢复页面。 redo log是以组的形式来写入的,一组redo log是不可分割的,要么不恢复,要么全部恢复。InnoDB把这种对底层页面的一次原子访问称作一个Mini Transaction,缩写MTR,一个MTR就对应一组redo log。

2. redo log block

生成的redo log怎么存储呢? 为了更好的管理,InnoDB设计一个叫redo log block的结构,它和页很像。一个redo log block占用固定的512字节,结构如下:

属性长度说明
log block header12字节block头信息
log block body496字节存放redo log
log block trailer4字节存放block checkSum

中间496字节的body部分用来存储实际的redo log,重点关注header部分,结构如下:

属性长度说明
LOG_BLOCK_HDR_NO4字节block唯一编号
LOG_BLOCK_HDR_DATA_LEN2字节block已经使用了多少字节,512代表全部写满
LOG_BLOCK_FIRST_REC_GROUP2字节MTR第一条日志的起始地址
LOG_BLOCK_CHECKPOINT_NO4字节checkpoint序号
  • LOG_BLOCK_HDR_NO:每个redo log block都会分配一个唯一的编号。
  • LOG_BLOCK_HDR_DATA_LEN:代表该block已经使用了多少字节,从12开始,因为header部分是被固定占用的,512代表block被写满了。
  • LOG_BLOCK_FIRST_REC_GROUP:一个MTR会生成多条redo log,有时甚至会跨越多个block,该属性代表block内第一个MTR第一条redo log的起始地址。
  • LOG_BLOCK_CHECKPOINT_NO:记录checkpoint序号。

3. redo log buffer

磁盘IO一直是数据库的性能杀手,事务执行期间会生成大量的redo log,如果每次都写磁盘,效率是很差的。MySQL会在启动时向操作系统申请一块连续的内存空间,然后将其划分成若干个redo log block,这就是redo log buffer。 MySQL通过启动项innodb_log_buffer_size来控制redo log buffer的大小,默认是16MB。 redo log buffer也是顺序写入的,从前往后写,InnoDB提供了一个全局变量buf_free,buf_free前面的是已经使用的空间,后面的是空闲空间,通过buf_free就知道每次该往哪个block的哪个位置写了。

redo log是以组的方式写入的,它们首先会被暂存起来,等MTR结束时再统一复制到redo log buffer里。

redo log buffer空间很有限,一直往里写也不是个事儿,InnoDB会在下述场景将redo log buffer刷新到磁盘:

  • redo log buffer空间被占用超过50%时。
  • 事务提交的时候,必须刷盘,否则数据丢失。
  • 后台线程以每秒一次的频率刷盘。
  • 正常关闭MySQL时。
  • 做checkpoint操作时。

4. redo log file

redo log buffer空间有限,部分场景下InnoDB会将log buffer里的redo log刷新到磁盘,对应的磁盘文件就是redo log file。 redo log file由一组文件组成,默认情况下会有ib_logfile0ib_logfile1两个日志文件,可通过启动项innodb_log_group_home_dir指定日志文件的路径,innodb_log_file_size指定单个日志文件的大小,innodb_log_files_in_group指定日志文件的个数。 redo log file是循环写的,从下标为0的文件开始写,写满了自动切换到下一个,都写满了又会从下标为0的文件开始写。

循环写会存在“追尾”的问题,InnoDB引入了checkpoint操作,必须确保redo log可以被覆盖。怎么判断呢?只要redo log对应的脏页已经刷新到磁盘,那么redo log就没用了,可以被覆盖。

redo log file的格式很简单,就是log buffer里redo log block的镜像,唯一的区别是redo log file使用前4个block来存储log file的头信息和checkpoint相关的信息。也就是说,redo log file是从第2048个字节开始写入redo log block的。 第一个block存储log file头信息,格式如下:

属性长度说明
LOG_HEADER_FORMAT4字节redo log版本
LOG_HEADER_PAD14字节填充字节
LOG_HEADER_START_LSN8字节2048字节处对应的lsn值
LOG_HEADER_CREATOR32字节log file创建者
LOG_BLOCK_CHECKSUM4字节校验和

第三个block目前没使用,第二和四个block分别存储checkpoint1和checkpoint2,格式是一样的:

属性长度说明
LOG_CHECKPOINT_NO8字节checkpoint编号
LOG_CHECKPOINT_LSN8字节最后一次checkpoint对应的lsn值
LOG_CHECKPOINT_OFFSET8字节最后一次checkpoint对应的lsn值在文件里的偏移量
LOG_CHECKPOINT_LOG_BUF_SIZE8字节执行checkpoint时log buffer的大小
LOG_BLOCK_CHECKSUM4字节校验和

checkpoint操作时,需要把此次checkpoint相关的信息写入到第二和第四个block里,当checkpoint_no是偶数时写入到checkpoint1,奇数写入到checkpoint2。

5. LSN

InnoDB有一个全局变量log sequence number,缩写lsn,它代表redo log写入的日志总量(因为redo log是写入到redo log block里面的,所以lsn的大小还包含了block header和block trailer的大小)。它的初始值是8704,MTR结束时会把redo log复制到redo log buffer里,同时lsn会累加上对应的redo log占用的空间大小。lsn是不断累加的,不可能减少,这意味着lsn越小对应的redo log生成的越早!

redo log buffer会在适当的时机刷盘,InnoDB有一个全局变量buf_next_to_write代表log buffer中哪些log已经写入磁盘,它到buf_free的部分代表日志已经写入log buffer,但是还没写入磁盘,两者相等代表log buffer里的所有日志都写入到磁盘了。 InnoDB之redo log写入和恢复

InnoDB还有一个全局变量flushed_to_disk_lsn代表系统写入磁盘的redo log日志总量,它和lsn的区别是:lsn代表系统生成的redo log日志总量,这包含写入了redo log buffer,但是还没写入磁盘的redo log,如果flushed_to_disk_lsn和lsn相等,代表所有redo log都写入到磁盘了。

redo log首先被写到redo log buffer,通过变量buf_freeInnoDB就知道该往log buffer的哪个位置写了。当redo log buffer要刷盘时,那么首先面临的问题就是:要写到哪个redo log file的哪个位置? 这个问题很好解决,由于redo log file文件大小是固定的,且是顺序写的,全局变量flushed_to_disk_lsn代表redo log写入磁盘的日志总量,根据该lsn值很容易计算它对应到哪个redo log file的偏移量。

MTR结束时,除了把redo log复制到redo log buffer里,还需要把Buffer Pool里被修改的脏页加入到flush链表,后台线程会异步的将这些脏页同步到磁盘,只要脏页同步到磁盘,那么它对应的redo log就没用了。 InnoDB之redo log写入和恢复 把脏页加入到flush链表,其实就是把脏页对应的控制块加入到flush链表的表头,同时控制块会有两个属性:

  • oldest_modification:第一次修改页面MTR对应的LSN值。
  • newest_modification:最后一次修改页面MTR对应的LSN值。

oldest_modification会在索引页第一次被修改时写入对应的LSN值,当脏页被再次修改时,不会再移动它的位置了,仅仅是将新的LSN值写入newest_modification属性。 如此一来,flush链表的链尾控制块里的oldest_modification,就是当前所有脏页的最小LSN值。也就是说,凡是小于该LSN的redo log,都是可以被覆盖的,这方便了后续的checkpoint操作。

命令SHOW ENGINE INNODB STATUS可以查看InnoDB引擎的状态信息,这里面就包含各种LSN的值。

Log sequence number 7853861756693
Log flushed up to   7853861756453
Pages flushed up to 7853579810002
Last checkpoint at  7853579810002
  • Log sequence number:生成的redo log总量,即lsn
  • Log flushed up to:写入到磁盘的redo log总量,即flushed_to_disk_lsn
  • Pages flushed up to:Buffer Pool里最早被修改页面的oldest_modification
  • Last checkpoint at:对应checkpoint_lsn值。

6. checkpoint

redo log日志文件组的容量大小是有限的,而redo log在系统运行过程中是不断产生的,redo log file采用循环写的方式,终有一天会发生“追尾”。怎么办呢?这就是checkpoint操作要干的活儿。

再次回顾一下,redo log的作用是什么?它是为了防止Buffer Pool里的脏页还没来得及刷盘时,系统崩溃后做数据恢复用的。也就是说,只要Buffer Pool里的脏页刷盘了,那么这些已经刷盘的脏页对应的redo log就一点用都没有了,这些redo log是可以被覆盖的。

如何判断哪些redo log可以被覆盖? Buffer Pool里有一条flush链表,它的链尾元素是脏页对应的控制块,代表当前还未被刷盘且最早被修改的脏页。控制块里有oldest_modification属性代表脏页最早修改时的LSN值,redo log file中小于该LSN的redo log都是可以被覆盖的。

InnoDB有一个全局变量checkpoint_lsn,它代表当前可以被覆盖的redo log日志总量是多少。所谓的checkpoint其实非常简单,分为两步:

  1. 将flush链表的链尾控制块的oldest_modification属性值赋值给checkpoint_lsn
  2. checkpoint_lsncheckpoint_nocheckpoint_offset等信息写入到redo log file的checkpoint1或checkpoint2中。

checkpoint需要将checkpoint相关信息写入到redo log file文件,因此它是有代价的。

7. redo log刷盘策略

事务提交时,redo log需要写入到磁盘,尽管是顺序写,但毕竟是磁盘啊,这个代价还是很大的。所以InnoDB提供了innodb_flush_log_at_trx_commit选项,可以让你配置redo log的刷盘策略:

  • 0:事务提交时,redo log不再刷盘,而是交给后台线程去异步刷盘。优点是可以显著提升性能,缺点是系统崩溃后可能会丢失数据。
  • 1:默认值,每次事务提交redo log必须同步刷新到磁盘。
  • 2:事务提交时,redo log只写入操作系统的缓冲区(例如Linux的Page Cache),不保证写入磁盘。MySQL进程崩溃后数据不会丢失,但是操作系统崩溃数据会丢失。

综上所述,只有配置1才是符合事务的持久性,0和2可以换来事务性能上的提升,但前提都是以牺牲数据安全为代价的,除非你对数据安全没有强要求,否则都不建议修改该配置。

8. redo log恢复

只要MySQL进程正常运行,redo log完全就是个累赘,不仅没有任何作用,还会拖累数据库的性能。但是只要发生故障崩溃了,那redo log的重要性就体现出来了,MySQL重启时会利用redo log进行数据恢复,将崩溃前Buffer Pool里被修改的还没来得及刷盘的脏页给恢复回去。

恢复的第一件事,就是确定恢复的起点。redo log file文件可能会很大且很多,该从哪个文件的哪个位置开始恢复呢?checkpoint_lsn代表可以被覆盖的redo log日志总量,redo log可被覆盖意味着对应的脏页已经刷新到磁盘了,也就是说,小于checkpoint_lsn的redo log是不用恢复的,checkpoint_lsn就是InnoDB恢复的起点。 上哪儿去找checkpoint_lsn?checkpoint操作时,InnoDB会将checkpoint相关信息写入到redo log file的第二或第四个block里,也就是checkpoint1或checkpoint2。只需要将checkpoint1和checkpoint2读取出来,比较一下checkpoint_no的大小就知道最近一次checkpoint操作时对应的checkpoint_lsn了,以及它在redo log file的偏移量checkpoint_offset

恢复的起点确定了,接下来就是往后顺序读取redo log并解析,然后调用系统函数进行页面恢复。由于redo log file是顺序写的,该如何确定恢复的终点呢?也很简单,redo log block的header部分有一个属性LOG_BLOCK_HDR_DATA_LEN,它代表当前block被使用的大小。恢复时,只要读取到第一个LOG_BLOCK_HDR_DATA_LEN值小于512的block,就意味着是终点了。

事实上,MySQL针对redo log的恢复还做了一些优化来加速恢复过程:

  • 根据redo log的Space ID和Page Number建立哈希表,相同页面的redo log放到一起,并根据时间排序,对于同一个页面的所有修改,可以一起恢复,避免了多次随机IO。
  • 索引页的File Header部分有FIL_PAGE_LSN属性,记录了最近一次修改当前页的LSN值,如果redo log的LSN值小于它,那么该redo log就可以不用执行了。

9. 总结

事务执行期间,InnoDB先修改内存里的缓存页数据,然后生成redo log记录下对页做了哪些修改,累加lsn值,并将脏页加入到flush链表,同时写入oldest_modification记录下当时的lsn值,最后将redo log复制到redo log buffer中。事务提交时,redo log buffer刷盘,redo log会被写入到redo log file中。只要redo log刷盘成功,事务就算提交完成了,尽管脏页还没有刷盘。脏页会由后台线程异步刷盘,哪怕系统崩溃也没关系,MySQL重启时会根据redo log恢复数据页。 redo log file是循环写的,会存在追尾的情况。所以InnoDB会有checkpoint操作,后台线程异步的将脏页刷盘,只要脏页同步到磁盘,它对应的redo log就可以被覆盖了。由于lsn是redo log的日志总量,不断递增不会减小,所以所谓的checkpoint操作其实就是将flush链表里最早的lsn赋值给checkpoint_lsn,并将此次checkpoint相关的数据写入到redo log file里而已。