InnoDB Crash Recovery 流程源码实现分析
2017-03-29 16:24
267 查看
Crash Recovery问题
本文主要分析了InnoDB整个crash recovery的源码处理流程,总入口函数是innobase_start_or_create_for_mysql()。InnoDB的crash recovery流程非常长,也十分复杂,以下我总结了几个问题,如果大家能够回答出这些问题,那么就不用看下面的内容了。crash recovery的起点,checkpoint_lsn存于何处?
redo过程是batch redo,还是single redo?如果是batch redo,那么是如何实现的?
redo过程中是否需要读取所有数据文件?
redo过程,日志解析,日志回放操作对应的分别是什么函数?
redo过程中,何时使用doublewrite检查错误页面?
InnoDB的系统表何时创建,有哪些系统表,分别是什么表结构?
undo操作的起点是什么?
一个rollback segment能够支持的并发事务数是多少?
多rollback segments是如何实现的?
如何找到每个rollback segment中的undo信息?
事务与undo是如何关联起来的?
同一事务的操作,是单一undo链表?还是需要区分insert与update操作?
同一事务的undo,是如何链接起来的?
最后,在crash recovery流程的哪些地方作调整,即可实现Percona XtraBackup类似的全量备份,增量备份的功能?
Crash Recovery流程
代码调用流程:ha_innodb.cc::innobase_init
// InnoDBCrash Recovery主函数入口,在此函数中完成所有动作,并open数据库
srv0start.c::innobase_start_or_create_for_mysql();
// 打开所有的InnoDB数据文件,并且获取所有数据文件中最大最小
//的flush_lsn,因为每个数据文件最后一次flush的时间可能不一致
open_or_create_data_files();
// 读取数据文件第一页,FIL_PAGE_FILE_FLUSH_LSN宏定义指向的flush_lsn
// 系统保证数据文件中page_lsn小于此flush_lsn的脏页都被写到磁盘
fil_read_flushed_lsn_and_arch_log_no();
open_or_create_log_files();
// 打开系统表空间对应的文件+日志文件,防止出现文件句柄不足导致
// 系统文件无法打开的情况。
fil_open_log_and_system_tablespace_files();
// 清空buffer pool,所有的page都需要重新读取
// 此时buffer pool中应该只有一个非脏page
buf_pool_invalidate();
// InnoDB恢复Redo入口函数
log0recv.c::recv_recovery_from_checkpoint_start_func();
// 遍历所有的log group,查找其中最大的checkpoint lsn
// checkpoint信息保存在每个日志组日志文件第一页之中,有两处
// LOG_CHECKPOINT_1(第一个LOG_BLOCK_SIZE处)处;LOG_CHECKPOINT_2(3)处
recv_find_max_checkpoint();
//
// 流程如下:
// 1. contiguous_lsn开始,遍历当前日志组。此lsn即为checkpoint lsn。
// group_scanned_lsn为当前日志组能够提供的最大lsn。
// 2. 根据给定的checkpoint lsn,计算在log文件中的偏移位置
// 3. 每次读取RECV_SCAN_SIZE (宏定义:4) 个日志pages,存入log buf。
recv_group_scan_log_recs(group, contiguous_lsn, group_scanned_lsn);
log_group_read_log_seg();
log_group_calc_lsn_offset();
// avail_mem为crash recovery期间,hash表能够使用的buffer pool
// 上限。若buffer pool的大于10MB,则需要预留512个free pages。
// 这些free pages buffer用于读取页面到内存中,进行恢复之用。
// hash表的bucket个数为 buffer pool的大小 / 512.
// 根据日志组,进行redo的流程如下:
// 1. 循环遍历buf中的每一个log block (buf 为4 pages,block = 512)
// 2. 对于每一个block,读取block头,获取block中已写日志长度
// 疑问:block头信息何时写入?block内容写完之后 or 之前?
// 3. 若当前block中最大lsn > checkpoint lsn,就需要进行崩溃恢复
// 4. 递归方式,遍历base目录下的所有目录与文件,并打开其中的
// ibd文件(数据文件),保存在fil_space_struct的chain链表之中
// 5. 读取系统表空间的第五个页面(TRX_SYS_PAGE_NO),获取其中的
// doublewrite信息。若使用doublewrite,则视情况恢复损坏页面
// 关于doublewrite相关的读取,恢复操作,将在后面详细讨论
// 6. 将log block中的log首先保存在recv_sys_struct的buf中,等
// 收集一次读取的4个pages中的所以日志之后,统一进行解析
// 7. 日志解析的操作,在函数recv_parse_log_recs中完成
// 8. 完成日志解析之后,所有的解析日志并非立即应用,而是存储
// 于hash表之中,hash表大小的上限就是前面提到的avail_mem
// 也就是buf pool大小 – 512个pages,hash表大小超过此限制,
// 则需要调用recv_apply_hashed_log_recs函数,redo hash表中的
// 日志,此函数的详细流程在下方给出。
recv_scan_log_recs(avail_mem, store_to_hash, buf, contigous_lsn);
recv_init_crash_recovery();
fil_load_single_table_tablespaces(); // 流程4
fil_load_single_table_tablespace();
fil_node_create();
trx_sys_doublewrite_init_or_restore_pages();// 流程5
recv_sys_add_to_parsing_buf(); // 流程6
start_offset = LOG_BLOCK_HDR_SIZE;
end_offset=OS_FILE_LOG_BLOCK_SIZE–LOG_BLOCK_TRL_SIZE;
// 将log block中的redo日志内容copy到recv_sys->buf中,
// 需要跳过log block的头部(12 bytes)与尾部(4 bytes)
memcpy(…,log_block+start_offset, end_offset-start_offset);
// 将recv_sys_struct->buf中保存的日志进行解析。解析完成
// 的日志存储到hash表之中,流程如下:
// 7.1 首先划分日志大类型:单一日志(文件操作?) or 多日志
// 7.2 解析一条日志,返回的内容包括:日志长度;日志类型;
// 日志操作对应的表空间ID;Page_no;以及日志主体body
// 关于日志类型,可参考mtr0mtr.h文件中的定义
// recv_parse_or_apply_log_rec_body函数遍历所有日志
// 类型,解析并应用(crash recovery时先不应用,解析返回)
// 7.3 若日志操作对应的是文件操作,那么crash recover不处理
// 但是XtraBackup需要处理文件操作日志,重用这些日志
// 7.4 日志操作对应的不是文件操作,则将解析的日志存入
// hash表,hash值根据(space, page_no)组合计算而来,
// 相同的page操作日志,在hash表中保存在一起,同时
// 按照日志操作的顺序,链接在双向链表之中。
// (可以尽量合并同一个page的操作,回放日志更加高效?)
// 7.5 上面提到的流程,同样适用于多日志的处理,唯一的不同
// 之处在于,多日志不包含文件操作,文件操作一定是单条
recv_parse_log_recs(); // 流程7
recv_parse_log_rec(type, space, page_no, body); // 流程7.2
mlog_parse_initial_log_record();
recv_parse_or_apply_log_rec_body();
recv_add_to_hash_table(); // 流程7.4
recv_get_fil_addr_struct();
// 若hash表空间占用量超过上限,那么则应用hash表中的
// 所有日志到对应的page上,过程中不允许使用Insert Buf
// 应用日志的流程如下:
// 8.1 遍历hash表中的每一个bucket,以及bucket中的每一项,
// 8.2 调用buf_page_peek函数,判断page是否已经在buffer,
// 若存在,则直接根据buffer pool中的页面进行日志重做,
// 重做(redo)按照日志生成的顺序进行,老日志先做。
// 首先,读取page的最新page_lsn,若大于日志lsn,跳过,
// 否则,调用recv_parse_or_apply_log_rec_body函数进行
// redo。需要记录redo日志,并且修改page_lsn。
// 重做完一个页面,recv_sys->n_addrs计数– .
// 8.3 若page当前不在buffer pool中,则调用recv_read_in_area
// 函数进行批量读取page(这些page都是需要恢复的page)
// 批量读取的页面数上限RECV_READ_AHEAD_AREA = 128。
// 批量读取page之后,视情况flush前面redo产生的脏页。
// 8.4 当前log hash表中的日志redo全部完成之后,清空
// buffer pool中的所有页面,开始进行下一轮的redo操作
// 8.5 清空当前的log hash表,为下一轮redo准备。
recv_apply_hashed_log_recs(FALSE); // 流程8
// 取出Hash表第i个bucket中的第一个页面对应的日志组
HASH_GET_FIRST(recv_sys->addr_hash, i);
buf_page_peek();
buf_page_get();
recv_recover_page(); // 流程8.2
recv_parse_or_apply_log_rec_body();
recv_read_in_area(); // 流程8.3
buf_read_recv_pages();
buf_flush_free_margins();
// 取出Hash表第i个bucket中的下一个页面对应的日志组
HASH_GET_NEXT(addr_hash, recv_addr);
Buf_pool_invalidate(); // 流程8.4
Recv_sys_empty_hash(); // 流程8.5
// 回到recv_recovery_from_checkpoint_start_func函数,在redo完成之后,
// 若当前有多个日志组,则同步所有日志组到一致状态。
// recv_recovery_from_checkpoint_start_func函数至此结束。
recv_synchronize_groups();
// 初始化表数据字典,同时初始化系统表数据字典结构,主要包括:
// SYS_TABLES; SYS_COLUMNS; SYS_INDEXES; SYS_FIELDS;
dict_boot();
// 前面的recv_recovery_from_checkpoint_start_func函数完成了crash recovery
// 的redo部分操作,而下面的trx_sys_init_at_db_start函数则为了实现crash
// recovery阶段的undo部分操作(基于rollback segment回滚段的undo),包括:
// 未成功commit事务的收集整理,按类别划分,真正undo操作的前期准备。
// undo信息收集整理的流程如下:
// 1. 读取Transaction system header页面,系统表空间的第五个page
// 2. 初始化内存回滚段对象,主要内容包括:
// 2.1 读取每个回滚段对应的回滚段段头页(page_no)
// 2.2 读取每个回滚段对应的表空间序号(space_id)
// 2.3 根据2.1;2.2读取的信息,重构回滚段内存结构:
// 2.3.1 将回滚段链接到系统事务管理结构的链表中(trx_sys->rseg_list)
// 2.3.2 读取当前回滚段的段头页(trs_rsegf_get_new()),结构可见trx0rseg.h
// 2.3.3 分析回滚段段头页,取出其中的undo slot (每个回滚段段头页,
// 最多能够包含个TRX_RSEG_N_SLOTS undo slot,page_size / 16 = 1024)
// 每个slot占用 4 bytes,记录的则是当前undo对应的undo page_no
// 每个事务需要占用两个undo slot(insert & update)
// 2.3.4 根据undo slot中记录的page_no,读取对应的undo page信息:
// undo_type: UNDO_INSERT or UPDATE;
// undo_state: TRX_UNDO_ACTIVE or TRX_UNDO_PREPARE
// undo_offset: 当前undo最后一条undo日志在页面中的位置
// trx_id: 读取最后一条undo日志头,获得日志对应事务ID
// xid: 若存在XA事务,则读取XA事务的xid
// last_page_no: 当前undo最后(最新)一条日志写的undo page
// 在这些信息读取之后,创建trx_undo_t,undo内存结构
// 2.3.5 根据last_page_no与undo_offset,读取最后一条undo日志内容
// 2.3.6 根据最后一条undo rec,读取其对应的undo 序列号
// 2.3.7 当前undo,链接到回滚段对应的链表中(insert/update_undo_list)
trx_sys_init_at_db_start();
trx_sysf_get(TRX_SYS_SPACE, TRX_SYS_PAGE_NO);
trx_rseg_list_and_array_init();
trx_rseg_create_instance();
trx_sysf_rseg_get_page_no(); // 流程2.1
trx_sysf_rseg_get_space(); // 流程2.2
trx_rseg_mem_create(); // 流程2.3
trx_undo_lists_init(); // 流程2.3.3
trx_undo_mem_create_at_db_start(); // 流程2.3.4
trx_undo_mem_create(); // 流程
trx_undo_page_get_last_rec(); // 流程2.3.5
trx_undo_rec_get_undo_no(); // 流程2.3.6
UT_LIST_ADD_LAST(undo_list, undo); // 流程2.3.7
// 完成undo信息的收集,接下来就是根据undo信息,重建未提交事务。
// 未提交事务的重建流程如下:
// 1. 遍历trx_sys结构中的rollback segment回滚段链表,取出所有回滚段
// 2. 遍历每个回滚段的insert_undo_list与update_undo_list,取出的undo
// 3. 根据undo日志信息,重建事务;并且根据事务的不同状态:
// 3.1 undo事务状态为TRX_UNDO_ACTIVE,为活跃事务,TRX_ACTIVE
// 3.2 undo事务状态为TRX_UNDO_PREPARED -> TRX_PREPARED
// 4. 将事务按照trx_id的顺序链入trx_sys->trx_list链表
// 注意:在重建事务时,事务的start_time也为新的时间,而不是crash
// 前的事务真正创建时间。
trx_lists_init_at_db_start();
trx_create(); // 流程3
// trx->undo_no,记录了当前事务的undo序列号,也代表当前事务一共
// 修改了多少行记录。
rows_to_undo += trx->undo_no;
// 创建purge操作系统结构。至此,undo信息与undo事务的创建结束。
trx_purge_sys_create();
// undo信息重构,事务创建完成之后,进入到crash recovery的最后一个步骤
// 最后一个步骤的流程如下:
// 1. redo最后hash表中最后一部分log,最后一次hash表未满,在此时redo
// 2. 打印InnoDB redo log位置信息;以及最后一次commit操作的binlog信息
// 两个信息都保存于transaction system header page中。[0, 5]
// 3. 回滚处于ACTIVE状态的DDL操作事务,此时并不回滚DML事务。
recv_recovery_from_checkpoint_finish();
recv_apply_hashed_log_recs();
trx_rollback_or_clean_recovered(FALSE);
// 回滚DML操作ACTIVE事务,在此函数中完成;而处于PREPARE状态的
// 事务,并不回滚,而是等待MySQL上层的binlog判断最终的回滚or提交
// 回滚DML操作,通过新创建一个线程完成,而非在主线程中执行
recv_recovery_rollback_active();
os_thread_create(trx_rollback_or_clean_all_recovered);
trx_rollback_or_clean_recover(TRUE);
// 创建加锁超时监控线程
os_thread_create(&srv_lock_timeout_thread);
// 创建InnoDB主线程,做purge,checkpoint,dirty pages flush操作
os_thread_create(&srv_master_thread);
// 至此,innobase_start_or_create_for_mysql函数处理完毕,crash recovery
// 操作也基本完成,InnoDB引擎恢复成功,处于open状态
Crash Recovery优化
根据crash recovery流程,后续人员发现其中有两个性能不足的点:控制redo log hash table的大小
redo log hash table大小,不能超过available_mem,如何判断?Bug #49535
redo 过程中维护flush_list
按照dirty page最早的修改时间排序的一个链表结构,方便进行fuzzy checkpoint?Bug #29847
在正常超过过程中,由于lsn是递增生成,因此新修改的page一定位于flush list的最前面。但是在crash recovery过程中,redo是按照页面进行batch操作,不同页面的oldest_modification不一定递增产生,因此每个页面加入flush list,都需要遍历flush list链表,消耗cpu性能。
两个问题的说明与解决方案,具体可参考[1][2]。而接下来的部分,主要是从源码分析两个问题的解决方案。
相关文章推荐
- innoDB Crash Recovery 流程源码实现分析
- InnoDB Crash Recovery 流程源码实现分析
- InnoDB Crash Recovery 流程源码实现分析
- InnoDB Crash Recovery 流程源码实现分析
- Struts2的执行流程解释以及源码分析(以登录 和自动登录实现 为例)
- SprignMVC+myBatis整合+mybatis源码分析+动态代理实现流程+如何根据mapper接口生成其实现类
- MySQL源码分析(4):InnoDB主要数据结构及调用流程
- Nginx源码分析 - 主流程篇 - 多进程实现
- InnoDB Rollback Segment & Undo Page Deallocation实现源码分析
- linux内存源码分析 - 内存压缩(实现流程)
- linux内存源码分析 - 内存压缩(实现流程)
- Netty学习之旅----源码分析netty服务端初始化流程(Reactor主从模式实现)
- Glide源码分析(六)——从DecodeJob相关实现看图片加载流程
- innodb crash recovery流程
- Android 7.0 虚拟按键(NavigationBar)源码分析 之 点击事件的实现流程
- Mysql Innodb中undo-log和MVCC多版本一致性读的实现(源码分析)
- 第二人生的源码分析(11)地面显示的实现
- 第二人生的源码分析(4)Log调试功能的实现
- 第二人生的源码分析(5)类Log的实现
- 第二人生的源码分析(10)登录授权的实现过程