在MySQL5.7之前的版本中, InnoDB每次做crash recovery之前都需要扫描数据目录,打开每个文件并创建内存对象。当目录下文件个数特别多时,会严重影响到崩溃恢复的速度。
为了解决这个问题,MySQL5.7通过结合checkpoint + 标注被修改的文件的方式,从一个checkpoint点开始,可以找到所有崩溃恢复需要打开的文件,从而避免扫描数据目录。
本文简单的记录了相关的代码,以及一个相关的优化点。
提交mini transaction
入口函数:
mtr_commit -->
mtr_t::Command::execute
mtr_t::Command::prepare_write()
// fil_names_write_if_was_clean
/// 如果space->max_lsn 等于0,表示从最近一次checkpoint开始至今,这是第一次被修改
/// fil_names_dirty_and_write:
1. 加入到链表fil_system->named_spaces的尾部
2. 更新fil_space_t::max_lsn为当前LSN
3. 写入一条MLOG_FILE_NAME,日志内容包括space id 及表空间文件路径
// 如果是从上次checkpoint后第一次修改该tablespace, fil_names_dirty_and_write返回true,表示一条 MLOG_FILE_NAME已经追加到当前mtr了,因此在日志组的尾部增加”MLOG_MULTI_REC_END”
// 否则,如果从上次checkpoint之后不止一次修改,则继续走之前的逻辑:单个rec,对第一个字节设置 MLOG_SINGLE_REC_FLAG的flag,或者多个rec的话追加一字节的MLOG_MULTI_REC_END,来标记日志组的结尾位置
在prepare_write中保证了从上次checkpoint后第一次修改的tablespace都有一个MLOG_FILE_NAME写到了日志组中. 随后调用mtr_t::Command::finish_write将日志拷贝到公共buffer中.
Make Checkpoint
入口函数: log_checkpoint
这里会在持有log_sys->mutex的情况下,在做checkpoint前追加MLOG_CHECKPOINT (但如果是一次clean的shutdown,则无需此步骤,因为已经保证了checkpoint后没有新的日志写入)
// fil_names_clear
/// 遍历fil_system->named_spaces链表:
1. 如果fil_space_t::max_lsn小于请求checkpoint的LSN(通常是BufferPool中最老的脏block的LSN),则清空fil_space_t::max_lsn为0,并从链表移除
2. 不管max_lsn是大于checkpoint的LSN还是小于,都会调用fil_names_write, 写入MLOG_FILE_NAME
/// 追加一条日志MLOG_CHECKPOINT, 记录checkpoint发生的LSN
在函数fil_names_clear中产生的日志聚合在一个mtr中提交。(但这个mtr中实际上包含两个log body, MLOG_CHECKPOINT被当做singl rec)。也就是说,除非clean shutdown之外,每个完整的checkpoint,必然要有对应的mlog_checkpoint日志.
随后将日志刷入磁盘: 这是一个临界点,假如在这里crash了,这意味着checkpoint还没更新下去, MLOG_CHECKPOINT已经写下去了,如果从上次checkpoint的点开始扫描,可能会找到两个MLOG_CHECKPOINT日志
Append on checkpoint:
在看代码时发现一个和旧版本不同的变量log_sys->append_on_checkpoint,指向的是一段redo cache。 在做checkpoint时写MLOG_FILE_NAME之前会先看这个指针是否为空,如果不为空,就将这段cache的日志写到全局Buffer。
在函数ha_innobase::commit_inplace_alter_table中被设置,在ddl的最后一步. 设置和重置append_on_checkpoint是在持有数据词典锁时进行的。如果DDL需要最后做临时表和用户表的交换, 此时会写两条MLOG_FILE_RENAME2日志,第一条是老表rename成一个临时表,第二个是完成DDL的新表rename为老表名
然后将这个日志组拷贝下来,并赋值到log_sys->append_on_checkpoint。根据commit log的描述,主要是解决如下场景:
1. The changes to SYS_TABLES were committed, and MLOG_FILE_RENAME2
records were written in a single mini-transaction commit.
2. A log checkpoint and a server kill was injected.
3. Crash recovery will see no records (other than the MLOG_CHECKPOINT).
4. dict_check_tablespaces_and_store_max_id() will emit a message about
a non-found table #sql-ib22*.
5. A mismatch is triggering the assertion failure.
Crash Recovery
入口函数: recv_recovery_from_checkpoint_start:
在从第一个日志文件ib_logfile0找到checkpoint点后,就可以从该点开始扫描日志:
scan 1: 找到MLOG_CHECKPOINT的位置(STORE_NO)
recv_sys_t::mlog_checkpoint_lsn 记录出现MLOG_CHECKPOINT的日志位置. 找到和checkpoint lsn匹配的MLOG_CHECKPOINT后结束第一次扫描
在扫描的过程中,如果遇到如下几类日志,调用fil_name_parse:
case MLOG_FILE_NAME:
case MLOG_FILE_DELETE:
case MLOG_FILE_CREATE2:
case MLOG_FILE_RENAME2:
对于涉及到的表名,会调用fil_name_process存储到recv_spaces_t中, 这是个map, 以space_id为Key. 对于MLOG_FILE_NAME or MLOG_FILE_RENAME2, 将对应的space载入内存( fil_ibd_load ). 对于MLOG_FILE_DELETE,如果map中已经存在,将flag设置为deleted,并释放fil_space_t
如果找不到MLOG_CHECKPOINT的话,就认为崩溃恢复失败了(clean shutdown除外)
scan 2:
开始扫描并存储到hash中(STORE_YES),解析到的日志被存储到hash中(不判断tablespace是否存在)。STORE_YES的意思是不去判断是否redo对应的tablespace是否存在或被修改. 因为第二次scan的另外一个目的是搜集所有在checkpoint后被修改过的表空间(MLOG_FILE_NAME)
如果内存不够用于存储redo log时,那就不再将redo存到hash中,但会继续扫描到日志尾部,确保所有被修改的表空间都被检测到了并维护下来。
调用recv_init_crash_recovery_spaces
// 对于被修改过的tablespace,加入到链表fil_system->named_spaces上(fil_names_dirty)
// 如果已经删除的tablespace,就将对应的日志设置为RECV_DISCARDED, 这些日志无需apply
// 根据double write buffer校验及载入(buf_dblwr_process)
// 开启后台进程, 用于flush dirty page (recv_writer_thread)
scan 3:
如果scan 2由于内存不足未完成, 会最后重新扫描,由于scan 2已经确保了 这一轮只将tablespace存在的日志加入到hash中(STORE_IF_EXISTS),如果内存不足了,则直接apply掉,再继续解析.
问题及优化
对于第一次扫描寻找mlog_checkpoint可以做一些优化,在做checkpoint时直接将对应的位点存到checkpoint信息里,这样在崩溃恢复时,就可以直接跳到对应的位置,从而避免扫描. 我们将这个Issue report到官方,已经得到确认,ref: (bug#80788)