MySQL InnoDB Update和Crash Recovery流程

本文将为大家介绍Redo,Undo,Log Sequence Number (LSN)等概念,以及MySQL Update过程,Redo、Undo双写之间的配合,脏页刷新时间和Crash Recovery时如何做恢复。

作者 罗小波·沃趣科技高级数据库技术专家
出品 沃趣科技

| InnoDB 术语和概念

我们首先来InnoDB的一些基本术语和概念,以便更好地理解下文中介绍的Update和Crash Recovery流程

InnoDB概述图

InnoDB 重要术语和概念

什么是Redo?

通常也会叫做"InnoDB log(s)",预先分配至少2个日志文件,第一个文件开头和最后一个文件结尾进行首尾相连以循环的方式重复使用。"Redo"的意思是在必要时(如:崩溃恢复时)可以使用Redo Log中的数据来重新应用到InnoDB数据文件中,使得InnoDB能够恢复到一个一致性状态

Redo Log 是一个预写日志(WAL),是一种用于在数据库或数据库所在主机发生崩溃时确保数据完整性的技术。Redo Log日志记录必须在数据实际更改(buffer pool中的脏页刷新到数据文件)之前写入磁盘。因此,对数据表做修改时,每个数据记录的修改都会写入Redo Log Buffer中(作为重做日志记录)。当一个页面的修改操作完成时,Redo Log Buffer将执行flush到Redo Log文件操作(但此时可能并未sync到磁盘)

根据WAL日志先行原则,buffer pool中的脏页被刷新到数据文件中之前,需要确保对应LSN号的Redo Log先sync到磁盘文件中,Redo Log的刷盘机制以及脏页的刷盘机制确保了Redo Log总是先于脏页落盘。另外,如果系统参数innodb_flush_log_at_trx_commit设置为1,则每个事务提交时也会将Redo Log sync到磁盘文件中

Redo Log 日志组结构

什么是Undo?

用于撤消(或还原)对InnoDB中存储的数据的变更及回滚事务,也用于实现多版本控制(mvcc),通过构建一致性视图(read view)实现对数据库的一致性读

对数据库的每一次更改,Undo Log都会保存之前版本的数据,每个聚簇(PK)索引记录都有一个指向该修改记录之前版本数据的指针(称为“回滚指针”),每个Undo Log记录都会存储一个回滚指针指向之前版本的数据,另外,每个Undo Log的变更也必须记录到Redo Log中

PS:Undo Log在共享表空间的基本结构

· 在共享表空间的第6个页存放了InnoDB的事务系统信息,其中也包含了Undo Log的一些系统信息

· InnoDB事务系统信息页结构

· InnoDB事务系统最多可以创建128个回滚段(MySQL 8.x版本除外),每个回滚段中都需要有一个单独的page来维护其拥有的undo solt(通常是每个回滚段中的第一个页),每个回滚段有1024个事务槽,每个事务槽指针都指向每个回滚段中的第一个UNDO_lOG页中的回滚段头

· Undo Log的数据存储在系统表空间的UNDO_LOG页中,下面分别是UNDO_LOG页结构、UNDO_LOG页中UNDO页头部和UNDO段头部、UNDO_LOG记录格式示意图

什么是Log Sequence Number (LSN)?

一个64位无符号整数,表示Redo Log系统中的时间点,也是事务写入Redo Log的字节总量,从日志初始化开始计数(数据库初始化安装时间点开始且单调递增)

LSN不仅存在于Redo Log中,在每个数据页中都保存着一个LSN,在进行数据恢复时通过LSN做比较运算可以判断出每个数据页是否需要进行恢复操作

什么是Checkpoint?

一个时间点,由一个LSN值(Checkpoint LSN)表示的整型值,在Checkpoint LSN之前的每个数据页(buffer pool中的脏页)的更改都已经落盘(刷新到数据文件中),Checkpoint 完成后,在Checkpoint LSN之前的Redo Log就不再需要了

Checkpoint技术是为了解决:全量Redo Log恢复时间太长、buffer pool中的空闲页不够用时将脏页刷新到磁盘数据文件、Redo Log空间不够用时将脏页刷新到磁盘数据文件等问题

Checkpoint方式有两种:Sharp Checkpoint和Fuzzy Checkpoint(又可根据不同的场景细分)

· Sharp Checkpoint:将所有的脏页刷回磁盘,数据库实例关闭时系统参数innodb_fast_shutdown设置为0,才需要把所有的脏页都刷回磁盘,刷脏时系统hang住

· Fuzzy Checkpoint:持续的每次只刷新一部分脏页到磁盘,数据库正常运行过程中都是使用这种方式刷脏,在InnoDB内部还可细分为如下几种:

·· Master线程每秒/每十秒固定执行Checkpoint

·· LRU list中空闲页不够时,触发Checkpoint从LRU list刷新脏页以释放足够的空闲页

·· Redo Log空间不够时,触发Checkpoint从Flush list刷新脏页,Checkpoint执行完成之后,在这个位置之前的Redo Log不再需要(即,可以循环覆盖使用)

·· 脏页太多达到脏页比例阀值(系统参数innodb_max_dirty_pages_pct和innodb_max_dirty_pages_pct_lwm控制脏页比例阀值),触发Checkpoint

什么是Rollback Pointer (ROLL_PTR)?

一个由rollback segment number、page number和page offset组成的指针,指向Undo Log中包含之前版本数据的具体Undo Log日志记录

可用于为任何数据记录回退到一个历史版本记录、可用于mvcc中重建旧版本记录、可用于事务回滚

什么是Transaction ID (TRX_ID)?

· 表示事务开始点的一个64位无符号整数

· 每个事务的事务号增量增加

· 事务号会写入聚簇索引的每个记录中

· 最大事务号会写入系统表空间的TRX_SYS页

什么是Transaction Serialization Number(TRX_NO) ?

· 一个64位无符号整数,表示事务提交时的最大TRX_ID

· TRX_NO在事务提交时会写入Undo Log Header

· TRX_NO可用于purge Undo Log中的旧版本记录

| Update流程

事务start(事务首次开启)

为这个事务分配事务ID(TRX_ID),该事务ID可能被写入系统表空间的TRX_SYS页面中的最大的事务ID字段(Transaction ID)

· 如果系统表空间的TRX_SYS页面中的最大的事务ID字段被更新,则该更新会被记录到Redo Log中

根据分配的TRX_ID创建read view

记录修改(每次只修改一行记录)

分配Undo Log日志空间

拷贝该记录修改之前的值到Undo Log中

将Undo Log的修改记录写入Redo Log中

在buffer pool中修改数据页,回滚段指针指向Undo Log中该记录之前的版本

将该记录对应的数据页变更部分写入Undo Log中

buffer pool中该记录修改之后的数据页被标记为"脏页"(需要刷新到磁盘的数据页)

此时其他事务的修改会怎样?

一旦记录被修改,即使没有提交,其他事务也可能会看到被修改后的记录,这依赖于他们的事务隔离级别而定

· 如果是RU隔离级别,则使用索引页读取最新版本记录
· 如果是RC隔离级别,则查找记录的最新提交版本
· 如果是RR隔离级别,则查找与其read view相对应的记录版本

任何需要使用索引页来读取比最新的版本记录旧的版本记录时,都必须使用Undo Log来重建之前的版本记录

事务提交(显式和隐式提交)

事务对应的Undo Log页被设置为"purge"(意味着当这个Undo Log页不再被任何其他事务引用时可以将其清除)

将Undo Log的修改记录写入Redo Log中

Redo Log Buffer刷新到磁盘(是否刷盘取决于系统变量innodb_flush_log_at_trx_commit的设置)

后台线程刷脏(后台线程连续不断地根据不同触发机制触发刷新)

查找最旧的“脏”页面(修改时间最早的页面)并将其添加到flush batch中

确保在flush batch中最新的LSN号已经写入到了Redo Log中且已经落盘

如果开启了双写,则先将脏页刷新到双写缓冲区(并等待同步)

将每个脏页从buffer pool中写入最终目的地:表空间文件中的

PS:对于后台线程刷脏部分,执行刷新脏页时,与该脏页的事务是否提交无关,只需要确保该页对应LSN号的Redo Log记录落盘,而不会去判断事务的状态是否是提交还是未提交状态,因为,数据页结构中并没有地方单独记录事务的状态(即,无法判断事务是否提交),只是在每行数据中有记录事务号、回滚段指针(所以一个页中也可能包含多个事务的修改记录)。当需要对某个事务进行回滚时,重新从表空间中读取这个未提交的脏页,使用undo log中的反向数据进行反向修改,然后再重新刷脏

定期执行Checkpoint

确保比Checkpoint 点更旧(比Checkpoint LSN小)的脏页已刷新到表空间文件,如果存在有比Checkpoint LSN大的脏页,则立即刷新脏页到数据文件中。说白了Checkpoint机制主要作用就是用于刷新脏页

把Checkpoint LSN写到Redo Log Header中 (从这个Checkpoint LSN开始,之前的Redo Log记录不再需要)

后台线程Purge(后台线程连续不断地根据需要定期执行Purge,包括Undo Log和历史链表)

查找每个回滚段中不再需要的最旧的Undo Log

实际上是从索引中删除任何带有删除标记的记录

释放Undo Log页

修剪history lists

| Creash Recovery流程

什么时候会进行Crash Recovery?

实例崩溃之后重启

使用一个备份还原(如:LVM 快照、xtrabackup备份)后

在“快速”(innodb_fast_shutdown不为0值关闭实例)关闭实例后重新启动

检测实例是不是干净地关闭的

打开Redo Logs和系统表空间文件(ibdataN)

读取并从中找到最大的Checkpoint LSN

从最近的Checkpoint 开始往后扫描Redo Log

如果能够找到Redo Log记录,说明还有数据页的更改没有刷新到数据文件上,启动Crash Recovery,使用Redo Log来恢复数据的一致性

使用所有独立表空间的表名和表空间ID创建一个名称到ID的映射

打开datadir下的所有.ibd文件

在这些表空间文件的offset 0的页(FSP_HDR页)头读取其表空间ID(FSP_HDR页中FSP Header的前四个字节记录着表空间ID)

将表空间ID与表名建立映射

PS:

· page offset 0(FSP_HDR页)页结构

· FSP_HDR页中FSP Header的前四个字节记录着表空间ID

损坏页修复(检查是否有不完整的页,如果有则使用Double Write Buffer进行修复)

检查双写缓冲区中的所有128个页:

· 读取表空间中的每个“目标”页

· 如果页头和页尾的LSN不匹配或页面校验和无效,则使用双写缓冲区中的页进行还原

· 如果该页在双写缓冲区中的版本也被破坏,则server将crash

前滚Redo,回滚未提交事务

· 事务系统初始化(回滚段初始化)

· 从最近的Checkpoint 往后扫描到的Redo Log记录将被应用到各个数据文件中

· 从Undo Log中恢复处于'ACTIVE'状态的事务,重新生成read view

· 使用Undo Log回滚未提交的'ACTIVE'状态的事务

· 处于PREPARE状态的事务,如果打开了binlog且在binlog有找到对应事务的日志则重新提交,否则回滚

| 参考资料

本文大部分为译文,原文PDF下载链接:

https://www.percona.com/live/mysql-conference-2013/sites/default/files/slides/InnoDB%20-%20A%20journey%20to%20the%20core%20-%20PLMCE%202013.pdf

https://www.percona.com/live/mysql-conference-2014/sites/default/files/slides/InnoDB%20-%20A%20journey%20to%20the%20core%20II.pdf

https://www.percona.com/live/mysql-conference-2015/sites/default/files/slides/InnoDB%20-%20A%20journey%20to%20the%20core%20III.pdf

解析表空间工具:https://blog.jcole.us/2013/01/03/a-quick-introduction-to-innodb-ruby/

深入了解InnoDB学习资料:https://blog.jcole.us/innodb/

MySQL · 引擎特性 · InnoDB 文件系统之文件物理结构:https://yq.aliyun.com/articles/51133

关于2.5. 小节的PS部分,参考了oracle的资料,个人觉得MySQL也是同样的逻辑:http://blog.csdn.net/dba_waterbin/article/details/7823519


| 作者简介

罗小波·沃趣科技高级数据库技术专家

IT从业多年,历任运维工程师、高级运维工程师、运维经理、数据库工程师,曾参与版本发布系统、轻量级监控系统、运维管理平台、数据库管理平台的设计与编写,熟悉MySQL体系结构,Innodb存储引擎,喜好专研开源技术,追求完美。


发表评论

电子邮件地址不会被公开。 必填项已用*标注