暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

全方位解读 MySQL 日志实现内幕(二)

Qunar技术沙龙 2021-03-31
613


点击上方蓝字关注我们!



作者介绍

王竹峰,去哪儿网数据库专家,擅长数据库开发、数据库管理及维护,一直致力于 MySQL 数据库源码的研究与探索,对数据库原理及实现具有深刻的理解。曾就职于达梦数据库,多年从事数据库内核开发的工作,后转战人人网,任职高级数据库工程师,目前在去哪儿网负责 MySQL 源码研究与运维、数据库管理和自动化运维平台设计开发及实践工作,是 Inception 开源项目及《MySQL 运维内参》的作者,也是 Oracle MySQL ACE。


------

本文作者将出版于《MySQL 运维内参》中部分内容进行分享,通过多篇文章连载形式,全方位介绍 MySQL 日志实现内幕,可持续关注我们的推文哦!


REDO LOG 日志文件管理的用途

REDO LOG 是用来做数据库 crash recovery 的,这是数据库保障数据安全的重要功能之一。在数据库操作中,它保存了对 InnoDB 表中数据的修改记录,所以也叫日志文件。在 InnoDB 存储引擎中,一般默认包括 2 个日志文件,新建数据库之后,会有名为 ib_logfile0 和 ib_logfile1 的两个文件,如果在启动数据库时,这两个文件不存在,则 InnoDB 会根据配置参数或默认值,重新创建日志文件。

在 InnoDB 内部的日志管理中,一个很重要的概念是 LSN,全名叫 Log Sequence Number,它用来精确记录日志位置信息,且是连续增长的。在 InnoDB 中,大小为 8 个字节的值,它的增长量是根据一个 MTR(mini-transaction,后面会讲到)写入的日志量来计算的,写多少日志(单位字节),LSN 就增长多少。日志文件轮循一圈(所有日志文件是以循环方式使用的),那么 LSN 的增长量大约就是整个日志文件的大小(日志文件存在文件头等会占用一部分空间)。它是一个集逻辑意义与物理意义于一身的概念。而在有些数据库中,LSN 是一个完全逻辑的概念,每提交一个物理事务,LSN 就加 1。

上面提到,日志文件是以类似循环圈的方式使用的,如图 11.2 所示。

在 InnoDB 中,通过日志组来管理日志文件,是一个逻辑定义,包含若干个日志文件,一个组中的日志文件大小相等,大小通过参数来设置。现在 InnoDB 只⽀持一个日志组。在 MySQL5.5 及之前的版本中,整个日志组的容量不能大于 4GB(实际上是 3.9GB 多,因为还有一些文件头信息等),到了 MySQL 5.6.3 版本之后,整个日志组的容量可以设置得很大,最大可以达到 512GB。

REDO 日志的写入,都是字节连续的,虽然看上去是多个日志文件,但理解的时候,完全可以把它想象成一个文件,对每一文件掐头去尾,把剩下的空间连接起来,就是总的日志空间了。

日志组中的每一个日志文件,都有自己的格式,内部也是按照大小相等的页面切割,但这里的页面大小是 512 个字节,由于历史的原因,考虑到机械硬盘的块大小是 512 字节,日志块大小也如此设计。这是因为写日志其实就是为了提高数据库写入吞吐量,如果每次写入是磁盘块大小的倍数,效率才是最高的,并且日志将逻辑事务对数据库的分散随机写入转化成了顺序的 512 字节整数倍数据的写入,这样就大大提高了数据库的效率。正是因为这个原因,REDO 日志才可以说是数据库管理系统与通过直接写文件来管理数据的最根本的区别之一。

下面展示的是日志文件的格式。

需要注意的是,图 11.3 中第一列指的是每一项在页面中的偏移位置,而下一项则是这个值加上该值在页面中所占长度得到的值。

图 11.3 中展示的 4 个页面(2048 字节),主要用于管理日志内容及整个数据库状态。在这 2KB 内容之后,就是正常的用来存储日志内容的部分,也是按照 512 字节页面大小的方式存储,图 11.4 中展示的是正常日志页面的格式。

普通页面中,都会有 12 个字节用来存储页面头信息,这些信息主要用于管理这个页面本身的数据存储方式。

  • LOG_BLOCK_HDR_NO:4 个字节,一个与 LSN 有关系的块号。

  • LOG_BLOCK_HDR_DATA_LEN:2 个字节,表示当前页面中存储的日志长度,这个值一般都等于 512-12(12 为页面头的大小),因为日志在相连的块中是连续存储的,中间不会存在空闲空间,所以如果这个长度不为 500,表示此时日志已经扫描完成(Crash Recovery 的工作)。

  • LOG_BLOCK_FIRST_REC_GROUP:2 个字节,表示在当前块中是不是有一个 MTR(关于这个概念的意义,会在下一节中专门介绍)的开始位置。因为一个 MTR 所产生的日志量有可能是超过一个块大小的,那么如果一个 MTR 跨多个块时,这个值就表示了这个 MTR 的开始位置究竟是在哪一个块中。如果为 0,则表示当前块的日志都属于同一个 MTR;而如果其值大于 0 并且小于上面 LOG_BLOCK_HDR_DATA_LEN 所表示的值,则说明当前块中的日志是属于两个 MTR 的,后面 MTR 的开始位置就是 LOG_BLOCK_FIRST_REC_GROUP 所表示的位置。

  • LOG_BLOCK_CHECKPOINT_NO:4 个字节,存储的是检查点的序号。具体什么是检查点,后面会详细介绍。

上面所讲述的就是日志文件的组织结构,只有前面 2KB 是日志头,后面所有的都是一个个连续的、用来存储 MTR 产生的日志页面。

MTR InnoDB 物理事务

上面已经提到了关于 MTR 的概念,实际上,它是 InnoDB 存储引擎中一个很重要的用来保证物理页面写入操作完整性及持久性的机制。之所以被称为 MTR,是因为它的意义相当于一个 Mini-transaction,用 MTR 来表示,这里把它称作“物理事务”,这样叫是相对逻辑事务而言的。

对于逻辑事务,熟悉数据库的人都很清楚,它是数据库区别于文件系统最重要的特性之一,它具有 ACID 四个特性,用来保证数据库的完整性——要么都做修改,要么什么都不做。物理事务从名字来看,是物理的,因为在 InnoDB 存储引擎中,只要是涉及文件修改、文件读取等物理操作的,都离不开这个物理事务,可以说物理事务是 Buffer Pool 中的内存 Page 与文件之间的一个桥梁。

通过图 11.5 可以先了解一下 MTR 在 InnoDB 中的作用或意义。

从图 11.5 中可以看出,不管读还是写,只要使用到底层 Buffer Pool 的页面,都会使用到 MTR,它是上面逻辑层与下面物理层的交互窗口,同时也是用来保证下层物理数据正确性、完整性及持久性的机制。

前面已经介绍过 InnoDB 的页面 Buffer Pool 系统,已经知道在访问一个文件页面的时候,系统都会将要访问的页面载入到 Buffer Pool 中,然后才可以访问这个页面,此时可以读取或更新这个页面。在这个页面不断更新变化的过程中,有一个系统一直扮演着很重要的角色,那就是日志系统。因为 InnoDB 采用的也是 LOGWRITE-AHEAD,所以所有的写操作,都会有日志记录,这样才能保证数据库事务的 ACID 特性。

而写日志是一个物理操作,其实它也需要一个完整性。比如在底层页面插入一条记录,如果只修改页头信息而没有修改页尾信息,其实对于这个页面来说是不完整的,所以这个物理操作还是需要一个机制来保证它的完整性的。那么在 InnoDB 中,这个机制就是上面介绍的物理事务,因为它也是用来保证完整性的,所以也被称作“事务”。

物理事务既然被称为事务,那它同样有事务的开始与提交,物理事务的开始其实就是对物理事务结构体 mtr_struct 的初始化,其中包括下面一些成员。

  1. struct mtr_struct{

  2. /* memo stack for locks etc. */

  3. dyn_array_t memo;


  4. /* start lsn of the possible logs entry for this mtr */

  5. ib_uint64_t start_lsn;


  6. /* end lsn of the possible logs entry for this mtr */

  7. ib_uint64_t end_lsn;


  8. /* mini-transaction logs */

  9. dyn_array_t log;


  10. /* count of how many page initial logs records have been written to the mtr logs */

  11. ulint n_log_recs;


  12. ulint log_mode;

  13. };

分别介绍一下每个成员的意义,如下。

  • memo:是一个动态数组空间,用来存储所有这个物理事务用到(访问)的页面(实际上存储的就是 Buffer Pool 中管理页面的控制块结构 buf_block_t),这些页面都是被所属的物理事务上了锁的(读锁或者写锁,某些时候会不上锁)。这个锁是读写锁、页面锁,与逻辑事务中所说的表锁和行锁要区分开来。

  • log:也是一个动态数组空间,用来存储这个物理事务在访问修改数据页面的过程中产生的所有日志,这个日志就是数据库中经常说到的重做(REDO)日志。

  • n_log_recs:表示这个物理事务产生的日志量,单位为日志记录条数。

  • log_mode:表示这个物理事务的日志模式,包括 MTR_LOG_ALL(写日志)、MTR_LOG_NONE(不写日志)等。

  • start_lsn:表示这个物理事务开始前的 LSN。

  • end_lsn:表示这个物理事务提交后产生的新的 LSN。

首先,在系统将一个页面载入 Buffer Pool 的时候,需要一个新开始(mtr_start)或者一个已经开始的物理事务,载入时需要指定页面的获取方式,比如是用来读取的还是用来修改的,这样会影响物理事务对这个页面的上锁情况,如果用来修改,则上 X 锁,否则上 S 锁(当然还可以指定不上锁)。在确定了获取方式、页面的表空间 ID 及页面号之后,就可以通过函数 buf_page_get 来获取指定页面了,当找到相应页面后,物理事务就要对它上指定的锁,此时需要对这个页面的上锁情况进行检查,一个页面的上锁情况是在结构体 buf_block_struct 的 lock 中体现的,此时如果这个页面还没有上锁,这个物理事务就会直接对其上锁,否则还需要考虑两个锁的兼容性,只有两个锁都是共享锁(S)的情况下才可以上锁成功,否则需要等待。当上锁成功后,物理事务会将这个页面的内存结构存储到上面提到的 memo 动态数组中,然后这个物理事务就可以访问这个页面了。

物理事务对页面的访问包括两种操作,一种是读,另一种是写。读就是简单读取其指定页面内偏移及长度的数据;写则是指定从某一偏移开始写入指定长度的新数据。同时,如果这个物理事务是写日志的(MTR_LOG_ALL),此时还需要对刚才的写操作记下日志,这里的日志就是逻辑事务中提到的 REDO 日志。写下相应的日志之后,同样将其存储到上面的 log 动态数组中,同时要将上面结构体中的 n_log_recs 自增,维护这个物理事务的日志计数值。

物理事务的读写过程主要就是上面介绍的内容,其最重要的是它的提交过程。物理事务的提交是通过 mtr_commit 来实现的。在讲 mtr_commit 之前,先讲一下,什么时候该提交,内部是如何控制的。

这里首先需要知道的是,InnoDB 的 REDO 日志不完全是物理日志,它包含了部分逻辑意义在里面,比如插入一行记录时,MTR 记录的是在一个页面中写入这条记录,内容大致包括页面号、文件号(表空间号)及这条记录的值(包括每个列信息),这样就有了逻辑概念。需要注意的是,在做 REDO 恢复时,需要保证这个页面是正确的、完整的,不然这个 REDO 就会失败,这也正是 InnoDB 存储引擎中著名的 DOUBLEWRITE 存在的意义,不过这是后话。而如果是纯物理的 REDO,日志内容应该会拆得更散,比如还是插入一条记录,它会记录页面号、文件号(表空间号)、页面内偏移值,并且有多个这样的 REDO 记录,因为会涉及多个位置的修改操作,这就没有任何逻辑内容了。而针对一个插入操作,需要在一个页面内的不同位置写入不同的数据,当然如果是纯物理 REDO,相应地会产生多条 REDO 记录,这是物理与逻辑的简单区别。

再说 MTR 的提交,一个逻辑事务是由多个物理事务组成,用来保证数据库的 ACID 特性的,有这个就够了,所以物理事务可以保证一次物理修改是完整的。所谓一次物理修改,可以理解为一个底层的相对完整的写入操作,比如插入一条记录的过程中,会包括写一条回滚记录及插入时写入一个页面等,那么这些逻辑上是一个动作的物理写入,就可以被认为是一个独立的物理事务,也就是在写回滚记录时执行 mtr_start,写完之后执行 mtr_commit,真正插入时写一个页面也是同样的道理。

接着介绍 MTR 提交的细节。物理事务的提交主要是将所有这个物理事务产生的日志写入到 InnoDB 日志系统的日志缓冲区中,然后等待 srvmasterthread 线程定时将日志系统的日志缓冲区中的日志数据刷到日志文件中,这会涉及日志刷盘时机的问题,不过还是先来看看 MTR、日志缓冲区及日志文件之间的关系,如图 11.6 所示。

从图 11.6 中可以看出,左边的若干个 MTR 产生了各自的 REDO LOG,有些 MTR 已经提交了,有些正在写入。正在写入日志的 MTR,它们的日志都存储在自己 MTR 结构的 log 动态数组中,这个 MTR 还是不完整的,所以还是自己保存着,而对于那些已经提交的 MTR,它们对应的日志已经在提交的时候转存到了日志缓冲区中,相当于这些日志已经是实实在在地产生了,将来必然要占用数据库日志文件的一部分空间(除非数据库此时挂了)。

日志缓冲区的存储只是一个暂时的中间状态,日志缓冲区的大小可以通过参数 innodb_log_buffer_size 来设置,一般都比较小,存储不了太多的日志。因为已经提交并写入到日志缓冲的日志是确定的,所以它们是占用了 LSN 的,也就是说它们会使 LSN 变大。

最后提交的那个 MTR 代表着整个数据库最新的 LSN 值,也就是图 11.7 中所示的 Log Sequence number,这也正是在 MySQL 客户端中执行命令 show engine innodb status\G 时,返回的信息中 Log 模块中的第一行。

而日志缓冲区也是有大小的,当多个 MTR 提交时,缓冲区被占满了,那么此时系统会将日志缓冲区的日志刷到日志文件中(这里涉及的另一个问题就是日志刷盘时机,这里只是一种情况,其他的后面做专门介绍),为其他新的 MTR 释放空间。此时,日志的流向就是从中间的日志缓冲区向右边的日志文件转移,上面已经提到过,转移其实是平移,在缓冲区是什么内容,写入文件也是什么内容,也是完全连续的,且在日志文件中,还是一个个的 MTR 连续存储。

最新写入日志文件的那个 MTR 产生的 LSN 值(图 11.7 中所示的 Log flushed up to),其实就是图 11.7 中所示的 Log 状态的第二行,也就是日志最新写入文件的 LSN 值,这个值的意义很重大,表示的是,到这个 LSN 为止,所有的修改都是完整的了,如果此时数据库挂了,写到这个位置的数据都是可以恢复的,而不需要去关心 Buffer 页面是不是被刷到磁盘。但此时在日志缓冲区中的日志所对应的操作就丢失了,这里是否会丢失事务数据与参数 innodb_flush_log_at_trx_commit 有关系,如果将参数 innodb_flush_log_at_trx_commit 设置为 1,当前事务的提交肯定会将日志缓冲区中的日志刷到日志文件中;如果设置为 2,那么日志只是写入了操作系统缓存,并没有写入磁盘,那么此时有可能丢失部分已经提交的事务,丢失多少由操作系统决定,这种情况下,即使数据库挂了,只要机器不挂,就问题不大,因为操作系统还会将它对应的缓存写入磁盘;但如果设置为 0 的话,就无能为力了,因为 InnoDB 只负责将事务对应的日志写入到日志缓冲区中,无论是操作系统,还是数据库,都不能保证日志的安全性,所以最好不要设置成这样。

进一步而言,日志文件的大小也是有限的,不可能无限量地将日志写入日志文件中。前面已经提到过,它是循环使用的,如果日志写入的头(图 11.7 中所示的Log flushed up to)和尾相遇了,此时日志就不能再写入了,因为如果再写入的话,就要“追尾”了,这样会将之前产生的日志覆盖掉,导致日志不可用,不完整。此时就会使用一种机制来保证新的日志还能继续写入,尾部日志还是完整的,这个机制叫作 checkpoint(检查点)。

说白了,日志产生的作用,是将随机页面的写入变成顺序日志的写入,从而用一个速度更快的写入来保证速度较慢的写入的完整性,以提高整体数据库的性能。其根本目的是要将随机变成顺序,所以日志的量才是一个相对固定循环使用的空间。有了这个思想之后,使用检查点来保证日志的重复写入、数据库完整性就是顺其自然的事情。

使用检查点来保证数据库完整性的主体思想,主要是让日志失效,也就是让 Buffer Pool 中的页面修改写入到磁盘上面。因为日志的存在实际上就是让 Buffer Pool 中的 Page 尽可能少地刷磁盘,尽可能长时间地将页面数据缓存起来,尽可能提高访问速度,因为不管如何修改,Buffer Pool 中的页面都是最新的,只是不一定写入磁盘中(没有刷入没关系,由日志来保证)。如果日志文件大小不够用,此时只要将 Buffer Pool 中的某些页面刷入到磁盘中,其对应的日志就失效了,因为这些日志就是用来保证 Page 没有刷入时但数据库挂了的情况下数据库的完整性的,而这些 Page 如果已经写入磁盘了,相应的日志也就没有用了,这就是检查点的根本意义所在。

而上面提到的,做检查点时,只是将某些页面刷入磁盘,其中的”某些”是有讲究的。俗话说:“家有三件事,先从紧处来”,现在的问题是日志空间不够用了,而日志是循环使用的,必须是按照顺序,不能跳着写,所以最主要的是从 LSN 值最小的日志开始,按照从小到大的顺序不断地让这些日志失效。每次做检查点都会有一个比例,此时系统会根据最小的有效 LSN(min_valid_lsn)和检查点处理的日志比例计算出最大的将要失效的 LSN 值(取名叫 lsn_checkpoint_up_to)。计算完之后,再去扫描 Buffer Pool 的 flushlist 链表,找出所有被更新过的页面中,曾经修改这些页面的 MTR 对应的 LSN 中的最小值(因为一个页面有可能被多次修改,但只需要考虑最小的 LSN 的那一次,使用的是前面介绍结构 buf_block_t 时,这里面所存储的 oldest_modification 的值),如果这个值比 lsn_checkpoint_up_to 值小,就将这个页面刷入磁盘,也就是说,如果将小于 lsn_checkpoint_up_to 的 MTR 修改过的页面都刷入磁盘了,那么日志文件中在 LSN 值 lsn_check_point_up_to 以前的日志就都可以失效了,那么在整个日志文件空间中,从 min_valid_lsn 到 lsn_checkpoint_up_to 之间的空间,又可以被重新使用了,直接覆盖即可,而不会导致数据库不完整、数据丢失等问题。

上面讲的整个检查点过程,用一个更形象的图表示,如图 11.8 所示。

此时,再接着上面 MTR 产生日志的图 11.6 来讲,上面找到的日志文件的位置 lsn_checkpoint_up_to 就是图 11.7 中所示的 Last checkpoint at,也是上面命令 show engine innodb status\G 中关于 Log 部分的第四行信息。而从这个点开始到最新的已经刷盘的日志文件位置 Log flushed up to 之间的日志都是有效日志了,不能被覆盖,只有空间又不够用了的情况下,再将最小的有效日志位置向前推,产生新的位置,像这样不断循环,周而复始的工作,这就是日志、Buffer Pool 及检查点之间的工作原理。

上面提到的 show engine innodb status\G 命令生成的 Log 部分中显示的第三行信息,是 Buffer Pool 中 Page 刷盘时刷到的一个最新的 LSN。但此时检查点的最新点不一定做得及时,所以它是大于等于第四行的,而图 11.7 中所示的四行对应的值,从上到下以递减的顺序排序,其中的道理都已经非常明确了。

上面已经讲过,物理事务和逻辑事务一样,也是可以保证数据库操作的完整性的。一般说来,一个操作必须要在一个物理事务中完成,也就是说要么这个操作已经完成,要么什么也没有做,否则就有可能造成数据不完整的问题,因为在数据库系统做 REDO 操作时是以一个物理事务为单位做的,如果一个物理事务的日志是不完整的,则它对应的所有日志都不会重做。那么,如何辨别一个物理事务是否完整呢?这个问题是在物理事务提交时用了一个很巧妙的方法来保证的。在提交前,如果发现这个物理事务有日志,则在日志最后再写一些特殊的日志,这些特殊的日志就是一个物理事务结束的标志,提交时一起将这些特殊的日志写入,在重做时如果当前这一批日志信息最后面存在这个标志,则说明这些日志是完整的,否则就是不完整的,就不会重做。

物理事务提交时还有一项很重要的工作就是处理上面结构体中动态数组 memo 中的内容,现在已经知道这个数组中存储的是这个物理事务访问过的所有页面,并且都已经上了锁。在它提交时,如果发现这些页面中已经有被修改过的,这些页面就成了脏页,这些脏页需要被加入到 InnoDB Buffer Pool 中的更新链表中(讲 BUFFER 时已经讲过)。当然,如果已经在更新链中,则直接跳过(不能重复加入),svr_master_thread 线程会定时检查这个链表,将一定数目的脏页刷到磁盘中,加入之后还需要将这个页面上的锁释放掉,表示这个页面已经处理完成;如果页面没有被修改,或者只是用来读取数据的,则只需要直接将其共享锁(S 锁)释放掉即可。

上面的内容就是物理事务的一个完整的讲述,它是比较底层的一个模块,牵扯的东西比较多,这里重点讲述了物理事务的意义、操作原理、与 BUFFER 系统的关联、日志的产生等内容。

下面继续讲述有关日志的其他内容。

【END】



文章转载自Qunar技术沙龙,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论