如果 InnoDB 没有正常关闭,会在服务器启动的时候执行崩溃恢复 (Crash Recovery),这一流程比较复杂,涉及到了 redo log、undo log 甚至包括了 binlog 。
在此简单介绍下 InnoDB 崩溃恢复的流程。

简介

如果 InnoDB 意外宕机了,那么会不会丢数据?
当然,这一问题比较复杂,根据不同的情况,可能会有数据丢失,不过至少有一点可以肯定,不会导致全部数据丢失。而这一过程,便涉及到了数据恢复。

初始化

在 MySQL 的主函数中,最终会通过 plugin_init() 对插件进行初始化,此时,会依次调用各个插件的初始化函数,同时也会调用 InnoDB 对应的初始化函数。
详细的调用流程如下。
mysqld_main()
|-init_server_components()
|-plugin_init()
|-plugin_initialize()
|-ha_initialize_handlerton()
|-innobase_init()
|-innobase_start_or_create_for_mysql()
InnoDB 崩溃恢复相关的入口是 innobase_start_or_create_for_mysql() 函数。首先,InnoDB 会检查上次数据库是否正常关闭,如果是则不需要恢复,否则就进入崩溃恢复的流程。

系统检查

数据库启动后,InnoDB 会通过 read_lsn_and_check_flags() 函数读取系统表空间中 flushed_lsn ,这一个 LSN 只在系统表空间的第一个页中存在,而且只有在正常关闭的时候写入。
系统正常关闭时,会调用 logs_empty_and_mark_files_at_shutdown() -> fil_write_flushed_lsn() ,也就是在执行一次 sharp checkpoint 之后,将 LSN 写入。
flushed_lsn 只有在系统表空间的第一页存在,偏移量为 FIL_PAGE_FILE_FLUSH_LSN(26),也就是保证至少在此 LSN 之前的页已经刷型到磁盘。
另外需要注意的是,写 flushed_lsn 时会同时写入到 Double Write Buffer,如果 flushed_lsn 对应的页损坏,则可以从 dbwl 中进行恢复。
接下来,InnoDB 会通过 redo-log 日志找到最近一次提交的 checkpoint,读取该 checkpoint 对应的 LSN 。其中,checkpoint 信息会保存在 redo-log 的第一个文件中,在两个固定偏移中轮流写入;所以,需要同时读取两个,并比较获取较大的一个值。
比较获得的 flushed_lsn 以及 checkpoint_lsn ,如果两者相同,则说明正常关闭;否则,就需要进行故障恢复。

重做日志

简单来说,如果需要执行崩溃恢复,会从上述读取的 checkpoint 信息,直接找到 redo-log 文件中相应的偏移量,也就是从 checkpoint 指定的位置开始读取日志,并保存到一个哈希表中,最后通过遍历哈希表中的 redo log 信息,读取相关页进行恢复。

日志扫描

假设,从上述 checkpoint 定位到开始恢复的 redo log 位置是在 ib_logfile1 文件中的某个位置,那么整个 redo log 扫描的过程可能是这样的:

  • 从 ib_logfile1 的指定位置开始读取 redo log,每次读取 RECV_SCAN_SIZE (4*page_size=64k) 大小,写入时是以 block(512B) 为单位;
  • 将从文件中读取的日志保存在 recv_sys->buf 中,然后进行校验,并解析日志,然后将结果保存在以 (space, page_no) 做 key 的 recv_sys->addr_hash 表中,这样一个 key 就对应了一个数据页的修改;

redo log 被保存到哈希表中之后,InnoDB 就可以开始进行数据恢复,只需要轮询哈希表中的每个节点获取 redo 信息,根据 (space, page_no) 读取指定的数据页,并进行日志覆盖。

优化

如上,在恢复时,需要获取 space id 与 *.ibd 文件的对应关系,这就需要打开所有的 ibd 文件获取,如果文件有成百上千,甚至以万计的时候,那么这一操作将会非常耗时。
为此,5.7 在 redo log 中添加了两个新的类型:MLOG_FILE_NAME 记录在 checkpoint 之后,所有被修改过的信息(space, filepath);MLOG_CHECKPOINT 用于标志 MLOG_FILE_NAME 的结束。

源码分析

InnoDB 的数据恢复是一个很复杂的过程,在其恢复过程中,需要 redolog、binlog、undolog 等参与,接下来从源码角度具体了解下整个恢复的过程。
innobase_init()
|-innobase_start_or_create_for_mysql()
|
|-recv_sys_create() 创建崩溃恢复所需要的内存对象
|-recv_sys_init()
| |-hash_create()
|
|-srv_sys_space.check_file_spce() 检查系统表空间是否正常
|-srv_sys_space.open_or_create() 1. 打开系统表空间,并获取flushed_lsn
| |-read_lsn_and_check_flags()
| |-open_or_create()
| |-read_first_page()
| |-buf_dblwr_init_or_load_pages() 将双写缓存加载到内存中,如果ibdata日志损坏,则通过dblwr恢复
| |-validate_first_page() 校验第一个页是否正常,并读取flushed_lsn
| | |-mach_read_from_8() 读取LSN,偏移为FIL_PAGE_FILE_FLUSH_LSN
| |-restore_from_doublewrite() 如果有异常,则从dblwr恢复
|
|-log_group_init() redo log的结构初始化
|-srv_undo_tablespaces_init() 对于undo log表空间恢复结构初始化
|
|-recv_recovery_from_checkpoint_start() 2. 从redo-log的checkpoint开始恢复;注意,正常启动也会调用
| |-buf_flush_init_flush_rbt() 创建一个红黑树,用于加速插入flush list
| | 通过force_recovery判断是否大于SRV_FORCE_NO_LOG_REDO
| |-recv_find_max_checkpoint() 查找最新的checkpoint点,在此会校验redo log的头部信息
| | |-log_group_header_read() 读取512字节的头部信息
| | |-mach_read_from_4() 读取redo log的版本号LOG_HEADER_FORMAT
| | |-recv_check_log_header_checksum() 版本1则校验页的完整性
| | | |-log_block_get_checksum() 获取页中的checksum,也就是页中的最后四个字节
| | | |-log_block_calc_checksum_crc32() 并与计算后的checksum比较
| | |-recv_find_max_checkpoint_0()
| | |-log_group_header_read()
| |
| |-recv_group_scan_log_recs() 3.1 从checkpoint-lsn处开始查找MLOG_CHECKPOINT
| | |-log_group_read_log_seg() 从文件中读取64K日志,并未校验
| | |-recv_scan_log_recs()
| | |-log_block_get_hdr_no()
| | |-log_block_convert_lsn_to_no()
| | |-log_block_checksum_is_ok() 校验页是否正常
| | |-recv_parse_log_recs() 解析redo-log,并添加到hash表中
| | |-recv_add_to_hash_table()
| | |-recv_hash()
| |
| |-recv_group_scan_log_recs()
| | ##如果flushed_lsn和checkponit lsn不同则恢复
| |-recv_init_crash_recovery()
| |-recv_init_crash_recovery_spaces()
| |
| |-recv_group_scan_log_recs()
|
|-trx_sys_init_at_db_start()
|
|-recv_apply_hashed_log_recs() 当页LSN小于log-record中的LSN时,应用redo日志
| |-recv_recover_page() 实际调用recv_recover_page_func()
| |-recv_parse_or_apply_log_rec_body()
|
|-recv_recovery_from_checkpoint_finish() 完成崩溃恢复
接下来,首先重点看下 redo-log 的扫描函数。
static bool recv_group_scan_log_recs(
log_group_t group,
lsn_t
contiguous_lsn,
bool last_phase)
{
mutex_enter(&recv_sys->mutex);
recv_sys->len = 0;
recv_sys->recovered_offset = 0;
recv_sys->n_addrs = 0;
recv_sys_empty_hash();
srv_start_lsn = contiguous_lsn;
recv_sys->parse_start_lsn =
contiguous_lsn;
recv_sys->scanned_lsn = contiguous_lsn;
recv_sys->recovered_lsn =
contiguous_lsn;
recv_sys->scanned_checkpoint_no = 0;
recv_previous_parsed_rec_type = MLOG_SINGLE_REC_FLAG;
recv_previous_parsed_rec_offset = 0;
recv_previous_parsed_rec_is_multi = 0;
ut_ad(recv_max_page_lsn == 0);
ut_ad(last_phase || !recv_writer_thread_active);
mutex_exit(&recv_sys->mutex);

  1. lsn_t checkpoint_lsn = *contiguous_lsn;<br /> lsn_t start_lsn;<br /> lsn_t end_lsn;
  2. // 在此会根据三个不同的阶段调用不同的变量 // 1. 如果还没有扫描到MLOG_CHECKPOINT,则为STORE_NO // 2. 第二次扫描则为STORE_YES // 3. 第三次扫描则为STORE_IF_EXISTS store_t store_to_hash = recv_sys->mlog_checkpoint_lsn == 0<br /> ? STORE_NO : (last_phase ? STORE_IF_EXISTS : STORE_YES);
  3. ulint available_mem = UNIV_PAGE_SIZE<br /> * (buf_pool_get_n_pages()<br /> - (recv_n_pool_free_frames * srv_buf_pool_instances));
  4. end_lsn = *contiguous_lsn = ut_uint64_align_down(<br /> *contiguous_lsn, OS_FILE_LOG_BLOCK_SIZE);
  5. do {<br /> if (last_phase && store_to_hash == STORE_NO) {<br /> store_to_hash = STORE_IF_EXISTS;<br /> /* We must not allow change buffer merge here, because it would generate redo log records before we have finished the redo log scan. */<br /> recv_apply_hashed_log_recs(FALSE);<br /> }
  6. start_lsn = end_lsn;<br /> end_lsn += RECV_SCAN_SIZE; // 每次读取的大小 <br /> // 从磁盘中读取数据 log_group_read_log_seg(<br /> log_sys->buf, group, start_lsn, end_lsn);
  7. // 从缓存中读取日志,并解析,当hash表满时则直接执行 } while (!recv_scan_log_recs(<br /> available_mem, &store_to_hash, log_sys->buf,<br /> RECV_SCAN_SIZE,<br /> checkpoint_lsn,<br /> start_lsn, contiguous_lsn, &group->scanned_lsn));
  8. if (recv_sys->found_corrupt_log || recv_sys->found_corrupt_fs) {<br /> DBUG_RETURN(false);<br /> }
  9. DBUG_RETURN(store_to_hash == STORE_NO);<br />}