前言:
为什么需要redo log?
为了取得更好的读写性能,InnoDB会将数据缓存在内存中(InnoDB Buffer Pool),对磁盘数据的修改也会落后于内存,这时如果进程或机器崩溃,会导致内存数据丢失, 为了保证数据库本身的一致性和持久性,InnoDB维护了一套redo log机制。修改页(Page)之前需要先将修改的内容记录到redo log中,并保证redo log 早于对应的页落盘,也就是常说的WAL(Write Ahead Log)。当故障发生导致内存数据丢失后,InnoDB会在重启时,通过重放redo log,将页恢复到奔溃前的状态。
那为什么不能直接在事务提交之前,把该事务所修改的所有页面都刷盘来做持久性保障呢?
- 刷新一个完整的数据页太浪费了
有时候我们仅仅是修改了某个页面中的一个字节,但是InnoDB 是以页为单位来进行磁盘IO的,也就是说我们仅仅修改了一个字节,我们都不得不将一个完整的页面(一般是16KB)从内存刷新到磁盘中。
- 随机IO效率太低
一个事务可能包含很多语句,即使是一条语句也有可能修改很多页面(如B+树分裂),而且这些页面可能都不相邻,这就意味着把事务修改的页面刷新到磁盘时,需要进行很多的随机IO,随机IO比顺序IO要慢,尤其是对传统的机械硬盘来说。
我们只是想让已经提交的事务对数据库中数据所做的修改永久生效,即使崩溃,再重启后我们也可以恢复出来。所以我们其实没必要把修改过的所有页面都刷新到磁盘,只需要把修改了哪些东西记录一下,在事务提交时把记录的修改刷新到磁盘中。并且事务执行过程中的修改记录是按产生的顺序写入磁盘的,也就是常说的顺序IO,效率比较高。
我们小结下使用redo log做故障恢复的好处
- redo log占用的空间非常小
- redo log顺序写入磁盘(速度快)
有点类似于Redis的持久化机制,也是只记录改动操作本身。
redo log 的定义与格式:
redo log是一种基于磁盘文件的数据类型,用于在宕机时恢复出不完整的事务更新数据。
InnoDB是带事务的存储引擎,其通过Force Log at Commit 机制实现事务的持久性,即当事务提交时,必须先将该事务的所有重做日志写入到重做日志文件进行持久化,该事务的COMMIT操作才算完成。
redo log本质上只是记录了一下事务对数据库做了哪些修改。 InnoDB针对事务对数据库的不同修改场景定义了多种类型的redo log,但是绝大部分类型的redo log都有下边这种通用的结构:
各个部分的详细释义如下:
- Type:该条redo日志的类型。在MySQL 5.7.21这个版本中,设计InnoDB的大叔一共为redolog设计了53种不同的类型。
- Space ID:表空间ID。
- Page number:页号。
- Data:该条redo log的具体内容。
redo log 的存储:
我们来看下MySQL中redo log的三种存储状态:
这三种状态分别是:
- 存在redo log buffer 中,物理上是在MySQL 进程的内存中,就是图中红色部分;
- 写到(write)文件系统缓存中,但是没有持久化(没有刷到磁盘fsync),物理上是在文件系统的page cache里面,也就是图中黄色的部分;
- 持久化到磁盘(fsync), 对应的是hard disk, 也就是图中的绿色部分。(后文的刷盘指的都是持久化到磁盘)
redo log 写到redo log buffer是很快的,write 到page cache 也差不多,但是持久化fsync到磁盘的速度就慢多了。
redo log的刷新策略:
前面我们看到了redo log有三种不同的存储形态,前两种都是基于缓存,为了确保事务不丢失,redo log需要持久化到磁盘,但是这非常影响数据库的性能。所以InnoDB存储引擎允许用户通过参数innodb_flush_log_at_trx_commit手工设置redo log刷新磁盘的策略。
- 参数值为1(默认值): 表示事务提交时必须调用一次fsync操作。性能相对较差,但是强保证持久性。
- 参数值为0 : 表示事务提交时不进行写入redo log操作,把写入redo log的工作交给后台进程,但是如果事务提交后,后台线程没有来得及把redo log刷新磁盘,那事务的修改结果就会丢失。性能好,存在一定概率丢失事务结果。
- 参数值为2: 表示在事务操作时将redo log仅写入文件系统的缓存中,不进行fsync操作。这个设置下,当MySQL宕机而非操作系统宕机并不会导致事务的丢失,所以相比参数0,事务丢失概率更小一点,但是响应的性能稍差。
下图表示了不同配置值的持久化程度。
Mini-Transaction (redo log组)
我们知道事务在写入数据的时候会产生redo log,一次原子的操作可能会包含多条redo log,这些记录可能是访问同一个Page的不同位置,也可能是访问不同的Page(如Btree节点分裂)。Innodb有一套完整的机制来保证涉及一次原子操作的多条redo log的原子性,即恢复时要么全部重放,要么全部不重放,因此这些redo log记录必须连续。Innodb中通过mini_transaction实现,简称mtr,需要进行原子操作时,先生成一个mtr,接下来将这个原子操作所需要写的所有redo log都写到这个mtr中,当原子操作结束后,将这个mtr数据拷贝到redo log buffer中。
一个事务可以包含若干条语句,每一条语句其实是由若干个mtr组成,每一个mtr又可以包含若干条redo log,画个图表示它们的关系就是这样:
redo log buffer (重做日志缓存)
redo log buffer是一片连续的内存空间,被划分成了若干个512字节大小的block。redo log buffer 缓存了redo log的内容,由多个redo log首尾相连组成。
我们可以通过启动参数innodb_log_buffer_size来指定log buffer 的大小。
redo log block
redo log buffer 以Block为单位,被划分为连续的多个块。每个Block大小(OS_FILE_LOG_BLOCK_SIZE)为521B 等于磁盘扇区的大小, 每次IO读写的最小单位都是一个Block。除了redo log的数据外,Block中还需要一些额外的信息,下图所示为一个redo log block的组成,包括12字节的log block header: 前4字节中Flush Flag占用最高位bit, 标示一次IO的第一个Block,剩下的31个bit是Block编号;之后是2字节的数据长度,取值[2, 508];紧接着2字节的First Record Offset用来指向Block中第一个mtr组的开始,这个值的存在使得我们对任何一个Block都可以找到一条合法的redo log开始位置;最后的4字节Checkpoint Number记录写Block时的next_checkpoint_number,用来发现文件的循环使用,这个值会在redo log file小节详细讲解。Block末尾是4字节的Block Tailer,记录当前Block的Checksum,通过这个值,读取Log时可以明确Block数据是不是完整的。
Block中的Block Body用来存放真正的redo log 内容。由于Block内的空间固定,而一组redo log长度不定,因此可能一个Block中有多组redo log,也有可能一组redo log 被拆分到多个Block中。如下图所示,棕色和红色分别代表Block Header和Tailer,中间的一组redo log由于前一个Block剩余空间不足,而被拆分在连续的两个Block中。
同时我们前面讲过,一个事务可能包含多个mtr组,不同的事务是可以并发执行的,所以不同事务的mtr是有可能交替写入redo log buffer的。
redo log 刷盘时机:
我们前面讲到,redo log首先是保存在redo log buffer中,为了保证事务的数据数据不丢失,还需要进一步刷新到磁盘中,才能永久保留下来。刷新内存到磁盘中,是耗时的操作,Innodb作为一款高性能的数据库引擎,它的redo log 什么时候会刷新到磁盘呢?
- redo log buffer 空间不足时:当log buffer 中有一半的内存空间已经被使用时;
- 事务提交时;
- 系统做checkpoint时;
- 将脏页刷新到磁盘前:会保证先将该脏页对应的redo日志刷新到磁盘中。(redo log是顺序刷新的,所以在将某个脏页对应的redo日志从redo log buffer刷新到磁盘时,也会保证其之前产生的redo日志也刷新到磁盘中)
- 后台线程刷盘:后台有一个线程,大约每秒都会刷新一次log buffer中的redo log到磁盘;
- 正常关闭服务器时;
没有提交的事务的redo log 也有可能写入到磁盘中的三种场景:
- 后台线程每一秒的轮询刷盘操作
- redo log buffer 一半的空间被使用,会有后台线程主动写盘。注意,由于这个事务还没有提交,所以这个写盘动作只是write,而没有调用fsync, 也就是只留在了文件系统的page cache。
- 并行的事务提交时,顺带将这个事务的redo log buffer 持久化到磁盘。假设一个事务A执行到一半,已经写了一些redo log到buffer中,这时候有另外一个事务B提交,如果
innodb_flush_log_at_trx_commit设置的是1,那么事务B会把redo log buffer里的日志全部持久化到磁盘。这时候就会带上事务A在redo log buffer里的日志一起持久化到磁盘。
redo log file (redo log文件)
redo log文件组
redo log buffer中的redo log记录最终都会被写入到redo log file文件中,以ib_logfile0、ib_logfile1...命名,为了避免创建文件及初始化空间带来的开销,Innodb的redo log file会循环使用,多个文件首尾相连顺序写入REDO内容。
redo log文件格式
我们前边说过redo log buffer本质上是一片连续的内存空间,被划分成了若干个512字节大小的block。将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入redo log文件中,所以redo log文件其实也是由若干个512字节大小的block组成。每个文件的开头固定预留4个Block来记录一些额外的信息,其中第一个Block称为Header Block,之后的3个Block在0号文件上用来存储Checkpoint信息,而在其它文件上留空:
其中第一个Header Block的数据区域记录了一些文件信息,如下图所示,4字节的Format字段记录redo log的版本,不同版本的LOG,会有REDO类型的增减,这个信息是8.0开始加入的;8字节的Start LSN表示当前文件开始对应的LSN(是该文件偏移量为2048字节对应的LSN值),通过这个信息可以将文件的offset与对应的LSN对应起来;最后是最长32位的Creator信息,正常情况下会记录MySQL的版本。
预留的两个Checkpoint Block会在打Checkpoint的时候交替使用,这样来避免写Checkpoint过程中的崩溃导致没有可用的Checkpoint。Checkpoint Block中的内容如下:
首先8个字节的Checkpoint Number,通过比较这个值可以判断哪个是最新的Checkpoint记录,之后8字节的Checkpoint LSN 为系统做checkpoint结束时对应的LSN值,恢复时会从这个位置开始重放后面的redo log。之后的8个字节的Checkpoint Offset,是Checkpoint LSN在redo log文件组中的偏移量,将Checkpoint LSN与文件空间的偏移对应起来。最后8字节是前面提到的Log Buffer的长度,这个值目前在恢复过程中并没有使用。\
现在我们将redo log放到文件空间中,如下图所示,Logical REDO是真正需要的数据,用SN索引,Logical REDO按固定大小的Block组织,并添加Block的头尾信息形成Physical REDO,用LSN索引,这些Block又会放到循环使用的文件空间中的某一个位置,文件中用offset索引:
使用SHOW VARIABLES LIKE 'datadir',我们就可以看到redo文件组。如果我们对前面默认的redo日志文件不满意,可以通过下边几个启动参数来调节:
- innodb_log_group_home_dir该参数指定了redo日志文件所在的目录,默认值就是当前的数据目录。
- innodb_log_file_size该参数指定了每个redo日志文件的大小,在MySQL 5.7.21这个版本中的默认值为48MB,
- innodb_log_files_in_group该参数指定redo日志文件的个数,默认值为2,最大值为100。
LSN (Log Sequence Number)
lsn是一个8字节无符号整形数字,存储在log_sys对象中的全局变量,表示事务写入redo log的字节的总量。可以简单理解LSN就是从开始到现在已经产生了多少字节的redo log记录,LSN越小,说明redo 日志产生的越早。
flushed_to_disk_lsn 是一个表示刷新到磁盘中的redo log总量的全局变量。当有新的redo日写入到log buffer时,首先lsn会增长,但是flushed_to_disk_lsn不变,然后随着不断有log buffer中的日志被刷新到磁盘上(fsync),flushed_to_disk_lsn的值也跟着增长。如果两者的值相同时,说明log buffer的所有日志都已经刷新到磁盘中了。用一张图来表示各个变量,会很有体感。
我们可以使用SHOW ENGINE INNODB STATUS 命令查看当前InnoDB存储引擎中的各种LSN值的情况:
mysql> SHOW ENGINE INNODB STATUS\G
(...省略前边的许多状态)
LOG
---
Log sequence number 124476971
Log flushed up to 124099769
Pages flushed up to 124052503
Last checkpoint at 124052494
0 pending log flushes, 0 pending chkp writes
24 log i/o's done, 2.00 log i/o's/second
----------------------
(...省略后边的许多状态)
Log sequence number
表示当前系统的lsn值,也就是当前系统已经写入的redo 日志量,包括写入log buffer 但是没有刷新到磁盘的redo log。
Log flushed up to
redo 日志是首先写到log buffer中,之后才会被刷新到磁盘上的redo日志文件。Log flushed up to
表示刷新到磁盘的redo日志文件的lsn,也就是flushed_to_disk_lsn。
Pages flushed up to
代表flush链表中被最早修改的那个页面对应的oldest_modification属性值
Last checkpoint at
当前系统的checkpoint_lsn 值,redo log file中,小于该值的文件都可以被覆盖掉。关于该值的详细作用,参考Checkpoint小节。
lsn值与redo日志文件偏移量的对应关系
因为lsn的值是代表系统写入redo日志量的一个总和,一次产生多少redo 日志,lsn的值就增加多少(当然有时候要加上log block header 和log block tailer的大小),这样redo 日志写到磁盘中时,就很容易计算某一个lsn值在redo日志文件组中的偏移量,如图:
Checkpoint
redo log file文件组中的文件大小是有限的,会循环使用,势必会存在需要覆盖之前的redo log file文件。那么我们如何判断文件是否可以被覆盖呢?联系到redo log的目的是为了避免只写了内存的数据由于故障丢失,只要保证打checkpoint的位置之前的所有redo log对应的内存脏页都已经刷盘。也就是说:判断redo log占用的redo log file是否可以被覆写的依据是它对应的脏页是否已经刷新到磁盘。
checkpoint_lsn代表当前系统中可以被覆盖的redo log的总量是多少。
系统在做checkpoint时,会计算当前可以被覆盖的redo日志对应的lsn值最大是多少。在当前Buffer Pool中最早修改的脏页之前生成的redo 日志都可以被覆盖(因为之前产生的脏页都被刷新到了磁盘,可以保证数据不会丢失,也就用不到redo 日志了),因此当前系统可以被覆盖的最大lsn值是该最早修改的脏页开始加入Buffer Pool时对应的lsn值(对应前小节中的Pages flushed up to),我们可以把该值赋给checkpoint_lsn。
接着我们把checkpoint_lsn和对应的redo日志文件组偏移量checkpoint_offset以及此次checkpoint的编号写到日志文件的管理信息中(对应Checkpoint block)。checkpoint的编号(checkpoint_no)是递增的,每做一次checkpoint,该变量的值就加1,checkpoint_no为偶数时,就写到checkpoint1中,是奇数时,就写到checkpoint2中。我们前面说过计算一个lsn值对应的redo日志文件组偏移量是很容易的,所以也可以方便的得到改checkpoint_lsn对应的checkpoint_offset。
崩溃恢复
我们前面说过,checkpoint_lsn之前的redo日志都可以被覆盖,也就是说这些redo日志对应的脏页都已经被刷新到磁盘中了,既然被刷盘,我们就没必要恢复它们了。对于checkpoint_lsn之后的redo日志,他们对应的脏页可能没有被刷盘,也可能被刷盘,不能确定,所以需要从checkpoint_lsn开始读区redo日志来恢复页面。
确定恢复的起点
redo日志文件组的第一个文件的管理信息中有两个block都存储了checkpoint_lsn的信息,我们只需要把checkpoint_no最大的对应的checkpoint_lsn和checkpoint_offset取出即可。
确定恢复的终点
普通block的log block header部分有一个称之为LOG_BLOCK_HDR_DATA_LEN的属性,该属性值记录了当前block里使用了多少字节的空间。对于被填满的block来说,该值永远为512。如果该属性的值不为512,那么就是它了,它就是此次崩溃恢复中需要扫描的最后一个block。
小结几个参数的作用
| 参数 | 来源 | 作用 |
|---|---|---|
| lsn | 系统全局变量 | 记录从MySQL启动到现在已经产生了多少字节的redo log记录,取值>=flushed_to_disk_lsn |
| flushed_to_disk_lsn | 系统全局变量 | 记录了刷新到磁盘(fsync)的redo log记录的总量,取值 checkpoint_lsn<=flushed_to_disk_lsn<=lsn |
| checkpoint_lsn | 系统全局变量,会记录到redo log file的Log File Header的Checkpoint Blocker中 | 记录了当前系统中可以被覆盖的redo log记录的总量时多少,取值:checkpoint_lsn<=flushed_to_disk_lsn |
参考资料:
time.geekbang.org/column/arti…