其他分享
首页 > 其他分享> > 21. 说过的话就一定要办到 --- redo 日志(下)

21. 说过的话就一定要办到 --- redo 日志(下)

作者:互联网

redo 日志(下)

标签: MySQL 是怎样运行的


redo日志文件

redo日志刷盘时机

我们前边说mtr运行过程中产生的一组redo日志在mtr结束时会被复制到log buffer中,可是这些日志总在内存里呆着也不是个办法,在一些情况下它们会被刷新到磁盘里,比如:

redo日志文件组

MySQL的数据目录(使用SHOW VARIABLES LIKE 'datadir'查看)下默认有两个名为ib_logfile0ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。如果我们对默认的redo日志文件不满意,可以通过下边几个启动参数来调节:

从上边的描述中可以看到,磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile[数字]数字可以是012...)的形式进行命名。在将redo日志写入日志文件组时,是从ib_logfile0开始写,如果ib_logfile0写满了,就接着ib_logfile1写,同理,ib_logfile1写满了就去写ib_logfile2,依此类推。如果写到最后一个文件该咋办?那就重新转到ib_logfile0继续写,所以整个过程如下图所示:

 

总共的redo日志文件大小其实就是:innodb_log_file_size × innodb_log_files_in_group

小贴士:如果采用循环使用的方式向redo日志文件组里写数据的话,那岂不是要追尾,也就是后写入的redo日志覆盖掉前边写的redo日志?当然可能了!所以设计InnoDB的大叔提出了checkpoint的概念,稍后我们重点唠叨~

redo日志文件格式

我们前边说过log buffer本质上是一片连续的内存空间,被划分成了若干个512字节大小的block。将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,所以redo日志文件其实也是由若干个512字节大小的block组成。

redo日志文件组中的每个文件大小都一样,格式也一样,都是由两部分组成:

所以我们前边所说的循环使用redo日志文件,其实是从每个日志文件的第2048个字节开始算,画个示意图就是这样:

 

普通block的格式我们在唠叨log buffer的时候都说过了,就是log block headerlog block bodylog block trialer这三个部分,就不重复介绍了。这里需要介绍一下每个redo日志文件前2048个字节,也就是前4个特殊block的格式都是干嘛的,废话少说,先看图:

从图中可以看出来,这4个block分别是:

Log Sequeue Number

自系统开始运行,就不断的在修改页面,也就意味着会不断的生成redo日志。redo日志的量在不断的递增,就像人的年龄一样,自打出生起就不断递增,永远不可能缩减了。设计InnoDB的大叔为记录已经写入的redo日志量,设计了一个称之为Log Sequeue Number的全局变量,翻译过来就是:日志序列号,简称lsn。不过不像人一出生的年龄是0岁,设计InnoDB的大叔规定初始的lsn值为8704(也就是一条redo日志也没写入时,lsn的值为8704)。

我们知道在向log buffer中写入redo日志时不是一条一条写入的,而是以一个mtr生成的一组redo日志为单位进行写入的。而且实际上是把日志内容写在了log blcok body处。但是在统计lsn的增长量时,是按照实际写入的日志量加上占用的log block headerlog block trailer来计算的。我们来看一个例子:

小贴士:

为什么初始的lsn值为8704呢?我也不太清楚,人家就这么规定的。其实你也可以规定你一生下来算1岁,只要保证随着时间的流逝,你的年龄不断增长就好了。

从上边的描述中可以看出来,每一组由mtr生成的redo日志都有一个唯一的LSN值与其对应,LSN值越小,说明redo日志产生的越早。

flushed_to_disk_lsn

redo日志是首先写到log buffer中,之后才会被刷新到磁盘上的redo日志文件。所以设计InnoDB的大叔提出了一个称之为buf_next_to_write的全局变量,标记当前log buffer中已经有哪些日志被刷新到磁盘中了。画个图表示就是这样:

 

我们前边说lsn是表示当前系统中写入的redo日志量,这包括了写到log buffer而没有刷新到磁盘的日志,相应的,设计InnoDB的大叔提出了一个表示刷新到磁盘中的redo日志量的全局变量,称之为flushed_to_disk_lsn。系统第一次启动时,该变量的值和初始的lsn值是相同的,都是8704。随着系统的运行,redo日志被不断写入log buffer,但是并不会立即刷新到磁盘,lsn的值就和flushed_to_disk_lsn的值拉开了差距。我们演示一下:

综上所述,当有新的redo日志写入到log buffer时,首先lsn的值会增长,但flushed_to_disk_lsn不变,随后随着不断有log buffer中的日志被刷新到磁盘上,flushed_to_disk_lsn的值也跟着增长。如果两者的值相同时,说明log buffer中的所有redo日志都已经刷新到磁盘中了。

小贴士:

应用程序向磁盘写入文件时其实是先写到操作系统的缓冲区中去,如果某个写入操作要等到操作系统确认已经写到磁盘时才返回,那需要调用一下操作系统提供的fsync函数。其实只有当系统执行了fsync函数后,flushed_to_disk_lsn的值才会跟着增长,当仅仅把log buffer中的日志写入到操作系统缓冲区却没有显式的刷新到磁盘时,另外的一个称之为write_lsn的值跟着增长。不过为了大家理解上的方便,我们在讲述时把flushed_to_disk_lsn和write_lsn的概念混淆了起来。

lsn值和redo日志文件偏移量的对应关系

因为lsn的值是代表系统写入的redo日志量的一个总和,一个mtr中产生多少日志,lsn的值就增加多少(当然有时候要加上log block headerlog block trailer的大小),这样mtr产生的日志写到磁盘中时,很容易计算某一个lsn值在redo日志文件组中的偏移量,如图:

 

初始时的LSN值是8704,对应文件偏移量2048,之后每个mtr向磁盘中写入多少字节日志,lsn的值就增长多少。

flush链表中的LSN

我们知道一个mtr代表一次对底层页面的原子访问,在访问过程中可能会产生一组不可分割的redo日志,在mtr结束时,会把这一组redo日志写入到log buffer中。除此之外,在mtr结束时还有一件非常重要的事情要做,就是把在mtr执行过程中可能修改过的页面加入到Buffer Pool的flush链表。为了防止大家早已忘记flush链表是个啥,我们再看一下图:

当第一次修改某个缓存在Buffer Pool中的页面时,就会把这个页面对应的控制块插入到flush链表的头部,之后再修改该页面时由于它已经在flush链表中了,就不再次插入了。也就是说flush链表中的脏页是按照页面的第一次修改时间从大到小进行排序的。在这个过程中会在缓存页对应的控制块中记录两个关于页面何时修改的属性:

我们接着上边唠叨flushed_to_disk_lsn的例子看一下:

总结一下上边说的,就是:flush链表中的脏页按照修改发生的时间顺序进行排序,也就是按照oldest_modification代表的LSN值进行排序,被多次更新的页面不会重复插入到flush链表中,但是会更新newest_modification属性的值。

checkpoint

有一个很不幸的事实就是我们的redo日志文件组容量是有限的,我们不得不选择循环使用redo日志文件组中的文件,但是这会造成最后写的redo日志与最开始写的redo日志追尾,这时应该想到:redo日志只是为了系统奔溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统奔溃,那么在重启后也用不着使用redo日志恢复该页面了,所以该redo日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的redo日志所重用。也就是说:判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。我们看一下前边一直唠叨的那个例子:

 

如图,虽然mtr_1mtr_2生成的redo日志都已经被写到了磁盘上,但是它们修改的脏页仍然留在Buffer Pool中,所以它们生成的redo日志在磁盘上的空间是不可以被覆盖的。之后随着系统的运行,如果页a被刷新到了磁盘,那么它对应的控制块就会从flush链表中移除,就像这样子:

 

这样mtr_1生成的redo日志就没有用了,它们占用的磁盘空间就可以被覆盖掉了。设计InnoDB的大叔提出了一个全局变量checkpoint_lsn来代表当前系统中可以被覆盖的redo日志总量是多少,这个变量初始值也是8704

比方说现在页a被刷新到了磁盘,mtr_1生成的redo日志就可以被覆盖了,所以我们需要进行一个增加checkpoint_lsn的操作,我们把这个过程称之为做一次checkpoint。做一次checkpoint其实可以分为两个步骤:

记录完checkpoint的信息之后,redo日志文件组中各个lsn值的关系就像这样:

 

批量从flush链表中刷出脏页

我们在介绍Buffer Pool的时候说过,一般情况下都是后台的线程在对LRU链表flush链表进行刷脏操作,这主要因为刷脏操作比较慢,不想影响用户线程处理请求。但是如果当前系统修改页面的操作十分频繁,这样就导致写日志操作十分频繁,系统lsn值增长过快。如果后台的刷脏操作不能将脏页刷出,那么系统无法及时做checkpoint,可能就需要用户线程同步的从flush链表中把那些最早修改的脏页(oldest_modification最小的脏页)刷新到磁盘,这样这些脏页对应的redo日志就没用了,然后就可以去做checkpoint了。

查看系统中的各种LSN值

我们可以使用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
----------------------
(...省略后边的许多状态)

其中:

innodb_flush_log_at_trx_commit的用法

我们前边说为了保证事务的持久性,用户线程在事务提交时需要将该事务执行过程中产生的所有redo日志都刷新到磁盘上。这一条要求太狠了,会很明显的降低数据库性能。如果有的同学对事务的持久性要求不是那么强烈的话,可以选择修改一个称为innodb_flush_log_at_trx_commit的系统变量的值,该变量有3个可选的值:

崩溃恢复

在服务器不挂的情况下,redo日志简直就是个大累赘,不仅没用,反而让性能变得更差。但是万一,我说万一啊,万一数据库挂了,那redo日志可是个宝了,我们就可以在重启时根据redo日志中的记录就可以将页面恢复到系统奔溃前的状态。我们接下来大致看一下恢复过程是个啥样。

确定恢复的起点

我们前边说过,checkpoint_lsn之前的redo日志都可以被覆盖,也就是说这些redo日志对应的脏页都已经被刷新到磁盘中了,既然它们已经被刷盘,我们就没必要恢复它们了。对于checkpoint_lsn之后的redo日志,它们对应的脏页可能没被刷盘,也可能被刷盘了,我们不能确定,所以需要从checkpoint_lsn开始读取redo日志来恢复页面。

当然,redo日志文件组的第一个文件的管理信息中有两个block都存储了checkpoint_lsn的信息,我们当然是要选取最近发生的那次checkpoint的信息。衡量checkpoint发生时间早晚的信息就是所谓的checkpoint_no,我们只要把checkpoint1checkpoint2这两个block中的checkpoint_no值读出来比一下大小,哪个的checkpoint_no值更大,说明哪个block存储的就是最近的一次checkpoint信息。这样我们就能拿到最近发生的checkpoint对应的checkpoint_lsn值以及它在redo日志文件组中的偏移量checkpoint_offset

确定恢复的终点

redo日志恢复的起点确定了,那终点是哪个呢?这个还得从block的结构说起。我们说在写redo日志的时候都是顺序写的,写满了一个block之后会再往下一个block中写:

 

普通block的log block header部分有一个称之为LOG_BLOCK_HDR_DATA_LEN的属性,该属性值记录了当前block里使用了多少字节的空间。对于被填满的block来说,该值永远为512。如果该属性的值不为512,那么就是它了,它就是此次奔溃恢复中需要扫描的最后一个block。

怎么恢复

确定了需要扫描哪些redo日志进行奔溃恢复之后,接下来就是怎么进行恢复了。假设现在的redo日志文件中有5条redo日志,如图:

 

由于redo 0checkpoint_lsn后边,恢复时可以不管它。我们现在可以按照redo日志的顺序依次扫描checkpoint_lsn之后的各条redo日志,按照日志中记载的内容将对应的页面恢复出来。这样没什么问题,不过设计InnoDB的大叔还是想了一些办法加快这个恢复的过程:

遗漏的问题:LOG_BLOCK_HDR_NO是如何计算的

我们前边说过,对于实际存储redo日志的普通的log block来说,在log block header处有一个称之为LOG_BLOCK_HDR_NO的属性(忘记了的话回头再看看哈),我们说这个属性代表一个唯一的标号。这个属性是初次使用该block时分配的,跟当时的系统lsn值有关。使用下边的公式计算该block的LOG_BLOCK_HDR_NO值:

((lsn / 512) & 0x3FFFFFFFUL) + 1

这个公式里的0x3FFFFFFFUL可能让大家有点困惑,其实它的二进制表示可能更亲切一点:

 

从图中可以看出,0x3FFFFFFFUL对应的二进制数的前2位为0,后30位的值都为1。我们刚开始学计算机的时候就学过,一个二进制位与0做与运算(&)的结果肯定是0,一个二进制位与1做与运算(&)的结果就是原值。让一个数和0x3FFFFFFFUL做与运算的意思就是要将该值的前2个比特位的值置为0,这样该值就肯定小于或等于0x3FFFFFFFUL了。这也就说明了,不论lsn多大,((lsn / 512) & 0x3FFFFFFFUL)的值肯定在0~0x3FFFFFFFUL之间,再加1的话肯定在1~0x40000000UL之间。而0x40000000UL这个值大家应该很熟悉,这个值就代表着1GB。也就是说系统最多能产生不重复的LOG_BLOCK_HDR_NO值只有1GB个。设计InnoDB的大叔规定redo日志文件组中包含的所有文件大小总和不得超过512GB,一个block大小是512字节,也就是说redo日志文件组中包含的block块最多为1GB个,所以有1GB个不重复的编号值也就够用了。

另外,LOG_BLOCK_HDR_NO值的第一个比特位比较特殊,称之为flush bit,如果该值为1,代表着本block是在某次将log buffer中的block刷新到磁盘的操作中的第一个被刷入的block。

 

标签:checkpoint,21,lsn,---,mtr,日志,redo,log
来源: https://www.cnblogs.com/zouzhibin/p/15898742.html