《MySQL运维内参》节选 | InnoDB日志管理机制(五)

社区广播:运维派(Yunweipai.com)是国内最早成立的IT运维社区,欢迎大家投稿,让运维人不再孤寂的成长!

前面几篇连载讲述了Buffer Pool以及日志的基础内容,接下来,讲述的是基于日志的想象,理解,以及它的存储格式。

日志的意义在哪里?

上面已经讲述过,日志是在逻辑事务对数据库做DML操作时,其所包含的物理事务MTR所记录的针对所有涉及到的Buffer Pool页面的修改记录。

为了更好的讲述日志的意义,这里通过下面几个方面来更好的说明。

假如没有写日志

假如没有写日志,那数据库在做了任何修改之后,必须要直接将Buffer Page刷磁盘,不然如果此时数据库挂了,即使事务被提交了,这些修改还是没法恢复。这将带来的灾难是,IO大量增加,这个时候的数据库,相当于是一个简单的文件系统,无论写什么数据,都必须马上刷入磁盘,Buffer Pool的作用可能只是一个用来修改文件页面的临时缓存而已。

假如没有写日志,在数据库做了DML操作之后,数据库可能在事务没有提交时就将Buffer Page刷到磁盘了,但此时需要回滚,而我们知道,回滚段的内容也是通过Buffer Pool管理的,它的每个页和B树页面是一样的,只是作用不一样而已,由此可知,回滚段数据也是通过REDO日志来保证完整性的。那么如果没有了日志,Buffer Page中的回滚段页面也需要直接写入,没有了任何缓存,性能会非常低。

假如没有写日志,数据库在关闭(挂掉)后再启动时,就不需要做REDO操作了(因为没有写日志),但需要做UNDO操作,因为UNDO不是通过REDO来恢复的,而是自己写入了(假设每次写Buffer Page之后都直接刷盘了),所以回滚段是有效的,还可以让没有提交的事务回滚掉(因为如果一个事务修改的页面很多的话,肯定会有一部分页面先刷掉的,所以有可能需要回滚),勉强还可以保证数据库的完整性。

综合上面的假设,现在已经明白,日志的作用就是用来保证Buffer Pool页面的数据写入不丢失,反过来说,如果每个Buffer Pool中的Page每次都刷入到磁盘了,这样就不需要REDO日志了,此时这个数据库就是一个文件系统了,因为Buffer Pool每次都刷,相当于每次写完直接写文件。所以说,日志,是数据库管理系统与文件系统的最核心的区别。

所以如果没有日志,数据库的性能就低到完全没法用了,因为IO太大了,同时,这种IO操作都是随机写入,很容易导致IO到达瓶颈,所以为了提高数据库性能,就必须要使用到REDO日志机制。

使用日志能提高性能的关键原因,有以下三方面:

1. 因为日志是用来记录Buffer Pool中Page的修改记录的,把对Page的写入转化为对日志的写入,那此时Page就不需要每次都刷盘,写Page页面只需要在内存中写入即可,性能会非常好。

2. 通常,一个页面是16K,如果不写日志的话,每次的写入单位还是16K,即使修改很少量的数据,也是如此,这样会导致无效IO非常严重,反过来说,也只有通过日志机制,才能真正体现出真实写入的数据量,不会存在对IO的浪费,Page的刷盘数量会大大减少。

3. 如果没有日志就会每次都刷Page,而这些Page的相对位置是乱的,并不是顺序的,刷盘大多都是随机IO,这对于机械硬盘,性能是非常差的,而有了日志,巧妙的将随机IO转化成了日志的顺序IO,这将大大的提高IOPS,性能会非常好。

日志文件大小区别

使用日志对数据库的性能有很大的影响,那日志还有什么其它的因素会影响数据库的性能呢?那就是日志文件空间容量。

现在已经知道,日志在设置好后其容量是固定的,它是循环使用的,如果不够用了,引发的事件是做一个检查点,让最小有效的LSN向前推,让出一部分空间给新产生的日志来使用,也就是说,只要这个日志空间未用完,那么Buffer Pool中的Page将会一直不刷盘(因为还有其它的刷盘时机,所以这里单指因为日志不够用导致检查点的刷盘),任何修改都是在内存中发生的,那么下面做一个计算。

假如当前日志容量设置为128M,某一个DML操作只针对某一行记录一直做修改操作。每次操作产生日志量为1KB(包括Buffer中数据页面的修改及UNDO记录的产生),这样算下来,128M的日志量可以容纳对这条记录的131072(128M/1K)次修改,也就是说,在这么多次修改之后,这个页面才需要刷盘,才会产生一次随机刷盘操作。而如果把日志文件设置为1280M,很容易知道,这将容纳对这条记录的1310720次修改,这么多次修改只产生一次随机刷盘操作,而如果还是128M的话,需要10次随机刷盘。很明显,日志容量对数据库的性能还是有很大影响的。

日志

但也不是设置的越大越好,这里有两点需要注意:

1. 如果设置的非常大,固然性能可能会很好,但如果某一天(真的有可能到来),数据库异常挂了,此时可能有很多的日志都没有刷盘,也就是Log flushed up to与Last checkpoint at两个值之间相差太多,恢复起来,可能需要比较长的时间。但这个一般问题不大,本身挂的机率不大,同时REDO日志的恢复是顺序的,都是根据页面号的大小排序恢复的,所以比较快。同时在以后的MySQL版本中,会有多线程REDO恢复(听说的),这样就更快了,所以这一点不需要太多担心。

2. 日志容量大小的设置,最好要与Buffer Pool的总大小匹配,假如日志容量太小,Buffer Pool太大,那么这样的一个后果是导致Buffer Pool频繁做检查点,大的Buffer Pool不能被好好的利用。如果是日志容量很大,而Buffer Pool很小,此时Buffer Page经常会被淘汰出去,增加了IO频次,同时如果数据库意外挂掉,Buffer Pool小的话恢复起来也会比较慢。一般情况下,Buffer Pool的总大小与日志容量的大小比例最好保持在10~5:1的范围内。

日志的格式

前面已经讲述了太多的日志相关的内容了,这一节将要讲一下,具体到一个日志记录时,它是如何组织的,一条日志记录,究竟存储了什么?在这里都会说清楚。

前面已经讲到,InnoDB的日志是具有逻辑意义的物理日志,所以日志记录的格式就不完全是物理信息,而是有一定的逻辑意义,首先看一个基本的格式,如下图所示:

格式

图中各个列代表的意义如下:

  1. Type:日志类型,是一个日志记录的最高位,只占一个字节的空间。
  2. Space:表空间ID值,如果是系统页面(UNDO页面,或者是字典表页面),则是0,如果是索引页面,则是这个索引所在的表空间ID值。
  3. Offset:在上面Space所指定的文件中的页面号,以页面大小为单位,它是第几个页面(从0开始计数),则这个Offset就是几。
  4. Data:表示这条日志记录对应的数据,这个数据是不确定的,根据不同的Type值而不同,分别具有自己的格式。

Type类型有很多,下面列举一些在InnoDB中比较常用的类型,并简单做一些解释,以便可以更好的理解。InnoDB中的REDO,究竟是在做什么?究竟是存储了什么内容?功能是什么?知道每个类型之后,这些问题也就清楚了。

注:下面讲到的数据记录,都是以Compact格式的记录为对象的,其它类型这里不考虑。

  1. MLOG_(1,2,4,8)BYTE: 这四个类型,表示要在某一个位置,写入一个(两个、四个、八个)字节的内容,在日志记录中,Type分别是MLOG_1BYTE(MLOG_2BYTES、MLOG_4BYTES、MLOG_8BYTES),Space就是对应的表空间ID,Offset对应的是页面号,在Data中,还需要存储三个(四个、六个、十个)字节,前两个为要写入的数据在页面内的偏移值,因为页面大小为16K,所以需要用两个字节来存储,而后面才是真正需要写入的数据,占一个(两个、四个、八个)字节,这就是关于这个类型的日志的完整内容。
  2. MLOG_WRITE_STRING:这种类型的日志,其实和MLOG_1BYTE是类似的,只是MLOG_1BYTE是要写一个固定长度的数据,而MLOG_WRITE_STRING是要写一段变长的数据,Data部分的格式,首先用2个字节存储在页面中的写入位置,然后是2个字节的写入数据长度,最后是存储指定长度的字符串。
  3. MLOG_COMP_REC_MIN_MARK:这个类型的日志,是在将一条记录设置为页面中的最小记录(这个涉及到页面管理的内容,在一个页面中只有一个最小记录,它指向的是B树下一层的最左边的节点)时产生的,因为只是打个标记,存储内容比较简单,除了基本的日志头外,在Data内容中只存储了这条最小记录在页面内的偏移位置。
  4. MLOG_UNDO_INSERT:这个类型的日志,是用来保证一个插入操作可以在事务没有提交的情况下回滚时用的,在插入一条记录时,不止要写一个插入操作的日志(类型为MLOG_REC_INSERT,后面会着重介绍),还要写一个针对这个操作的回滚记录。我们已经知道,回滚记录的写入,其实也是向ibdata文件中写入数据,同样也是写在Buffer Pool中的,这个操作对应的REDO日志,就是当前介绍的MLOG_UNDO_INSERT类型的日志,在数据库恢复时,只有这个REDO日志做完了,相应的UNDO记录才有效(存在),如果对应的事务没有提交,会通过这个回滚记录将这个插入操作回滚掉,这也正是为什么REDO必须要在UNDO之前执行的原因,这是后话。至于这种类型的日志格式是什么样子的,与前面所说类型的区别还是在Data上面。前面两个字节存储的是回滚记录的长度,接着就是回滚记录的完整数据,不包括回滚记录前后各两个字节的指针信息,具体到回滚记录的格式,后面会讲述。
  5. MLOG_INIT_FILE_PAGE:这个类型的日志比较简单,只有前面的基本头信息,没有Data部分,因为在InnoDB中,初始化一个页面,所有的信息都是固定的,没有额外的处理,只要表明初始化哪一个位置的页面就好了,所以没有Data部分。这里初始化页面所做的操作,只涉及到对页面中文件管理方面的信息,比如这个页面的页面号,文件号(表空间ID)等信息,这个与后面将要介绍的MLOG_COMP_PAGE_CREATE是不同的,这个属于页面管理的文件信息部分的初始化,而MLOG_COMP_PAGE_CREATE属于页面的索引、数据存储方面的管理信息的初始化。后者是在前者的基础上做的。
  6. MLOG_COMP_PAGE_CREATE:这个类型的日志,其实和上面已经说过的MLOG_INIT_FILE_PAGE是一样的道理,因为在Buffer中创建一个新的可以使用的页面是固定的,只需要存储一个类型及要创建的页面的位置即可。创建一个页面所做的操作,包括初始化页面头信息,创建页面中最小记录与最大记录,初始化页面中记录数、HEAP大小、HEAP首地址及槽信息,初始化之后,这个页面就可以在B树中使用了,它是一个页面在没有插入任何数据时的状态。
  7. MLOG_MULTI_REC_END:这个类型的日志是非常特殊的,它只起一个标记的作用,其存储的内容只有占一个字节的类型值。在前面介绍MTR时说到,一个MTR所写的日志,要么全部写入,要么全部不写入,如何保证这个原子性就是通过这个类型的日志来实现的,每次MTR提交时,都会在后面加上这个日志记录,用来表示这个MTR已经结束了。只有在恢复的时候才会使用到它,在分析MTR时,只有找到这个日志,前面的日志才会去做REDO,做完之后,再向后扫描找到这个日志,然后再去REDO,如此反复,如果有一次找不到了,则说明日志文件是不完整的,已经扫描到的REDO日志就不会去执行了,从而保证了已经执行的MTR每个都是完整的。
  8. MLOG_COMP_REC_CLUST_DELETE_MARK: 这个类型的日志是表示,需要将聚集索引中的某一个记录打上删除标志。因为,众所周知,在InnoDB数据库中聚簇索引的删除在没有提交之前,只是打了一个删除标志而已。这个类型的日志记录内容,除了基本的内容之外,其Data数据的组成主要包括,两个字节的索引列的个数n,两个字节的唯一索引的个数u。接下来存储的是所有索引列的长度信息,每个列用2个字节存储,占用空间2*n个字节,然后,再存储索引中两个系统列信息,分别是TRXID在索引中的列位置信息、ROLLPTR的值及TRXID的值,最后再存储当前要删除记录在所在页面中的偏移值,也就是那条记录的头指针信息。这个类型的日志,存储的内容比较复杂,其Data部分使用下图来简明表示一下:

Data

  1. 这里有一个奇怪的地方是,在给一个记录打删除标志时,为什么不使用这条记录的主键值来直接定位,而是使用了一些在定位记录时被认为是没有用的东西呢?因为如果需要数据恢复了,只需要找到这行记录的主键信息,就可以重新给这个记录打删除标志,那为什么存储的都是一些索引的定义信息,比如,索引列个数,唯一键列个数,每个列的长度等,关于这个问题,是因为这里InnoDB还需要考虑其自身的问题,那就是它的REDO日志是半逻辑半物理的,在恢复时,不能保证对应的数据字典是可用的(因为数据字典的正确性还是需要REDO来保证),所以这个日志就会记录一些索引的信息,在恢复时使用这些信息来构造一个LOG_DUMMY表及LOG_DUMMY索引,然后再用这个表和索引来辅助这个REDO日志的执行,这样真正的表及索引可以不正确(暂时的),因为此时是不需要它们的。综合上面所述的日志Data部分,就可以知道这条记录的确切信息了,也就可以对它加删除标志了。
  2. MLOG_COMP_REC_UPDATE_IN_PLACE:这个类型的日志,和上面的MLOG_COMP_REC_CLUST_DELETE_MARK基本是差不多的,只是这个会在最后存储原地更新后的记录信息,包括所有被更新的列的信息,存储方式是:前面用1个字节或2个字节来存储长度,后面跟着的是更新后的数据,直到记录所有的列为止。
  3. MLOG_COMP_REC_DELETE:在InnoDB中,删除数据是通过打删除标志来实现的,但是,在事务提交后做Purge操作时,这条记录始终是要被删除的,所以还存在一个真正的将数据记录删除的操作,那这个类型的日志就是用来记录这个动作的。不过这个日志需要记录的内容也比较少,除了基本的日志头信息之外,在Data中只需要存储这条记录在页面内的偏移即可。那么在恢复数据时,这种类型的日志的作用就是会将这个页面中的对应的记录直接删除掉,而不再是打删除标记那么简单了。
  4. MLOG_COMP_PAGE_REORGANIZE:这个类型的日志,表示的是要重组指定的页面,其记录的内容也是很简单的,只需要存储要重组哪一个页面即可,没有Data部分。在恢复的时候,找到这个页面,对其中的数据碎片做整理,将页面内部的记录一条条向前移,将原来记录之间不能再被使用的空间收回合在一起变成一块连续的空间,这样原来貌似已经满了的页面,又可以插入新的数据了,这个就是表的碎片整理过程。
  5. MLOG_COMP_REC_INSERT:这个类型是在插入一条记录时产生的,它的产生过程可能存在一点争议,这里重点说一下。首先当然还是基本的日志头信息,然后存储的是被插入记录在页面内的偏移信息,然后就是关于索引的信息,这些都与上面MLOG_COMP_REC_CLUST_DELETE_MARK类型日志的内容相同,然而,再后面,所存储的信息就比较复杂了。首先会计算出,当前要插入的记录与前一条记录第一个不相同的字节的位置,然后在日志中记录的是,从这个位置开始到当前记录结束位置之间的数据,当然还有一些其它的信息,比如第一个不相同的字节的位置信息等。这里主要想说的是它这个设计方式,简单一点说,就是当前记录中存储的只是记录的后半部分数据,前半部分数据依赖的是前一条记录,这样存储会比存储整个记录省多少空间呢?最主要的是,这需要依赖插入数据之间的相关性,如果非常像,则可能会省一些,否则效果可能不明显。

日志

上面讲述的是,InnoDB存储引擎中,REDO日志的一部分类型,并对不同类型做了解释。从解释中可以看到,基本上每一个类型其实都是具有逻辑意义的,与DML相关的类型中,不是存储了列数据,就是存储了记录在页面内的偏移等信息。

这样做的优点有:

  1. 可以写REDO解析工具,去做一个第三方的同步工具,或者了解数据库做了什么操作,类似Binlog内容,但侧重点不同。
  2. 日志占用空间比全物理日志少。

最大的缺点就是系统首先要保证日志对应的页面的正确性,否则会造成逻辑日志执行不成功,或者造成数据的不一致等问题,这个问题在InnoDB中的解决方式,就是后面介绍的Double Write机制。

本文就此结束,接下来继续连载,讲述有关日志的其他内容。

文章来自微信公众号:DBAce

网友评论comments

发表评论

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

  1. 涂盛霞说道:

    作者写的很好,最近想好好研究InnoDB日志这一块,这几篇博客就拿来研读了,是否还有资源可以分享

Copyright © 2012-2017 YUNWEIPAI.COM - 运维派 - 粤ICP备14090526号-3
扫二维码
扫二维码
返回顶部