简介: 数据库故障复原机制的前世今生一文中提到,今生磁盘数据库为了在保障数据库的原子性(A, Atomic) 和持久性(D, Durability)的同时,还能以灵便的刷盘策略来充分利用磁盘程序写的性能,会记录REDO和UNDO日志,即ARIES办法。本文将重点介绍REDO LOG的作用,记录的内容,组织构造,写入形式等内容,心愿读者可能更全面精确的了解REDO LOG在InnoDB中的地位。本文基于MySQL 8.0代码。

作者 | 瀚之
起源 | 阿里技术公众号

数据库故障复原机制的前世今生一文中提到,今生磁盘数据库为了在保障数据库的原子性(A, Atomic) 和持久性(D, Durability)的同时,还能以灵便的刷盘策略来充分利用磁盘程序写的性能,会记录REDO和UNDO日志,即ARIES办法。本文将重点介绍REDO LOG的作用,记录的内容,组织构造,写入形式等内容,心愿读者可能更全面精确的了解REDO LOG在InnoDB中的地位。本文基于MySQL 8.0代码。

一 为什么须要记录REDO

为了获得更好的读写性能,InnoDB会将数据缓存在内存中(InnoDB Buffer Pool),对磁盘数据的批改也会落后于内存,这时如果过程或机器解体,会导致内存数据失落,为了保障数据库自身的一致性和持久性,InnoDB保护了REDO LOG。批改Page之前须要先将批改的内容记录到REDO中,并保障REDO LOG早于对应的Page落盘,也就是常说的WAL,Write Ahead Log。当故障产生导致内存数据失落后,InnoDB会在重启时,通过重放REDO,将Page复原到解体前的状态。

二 须要什么样的REDO

那么咱们须要什么样的REDO呢?首先,REDO的保护减少了一份写盘数据,同时为了保证数据正确,事务只有在他的REDO全副落盘能力返回用户胜利,REDO的写盘工夫会间接影响零碎吞吐,不言而喻,REDO的数据量要尽量少。其次,零碎解体总是产生在始料未及的时候,当重启重放REDO时,零碎并不知道哪些REDO对应的Page曾经落盘,因而REDO的重放必须可重入,即REDO操作要保障幂等。最初,为了便于通过并发重放的形式放慢重启复原速度,REDO应该是基于Page的,即一个REDO只波及一个Page的批改。

相熟的读者会发现,数据量小是Logical Logging的长处,而幂等以及基于Page正是Physical Logging的长处,因而InnoDB采取了一种称为Physiological Logging的形式,来兼得二者的劣势。所谓Physiological Logging,就是以Page为单位,但在Page内以逻辑的形式记录。举个例子,MLOG_REC_UPDATE_IN_PLACE类型的REDO中记录了对Page中一个Record的批改,办法如下:

(Page ID,Record Offset,(Filed 1, Value 1) ... (Filed i, Value i) ... )

其中,PageID指定要操作的Page页,Record Offset记录了Record在Page内的偏移地位,前面的Field数组,记录了须要批改的Field以及批改后的Value。

因为Physiological Logging的形式采纳了物理Page中的逻辑记法,导致两个问题:

1、须要基于正确的Page状态上重放REDO

因为在一个Page内,REDO是以逻辑的形式记录了前后两次的批改,因而重放REDO必须基于正确的Page状态。然而InnoDB默认的Page大小是16KB,是大于文件系统能保障原子的4KB大小的,因而可能呈现Page内容胜利一半的状况。InnoDB中采纳了Double Write Buffer的形式来通过写两次的形式保障复原的时候找到一个正确的Page状态。这部分会在之后介绍Buffer Pool的时候具体介绍。

2、须要保障REDO重放的幂等

Double Write Buffer可能保障找到一个正确的Page状态,咱们还须要晓得这个状态对应REDO上的哪个记录,来防止对Page的反复批改。为此,InnoDB给每个REDO记录一个全局惟一递增的标号LSN(Log Sequence Number)。Page在批改时,会将对应的REDO记录的LSN记录在Page上(FIL_PAGE_LSN字段),这样复原重放REDO时,就能够来判断跳过曾经利用的REDO,从而实现重放的幂等。

三 REDO中记录了什么内容

晓得了InnoDB中记录REDO的形式,那么REDO里具体会记录哪些内容呢?为了应答InnoDB各种各样不同的需要,到MySQL 8.0为止,曾经有多达65种的REDO记录。用来记录这不同的信息,复原时须要判断不同的REDO类型,来做对应的解析。依据REDO记录不同的作用对象,能够将这65中REDO划分为三个大类:作用于Page,作用于Space以及提供额定信息的Logic类型。

1、作用于Page的REDO

这类REDO占所有REDO类型的绝大多数,依据作用的Page的不同类型又能够细分为,Index Page REDO,Undo Page REDO,Rtree PageREDO等。比方MLOG_REC_INSERT,MLOG_REC_UPDATE_IN_PLACE,MLOG_REC_DELETE三种类型别离对应于Page中记录的插入,批改以及删除。这里还是以MLOG_REC_UPDATE_IN_PLACE为例来看看其中具体的内容:

其中,Type就是MLOG_REC_UPDATE_IN_PLACE类型,Space ID和Page Number惟一标识一个Page页,这三项是所有REDO记录都须要有的头信息,前面的是MLOG_REC_UPDATE_IN_PLACE类型独有的,其中Record Offset用给出要批改的记录在Page中的地位偏移,Update Field Count阐明记录里有几个Field要批改,紧接着对每个Field给出了Field编号(Field Number),数据长度(Field Data Length)以及数据(Filed Data)。

2、作用于Space的REDO

这类REDO针对一个Space文件的批改,如MLOG_FILE_CREATE,MLOG_FILE_DELETE,MLOG_FILE_RENAME别离对应对一个Space的创立,删除以及重命名。因为文件操作的REDO是在文件操作完结后才记录的,因而在复原的过程中看到这类日志时,阐明文件操作曾经胜利,因而在复原过程中大多只是做对文件状态的查看,以MLOG_FILE_CREATE来看看其中记录的内容:

同样的前三个字段还是Type,Space ID和Page Number,因为是针对Page的操作,这里的Page Number永远是0。在此之后记录了创立的文件flag以及文件名,用作重启复原时的查看。

3、提供额定信息的Logic REDO

除了上述类型外,还有多数的几个REDO类型不波及具体的数据批改,只是为了记录一些须要的信息,比方最常见的MLOG_MULTI_REC_END就是为了标识一个REDO组,也就是一个残缺的原子操作的完结。

4、REDO是如何组织的

所谓REDO的组织形式,就是如何把须要的REDO内容记录到磁盘文件中,以不便高效的REDO写入,读取,复原以及清理。咱们这里把REDO从上到下分为三层:逻辑REDO层、物理REDO层和文件层。

1 逻辑REDO层

这一层是真正的REDO内容,REDO由多个不同Type的多个REDO记录收尾相连组成,有全局惟一的递增的偏移sn,InnoDB会在全局log_sys中保护以后sn的最大值,并在每次写入数据时将sn减少REDO内容长度。如下图所示:

2 物理REDO层

磁盘是块设施,InnoDB中也用Block的概念来读写数据,一个Block的长度OS_FILE_LOG_BLOCK_SIZE等于磁盘扇区的大小512B,每次IO读写的最小单位都是一个Block。除了REDO数据以外,Block中还须要一些额定的信息,下图所示一个Log Block的的组成,包含12字节的Block Header:前4字节中Flush Flag占用最高位bit,标识一次IO的第一个Block,剩下的31个个bit是Block编号;之后是2字节的数据长度,取值在[12,508];紧接着2字节的First Record Offset用来指向Block中第一个REDO组的开始,这个值的存在使得咱们对任何一个Block都能够找到一个非法的的REDO开始地位;最初的4字节Checkpoint Number记录写Block时的next_checkpoint_number,用来发现文件的循环应用,这个会在文件层具体解说。Block开端是4字节的Block Tailer,记录以后Block的Checksum,通过这个值,读取Log时能够明确Block数据有没有被残缺写完。

Block中残余的两头498个字节就是REDO真正内容的寄存地位,也就是咱们下面说的逻辑REDO。咱们当初将逻辑REDO放到物理REDO空间中,因为Block内的空间固定,而REDO长度不定,因而可能一个Block中有多个REDO,也可能一个REDO被拆分到多个Block中,如下图所示,棕色和红色别离代表Block Header和Tailer,两头的REDO记录因为前一个Block残余空间有余,而被拆分在间断的两个Block中。

因为减少了Block Header和Tailer的字节开销,在物理REDO空间中用LSN来标识偏移,能够看出LSN和SN之间有简略的换算关系:

constexpr inline lsn_t log_translate_sn_to_lsn(lsn_t sn) {  return (sn / LOG_BLOCK_DATA_SIZE * OS_FILE_LOG_BLOCK_SIZE +          sn % LOG_BLOCK_DATA_SIZE + LOG_BLOCK_HDR_SIZE);}

SN加上之前所有的Block的Header以及Tailer的长度就能够换算到对应的LSN,反之亦然。

3 文件层

最终REDO会被写入到REDO日志文件中,以ib_logfile0、ib_logfile1...命名,为了防止创立文件及初始化空间带来的开销,InooDB的REDO文件会循环应用,通过参数innodb_log_files_in_group能够指定REDO文件的个数。多个文件收尾相连程序写入REDO内容。每个文件以Block为单位划分,每个文件的结尾固定预留4个Block来记录一些额定的信息,其中第一个Block称为Header Block,之后的3个Block在0号文件上用来存储Checkpoint信息,而在其余文件上留空:

其中第一个Header Block的数据区域记录了一些文件信息,如下图所示,4字节的Formate字段记录Log的版本,不同版本的LOG,会有REDO类型的增减,这个信息是8.0开始才退出的;8字节的Start LSN标识以后文件开始LSN,通过这个信息能够将文件的offset与对应的lsn对应起来;最初是最长32位的Creator信息,失常状况下会记录MySQL的版本。

当初咱们将REDO放到文件空间中,如下图所示,逻辑REDO是真正须要的数据,用sn索引,逻辑REDO按固定大小的Block组织,并增加Block的头尾信息造成物理REDO,以lsn索引,这些Block又会放到循环应用的文件空间中的某一地位,文件中用offset索引:

尽管通过LSN能够惟一标识一个REDO地位,但最终对REDO的读写还须要转换到对文件的读写IO,这个时候就须要示意文件空间的offset,他们之间的换算形式如下:

const auto real_offset =      log.current_file_real_offset + (lsn - log.current_file_lsn);

切换文件时会在内存中更新以后文件结尾的文件offset,current_file_real_offset,以及对应的LSN,current_file_lsn,通过这两个值能够不便地用下面的形式将LSN转化为文件offset。留神这里的offset是相当于整个REDO文件空间而言的,因为InnoDB中读写文件的space层实现反对多个文件,因而,能够将首位相连的多个REDO文件看成一个大文件,那么这里的offset就是这个大文件中的偏移。

五 如何高效地写REDO

作为保护数据库正确性的重要信息,REDO日志必须在事务提交前保障落盘,否则一旦断电将会有数据失落的可能,因而从REDO生成到最终落盘的残缺过程成为数据库写入的要害门路,其效率也间接决定了数据库的写入性能。这个过程包含REDO内容的产生,REDO写入InnoDB Log Buffer,从InnoDB Log Buffer写入操作系统Page Cache,以及REDO刷盘,之后还须要唤醒期待的用户线程实现Commit。上面就通过这几个阶段来看看InnoDB如何在高并发的状况下还能高效地实现写REDO。

1 REDO产生

咱们晓得事务在写入数据的时候会产生REDO,一次原子的操作可能会蕴含多条REDO记录,这些REDO可能是拜访同一Page的不同地位,也可能是拜访不同的Page(如Btree节点决裂)。InnoDB有一套残缺的机制来保障波及一次原子操作的多条REDO记录原子,即复原的时候要么全部重放,要不全副不重放,这部分将在之后介绍复原逻辑的时候具体介绍,本文只波及其中最根本的要求,就是这些REDO必须间断。InnoDB中通过min-transaction实现,简称mtr,须要原子操作时,调用mtr_start生成一个mtr,mtr中会保护一个动静增长的m_log,这是一个动态分配的内存空间,将这个原子操作须要写的所有REDO先写到这个m_log中,当原子操作完结后,调用mtr_commit将m_log中的数据拷贝到InnoDB的Log Buffer。

2 写入InnoDB Log Buffer

高并发的环境中,会同时有十分多的min-transaction(mtr)须要拷贝数据到Log Buffer,如果通过锁互斥,那么毫无疑问这里将成为显著的性能瓶颈。为此,从MySQL 8.0开始,设计了一套无锁的写log机制,其外围思路是容许不同的mtr,同时并发地写Log Buffer的不同地位。不同的mtr会首先调用log_buffer_reserve函数,这个函数里会用本人的REDO长度,原子地对全局偏移log.sn做fetch_add,失去本人在Log Buffer中独享的空间。之后不同mtr并行的将本人的m_log中的数据拷贝到各自独享的空间内。

/* Reserve space in sequence of data bytes: */const sn_t start_sn = log.sn.fetch_add(len);

3 写入Page Cache

写入到Log Buffer中的REDO数据须要进一步写入操作系统的Page Cache,InnoDB中有独自的log_writer来做这件事件。这里有个问题,因为Log Buffer中的数据是不同mtr并发写入的,这个过程中Log Buffer中是有空洞的,因而log_writer须要感知以后Log Buffer中间断日志的开端,将间断日志通过pwrite零碎调用写入操作系统Page Cache。整个过程中应尽可能不影响后续mtr进行数据拷贝,InnoDB在这里引入一个叫做link_buf的数据结构,如下图所示:

link_buf是一个循环应用的数组,对每个lsn取模能够失去其在link_buf上的一个槽位,在这个槽位中记录REDO长度。另外一个线程从开始遍历这个link_buf,通过槽位中的长度能够找到这条REDO的结尾地位,始终遍历到下一地位为0的地位,能够认为之后的REDO有空洞,而之前曾经间断,这个地位叫做link_buf的tail。上面看看log_writer和泛滥mtr是如何利用这个link_buf数据结构的。这里的这个link_buf为log.recent_written,如下图所示:

图中上半局部是REDO日志示意图,write_lsn是以后log_writer曾经写入到Page Cache中日志开端,current_lsn是以后曾经调配给mtr的的最大lsn地位,而buf_ready_for_write_lsn是以后log_writer找到的Log Buffer中曾经间断的日志结尾,从write_lsn到buf_ready_for_write_lsn是下一次log_writer能够间断调用pwrite写入Page Cache的范畴,而从buf_ready_for_write_lsn到current_lsn是以后mtr正在并发写Log Buffer的范畴。上面的间断方格便是log.recent_written的数据结构,能够看出因为两头的两个全零的空洞导致buf_ready_for_write_lsn无奈持续推动,接下来,如果reserve到两头第一个空洞的mtr也实现了写Log Buffer,并更新了log.recent_written*,如下图:

这时,log_writer从以后的buf_ready_for_write_lsn向后遍历log.recent_written,发现这段曾经间断:

因而晋升以后的buf_ready_for_write_lsn,并将log.recent_written的tail地位向前滑动,之后的地位清零,供之后循环复用:

紧接log_writer将间断的内容刷盘并晋升write_lsn。

4 刷盘

log_writer晋升write_lsn之后会告诉log_flusher线程,log_flusher线程会调用fsync将REDO刷盘,至此实现了REDO残缺的写入过程。

5 唤醒用户线程

为了保证数据正确,只有REDO写完后事务才能够commit,因而在REDO写入的过程中,大量的用户线程会block期待,直到本人的最初一条日志完结写入。默认状况下innodb_flush_log_at_trx_commit = 1,须要等REDO实现刷盘,这也是最平安的形式。当然,也能够通过设置innodb_flush_log_at_trx_commit = 2,这样,只有REDO写入Page Cache就认为实现了写入,极其状况下,掉电可能导致数据失落。

大量的用户线程调用log_write_up_to期待在本人的lsn地位,为了防止大量有效的唤醒,InnoDB将阻塞的条件变量拆分为多个,log_write_up_to依据本人须要期待的lsn所在的block取模对应到不同的条件变量下来。同时,为了防止大量的唤醒工作影响log_writer或log_flusher线程,InnoDB中引入了两个专门负责唤醒用户的线程:log_wirte_notifier和log_flush_notifier,当超过一个条件变量须要被唤醒时,log_writer和log_flusher会告诉这两个线程实现唤醒工作。下图是整个过程的示意图:

多个线程通过一些外部数据结构的辅助,实现了高效的从REDO产生,到REDO写盘,再到唤醒用户线程的流程,上面是整个这个过程的时序图:

六 如何平安地革除REDO

因为REDO文件空间无限,同时为了尽量减少复原时须要重放的REDO,InnoDB引入log_checkpointer线程周期性的打Checkpoint。重启复原的时候,只须要从最新的Checkpoint开始回放后边的REDO,因而Checkpoint之前的REDO就能够删除或被复用。

咱们晓得REDO的作用是防止只写了内存的数据因为故障失落,那么打Checkpiont的地位就必须保障之前所有REDO所产生的内存脏页都曾经刷盘。最间接的,能够从Buffer Pool中取得以后所有脏页对应的最小REDO LSN:lwm_lsn。但光有这个还不够,因为有一部分min-transaction的REDO对应的Page还没有来的及退出到Buffer Pool的脏页中去,如果checkpoint打到这些REDO的后边,一旦这时产生故障复原,这部分数据将失落,因而还须要晓得以后曾经退出到Buffer Pool的REDO lsn地位:dpa_lsn。取二者的较小值作为最终checkpoint的地位,其外围逻辑如下:

/* LWM lsn for unflushed dirty pages in Buffer Pool */lsn_t lwm_lsn = buf_pool_get_oldest_modification_lwm();/* Note lsn up to which all dirty pages have already been added into Buffer Pool */const lsn_t dpa_lsn = log_buffer_dirty_pages_added_up_to_lsn(log);lsn_t checkpoint_lsn = std::min(lwm_lsn, dpa_lsn);

MySQL 8.0中为了可能让mtr之间更大程度的并发,容许并发地给Buffer Pool注册脏页。相似与log.recent_written和log_writer,这里引入一个叫做recent_closed的link_buf来解决并发带来的空洞,由独自的线程log_closer来晋升recent_closed的tail,也就是以后间断退出Buffer Pool脏页的最大LSN,这个值也就是下面提到的dpa_lsn。须要留神的是,因为这种乱序的存在,lwm_lsn的值并不能简略的获取以后Buffer Pool中的最老的脏页的LSN,激进起见,还须要减掉一个recent_closed的容量大小,也就是最大的乱序范畴,简化后的代码如下:

/* LWM lsn for unflushed dirty pages in Buffer Pool */const lsn_t lsn = buf_pool_get_oldest_modification_approx();const lsn_t lag = log.recent_closed.capacity();lsn_t lwm_lsn = lsn - lag;/* Note lsn up to which all dirty pages have already been added into Buffer Pool */const lsn_t dpa_lsn = log_buffer_dirty_pages_added_up_to_lsn(log);lsn_t checkpoint_lsn = std::min(lwm_lsn, dpa_lsn);

这里有一个问题,因为lwm_lsn曾经减去了recent_closed的capacity,因而实践上这个值肯定是小于dpa_lsn的。那么再去比拟lwm_lsn和dpa_lsn来获取Checkpoint地位或者是没有意义的。

下面曾经提到,ib_logfile0文件的前三个Block有两个被预留作为Checkpoint Block,这两个Block会在打Checkpiont的时候交替应用,这样来防止写Checkpoint过程中的解体导致没有可用的Checkpoint。Checkpoint Block中的内容如下:

首先8个字节的Checkpoint Number,通过比拟这个值能够判断哪个是最新的Checkpiont记录,之后8字节的Checkpoint LSN为打Checkpoint的REDO地位,复原时会从这个地位开始重放后边的REDO。之后8个字节的Checkpoint Offset,将Checkpoint LSN与文件空间的偏移对应起来。最初8字节是后面提到的Log Buffer的长度,这个值目前在复原过程并没有应用。

七 总结

本文零碎的介绍了InnoDB中REDO的作用、个性、组织构造、写入形式曾经清理机会,根本笼罩了REDO的大多数内容。对于重启复原时如何应用REDO将数据库复原到正确的状态,将在之后介绍InnoDB故障复原机制的时候具体介绍。

参考

[1] MySQL 8.0.11Source Code Documentation: Format of redo log
https://dev.mysql.com/doc/dev...

[2] MySQL 8.0: New Lock free, scalable WAL design
https://mysqlserverteam.com/m...

[3] How InnoDB handles REDO logging
https://www.percona.com/blog/...

[4] MySQL Source Code
https://github.com/mysql/mysq...

[5] 数据库故障复原机制的前世今生
http://catkang.github.io/2019...

原文链接
本文为阿里云原创内容,未经容许不得转载。