在《redo Log 格式浅析》文章中,我们介绍了redo log的基本格式和结构以及写入步骤。数据库系统与文件系统的最大的区别就是要最大限度的保证操作的原子性,在InnoDB存储引擎中就是依靠redo log来保证的。当数据库异常崩溃后,数据库重新启动时会根据redo log进行数据恢复,保证数据库恢复到崩溃前的状态。那么这个过程在InnoDB里面是如何进行的呢,本文将结合MySQL 8.0.12的源代码进行简要的解析。
1. InnoDB崩溃恢复相关参数
Innodb_fast_shutdown: 在mysql关闭时,参数innodb_fast_shutdown 影响着存储引擎innodb的行为。参数为0,1,2三个值。
0,代表当MYSQL关闭时,Innodb需要完成所有full purge和merge insert buffer操作,这需要花费时间来完成。
1,是参数的默认值,不需要完成full purge和merge insert buffer操作,但是在缓冲池的一些数据脏页还是会刷新到磁盘。
2,表示不需要完成full purge和merge insert buffer操作 ,也不将缓冲池中的数据脏页写回磁盘,而是将日志都写入日志文件。这样不会有任何事务丢失,但是MySQL在下次启动时,会执行恢复操作(recovery)。
innodb_force_recovery: 影响了整个Innodb存储引擎的恢复状况。该值默认为0,表示当需要恢复时执行所有的恢复操作。当不能进行有效恢复时(如数据页发生了corruption,InnoDB引擎可能会无法启动)把错误写入错误日志中。
Innodb_force_recovery可以设置6个非零值:
(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页。
(SRV_FORCE_NO_BACKGROUND):阻止主线程的运行,如主线程需要执行full purge操作,会导致crash。
(SRV_FORCE_NO_TRX_UNDO):不执行事务回滚操作.
(SRV_FORCE_NO_IBUF_MERGE):不执行插入缓冲的合并操作.
(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看重做日志,InnoDB存储引擎会将未提交的事务视为已提交。
(SRV_FORCE_NO_LOG_REDO):不执行前滚的操作。
2. 与崩溃恢复相关的重要数据结构
log_t : 该结构体的定义在storage/innobase/include/log0types.h中。InnoDB系系运行时刻只会有一个该数据结构的实例log_sys,log_sys的定义在strorage/innobase/log0log.cc中(log_t *log_sys) 。 log_sys中存储了innodb redo log系统运行时刻的各种状态。
recv_sys_t : 该结构体的定义在/storage/innobase/include/log0recv.h中。这个结构体变量用来描述恢复系统运行时刻的状态。InnoDB运行时刻有一个该数据结构的实例recv_sys,recv_sys的定义在storage/innobase/log/log0recv.cc中(recv_sys_t * recv_sys =nullptr)。
在这里我们列出一个该结构体内部的部分定义
1. struct recv_sys_t {
2. ……...
3. lsn_t parse_start_lsn;
4. lsn_t checkpoint_lsn;
5. lsn_t scanned_lsn;
6. lsn_t recovered_lsn;
7. Spaces *spaces;
8. …………..
9. }
其中Spaces *spaces 是以space_id做hash的hash表,表里面存放的元素是以page_no做hash的hash表(pages), pages表里存放的是按照lsn大小排序的需要在该页上进行恢复的日志记录。
parse_start_lsn:本次日志重做恢复起始的lsn,如果是从checkpoint处开始恢复,等于checkpoint_lsn。
scanned_lsn: 在恢复过程,将恢复日志从log_sys->buf解析块后存入recv_sys->buf的日志lsn.
recovered_lsn:已经将数据恢复到page中或者已经将日志操作存储addr_hash当中的日志lsn;
在日志开始恢复时:
parse_start_lsn = scanned_lsn = recovered_lsn = checkpoint_lsn。
在日志完成恢复时:
parse_start_lsn = checkpoint_lsn
scanned_lsn = recovered_lsn = log_sys->lsn。
另外还有2个重要的数据结构,结构体定义非常简单,大家看英文就能明白什么意思。
1. /** Hashed page file address struct */
2. struct recv_addr_t {
3. using List = UT_LIST_BASE_NODE_T(recv_t);
4.
5. /** recovery state of the page */
6. recv_addr_state state;
7. /** Space ID */
8. space_id_t space;
9. /** Page number */
10. page_no_t page_no;
11. /** List of log records for this page */
12. List rec_list;
13. };
1. /** Stored log record struct */
2. struct recv_t {
3. using Node = UT_LIST_NODE_T(recv_t)
4. /** Log record type */
5. mlog_id_t type;
6. /** Log record body length in bytes */
7. ulint len;
8. /** Chain of blocks containing the log record body */
9. recv_data_t *data;
10. /** Start lsn of the log segment written by the mtr which generated
11. this log record: NOTE that this is not necessarily the start lsn of
12. this log record */
13. lsn_t start_lsn;
14. /** End lsn of the log segment written by the mtr which generated
15. this log record: NOTE that this is not necessarily the end LSN of
16. this log record */
17. lsn_t end_lsn;
18. /** List node, list anchored in recv_addr_t */
19. Node rec_list;
20. };
这几个数据结构运行时刻的内存关系如下:
3. InnoDB崩溃恢复阶段基本流程
3.1. 基本总体流程
InnoDB的recovery的函数入口是srv_start(storage/innobase/srv/srv0start.cc)。
srv_start是MySQL启动的时候由innodb初始化函数innobase_init_files调用,
在srv_start中与崩溃恢复有关的代码流程如下:
首先获取当前已经写入redo log的日志量(flush_lsn),这个已经写入的日志量存放在系统表空间的第一页中。
srv_sys_space.open_or_create(false, create_new_db, &sum_of_new_sizes, &flushed_lsn);
然后将flush_lsn作为参数调用recv_recovery_from_checkpoint_start函数,初始化recv_sys_t结构,读取checkpoint, 然后从checkpoint开始读取日志,解析日志,应用日志。
err = recv_recovery_from_checkpoint_start(*log_sys, flushed_lsn);
最后调用recv_recovery_from_checkpoint_finish函数进行一些崩溃恢复的清理工作,释放创建的recv_sys_t内存空间。
srv_dict_metadata = recv_recovery_from_checkpoint_finish(*log_sys, false);
3.2. recv_recovery_from_checkpoint_start
其中崩溃恢复绝大部分的逻辑都集中在recv_recovery_from_checkpoint_start中,下面我们对它进行进一步的介绍。这其中的代码逻辑主要分为2个阶段,首先是日志扫描阶段,扫描阶段按照数据页的space_id和page_no分发redo日志到hash_table中,保证同一个数据页的日志被分发到同一个哈希桶中,且按照lsn大小从小到大排序。扫描完后,再遍历整个哈希表,依次应用每个数据页的日志。下面我们按照流程顺序做个基本的简介。
首先从ib_logfile0中找到最大的checkpoint
err = recv_find_max_checkpoint(log, &max_cp_field);
在日志头中有2个checkpoint block域。InnoDB是采用2个checkpoint了轮流写的方式来保证checkpoint写操作的安全(并不是一次写2份checkpoint, 而是轮流写)。 由于redo log是幂等的,应用一次和与应用两次都是一样的(在实际的应用redo log时,如果当前这一条log记录的lsn大于当前page的lsn,说明这一条log还没有被应用到当前的page中去)。所以,即使某次checkpoint block写失败了,那么崩溃恢复的时候从上一次记录的checkpoint点开始恢复也能正确的恢复数据库事务
接下来用读取到的checkpoint数据作为参数调用recv_recovery_begin。崩溃恢复的主要代码流程来到了recv_recovery_begin。
在recv_recovery_begin中,存在一个循环,该循环以checkpoint为起点,调用底层函数(recv_read_log_seg),按照RECV_SCAN_SIZE(64KB)大小分批读取日志数据到log_sys->buf中,然后调用调用recv_scan_log_recs对读取到的数据进行扫描解析和应用,直到所有的日志都处理完毕。
在recv_scan_log_recs中,首先通过block_no和lsn之间的关系以及日志checksum判断是否读到了日志最后,如果读到最后则返回(即使数据库是正常关闭的,也要走崩溃恢复逻辑,那么在这里就返回了,因为正常关闭的checkpoint值一定是指向日志最后),否则调用recv_sys_add_to_parsing_buf函数把日志去头去尾放到一个recv_sys->buf中,日志头里面存了一些控制信息和checksum值,只是用来校验和定位,在真正的应用时没有用。接下来就开始调用recv_parse_log_recs对recv_sys->buf中的日志数据进行解析然后放到前面提到的hash表中。当hash表中存放的数据recv_addr_t达到一定的大小之后,就调用recv_apply_hashed_log_recs进行日志应用。
在recv_parse_log_recs时,解析到的日志分两种:single_rec和multi_rec,前者表示只对一个数据页进行一种操作,后者表示对一个或者多个数据页进行多种操作。日志中还包括对应数据页的space_id,page_no,操作的type以及操作的内容(具体单条日志记录的解析逻辑在recv_parse_log_rec函数中)。解析出相应的日志后,按照space_id和page_no进行哈希并放到hash_table里面即可,等待后续应用。
在recv_single_rec和recv_multi_rec中都会调用到recv_parse_log_rec进行单条记录的解析(记录解析部分的逻辑与mtr/MLOG有关,以后会另外单独写一篇文章进行介绍),然后调用recv_add_to_hash_table放到hash表中去。
在recv_apply_hashed_log_recs中,就是遍历hash_table,针对hash表中的每一个有日志的数据页,调用recv_apply_log_rec应用与其有关的redo日志。应用完所有的日志后,如果需要则把buffer_pool的页面都刷盘(buf_pool_invalidate)。
在recv_apply_log_rec首先把需要应用日志的页读取到buffer pool中buf_page_get,然后调用recv_recover_page将日志的修改应用到该页中去。(具体的应用逻辑与mtr/MLOG后会另外单独写一篇文章进行介绍)。
下面我们用一个流程示意图归纳一下上面的流程
3.3. 回滚未完成的无效事务
严格来说上面的流程只是做到了数据库的前滚,也就是说到此为止恢复到了数据库崩溃前的状态,但是数据库崩溃前存在的一些未完成提交的事务需要在这个阶段做一些清理。(这里指的未完成的事务不是XA事务,处于Prepare阶段的XA事务的清理需要用到Binlog,这是MySQL服务层的概念和职责,与本地存储引擎无关,不在本文的阐述范围之内)。
在InnoDB初始化innobase_init_files函数执行完成之后,MySQL服务层会调用InnoDB引擎层的innobase_dict_recover函数,执行InnoDB更高层次的恢复过程。该函数会启动一个线程trx_recovery_rollback_thread,这个线程主要执行的函数为trx_rollback_or_clean_recovered,这里面会检测前滚阶段产生的事务是否已经提交,如果已经提交,那么清除这个事务可能存在的insert undo log(trx_undo_insert_cleanup)。如果这个事务未提交,那么就对其进行回滚(trx_rollback_active)。这里面的清除和回滚需要用到undo log。(关于undo log的介绍,我们会在另写一篇文章进行介绍)。