[MySQL学习] Innodb崩溃恢复流程

简要记录跟踪代码,很多代码流程没有细细的跟进去,只是了解了个大概,杂七杂八,还有太多不了解的地方。

不过,一知半解总比一无所知要好点…sign…

////////////////////////////////////////////

一、innobase_init
1.初始化存储引擎接口函数、检查指定的page大小(innodb_page_size,Percona版本支持16k以下的page size定义)、innodb_log_block_size。

2.检查是否通过记录在innodb层的relay log信息更新relay-info文件(通过innodb_recovery_update_relay_log来控制)

3.innodb文件路径、类型、各类全局变量等信息初始化,

4.innobase_start_or_create_for_mysql //主要函数,稍后细述

5.更新每个bp实例的buf_pool->LRU_old_ratio(innodb_old_blocks_pct),也就是LRU上old list的百分比。

6.初始化innobase_share_mutex、prepare_commit_mutex、commit_threads_m、commit_cond_m、commit_cond等变量。

可以看到在这个函数中除了innobase_start_or_create_for_mysql这个主要函数外,基本上都是些变量之类的初始化,我们简要看看innobase_start_or_create_for_mysql主要干了什么吧。


二、innobase_start_or_create_for_mysql
1.
当buffer pool size大于等于1000MB时,在innodb层最大允许等待同一信号量的线程数srv_max_n_threads=5000
当1000MB >= buffer pool size >= 8MB,只使用1个bp实例,srv_max_n_threads = 10000
当buffer pool size小于8MB时,只使用1个bp实例,srv_max_n_threads = 1000

2.调用srv_boot
srv_normalize_init_values(void) //初始化全局变量
srv_general_init(void) //Initialize synchronization primitives, memory management, and thread local storag
srv_init();
初始化各种全局信号量,kernel_mutex、srv_sys以及srv_sys->threads数组中每个slot的event(调用os_event_create初始化),同样的还需要初始化srv_mysql_table数组(类型为srv_slot_t)
dict_ind_init();//初始化dict_ind_redundant和dict_ind_compact,为infimum 和supremum 记录 创建一个名为SYS_DUMMY1/SYS_DUMMY2的表结构,暂时不了解用途。
初始化srv_conc_slots

3.创建临时文件srv_dict_tmpfile和srv_misc_tmpfile,调用函数os_file_create_tmpfile()来创建  
看起来会把一些诸如监控信息写入到这两个临时文件中,暂不清楚用途。

4.检查IO线程数,不应超过SRV_MAX_N_IO_THREADS(130个)
io_limit = 8 * SRV_N_PENDING_IOS_PER_THREAD; //256,表示每个segment(对应一个io线程)最大pending aio
然后做初始化:
    os_aio_init(io_limit,
            srv_n_read_io_threads,
            srv_n_write_io_threads,
            SRV_MAX_N_PENDING_SYNC_IOS);

异步IO线程包括insert buffer thread、log thread及读线程和写线程,另外还有一个os_aio_sync_array(对应AIO类型为OS_AIO_SYNC)

5.初始化fil_system(fil_init)

6.初始化buffer pool 
函数err = buf_pool_init(srv_buf_pool_size, srv_buf_pool_instances);
–>分配每个buffer pool实例的内存
–>buf_LRU_old_ratio_update(100 * 3/ 8, FALSE)  //更新每个bp实例的buf_pool->LRU_old_ratio
–>btr_search_sys_create//初始化adaptive hash index
为btr_search_sys、btr_search_latch_part、btr_search_sys->hash_index分配内存
adaptive hash index占的内存应为buffer pool的1/64,然后在平分到每个ahi实例上(5.5.28之前有bug,是每个ahi实例上为1/64的bp大小,当使用多实例ahi时,可能造成内存消耗过大)

7.
fsp_init() //空函数
log_init() //初始化log_sys
lock_sys_create(srv_lock_table_size); //初始化lock_sys,其中srv_lock_table_size = 5 * (srv_buf_pool_size / UNIV_PAGE_SIZE)

8.创建IO线程,回调函数为io_handler_thread

9.创建或打开ibdata数据文件open_or_create_data_files
–>调用fil_read_flushed_lsn_and_arch_log_no去读取ibdata的第一个page中记录的flushed_lsn,必须保证其他数据文件小于flush_lsn的脏页都被刷到磁盘
–>关闭文件
–>fil_space_create  //初始化ibdata对应tablespace的fil_space_t结构,并插入到fil_system->spaces、fil_system->name_hash和fil_system->space_list中。
–>fil_node_create  //初始化ibdata的fil_node_t并加到space->chain中
如果开启了独立的double write文件,则还需要创建或初始化double write文件(通过innodb_doublewrite_file来控制)

10.依次打开或创建iblog文件,调用函数open_or_create_log_file,同样需要将其加入到fil_system中

11.fil_open_log_and_system_tablespace_files
依次打开space->chain上的每个node对应的文件,也就是前面提到的iblog和ibdata文件,这些文件被一直打开,直到实例被shutdown。

12.检查系统table space的类型是否被支持trx_sys_file_format_max_check

13.buf_pool_invalidate(); //将buffer pool中所有page清空,所有bp中的page必须是可替换的,无脏页且无latch.

14.开始进行崩溃恢复操作,应用redo log
 err = recv_recovery_from_checkpoint_start(LOG_CHECKPOINT,
                              IB_ULONGLONG_MAX,
                              min_flushed_lsn,
                              max_flushed_lsn);
a.创建recovery子系统 recv_sys //recv_sys_create
b.recv_sys_init(buf_pool_get_curr_size())

buf_flush_init_flush_rbt();//初始化红黑树,主要用来加速插入到flush_list上。对应rbt在每个buf_pool->flush_rbt上

确定recv_n_pool_free_frames的大小,该值表示当我们扫描日志并存储扫描到的记录到bp中时,必须至少保留这么多空闲的frame.这样我们就可以把数据page读入内存并执行日志记录

当bp>=10MB时, recv_n_pool_free_frames = 512;

当bp>=32MB时,recv_n_pool_free_frames = 1024;


为recv_sys->buf分配2MB内存,及其他相关结构成员内存分配


c.设置recv_recovery_on = TRUE;表明recovery已经开始了,在很多代码逻辑里,都需要根据这个变量来判断是否处于崩溃恢复状态。

d.从日志组里找到最大的checkpoint
err = recv_find_max_checkpoint(&max_cp_group, &max_cp_field);
背景:一个日志文件头的偏移量相关含义
LOG_GROUP_ID 0 日志组ID
LOG_FILE_START_LSN 4 在该日志文件中数据开始的LSN
LOG_FILE_NO 12 4 byte的归档日志文件号,暂不清楚归档日志用途
LOG_FILE_WAS_CREATED_BY_HOT_BACKUP 16
当日志文件是通过ibbackup –restore产生时,保留32-byte记录了
字符串‘ibbackup’以及日志的创建时间
LOG_FILE_OS_FILE_LOG_BLOCK_SIZE 64 用于记录xtradb的log_block_size,在xtradb中这是可调整的,默认为512
LOG_FILE_ARCH_COMPLETED OS_FILE_LOG_BLOCK_SIZE 4-byte,为true表示归档日志完成 
LOG_FILE_END_LSN OS_FILE_LOG_BLOCK_SIZE + 4 lsn where the archived log file at least extends: actually the 
archived log file may extend to a later lsn, as long as it is 
within the same log block as this lsn; this field is defined 
only when an archived log file has been completely written 
LOG_CHECKPOINT_1 OS_FILE_LOG_BLOCK_SIZE 日志文件中的第一个checkpoint字段,当完成一次新的checkpoint
时,可选的选择是否记录,注意这只记录在第一个日志文件头部
 LOG_CHECKPOINT_2 (3 * OS_FILE_LOG_BLOCK_SIZE) 日志头部的第二个checkpoint字段 
LOG_FILE_HDR_SIZE 4 * OS_FILE_LOG_BLOCK_SIZE 日志头部长度,为4个log block size

在一个while循环中
>>读取每个log头部的checkpoint 字段(log_group_read_checkpoint_info(group, field))
>>验证checksum(函数recv_check_cp_is_consistent,读取头部的LOG_CHECKPOINT_CHECKSUM_1及LOG_CHECKPOINT_CHECKSUM_2)
>>读取checkpoint字段记录的LOG_CHECKPOINT_LSN、LOG_CHECKPOINT_NO、LOG_CHECKPOINT_OFFSET
>>比较checkpoint no,找出最大的那个日志文件

e.从找到的最大checkpoint_lsn开始扫描,需要做一个预处理:

    contiguous_lsn = ut_uint64_align_down(recv_sys->scanned_lsn,
                          OS_FILE_LOG_BLOCK_SIZE);
对contiguous_lsn 以log block size做对齐处理

g.开始遍历日志文件,从contiguous_lsn 开始读取日志记录,函数recv_group_scan_log_recs
>>调用函数log_group_read_log_seg读取一个日志段到内存中,每个段默认4个page(RECV_SCAN_SIZE)
>>扫描日志数据记录 //recv_scan_log_recs

>>>遍历刚刚读取到内存中的日志数据的每一个block

>>>检查block头部的block no以及checksum信息

>>>如果这个block是一次flush操作的开始(log_block_get_flush_bit(log_block)),则意味着该block之前的刷新操作都已经完成了,因此更新contiguous_lsn为scanned_lsn(不太理解)

>>>找到block中第一条mtr日志的起点位置

>>>如果当前scanned_lsn > recv_sys->scanned_lsn,表明有新的entries,随后需要解析这些记录,做崩溃恢复

recv_init_crash_recovery–>fil_load_single_table_tablespaces()

                                              —遍历data目录,读取其中所有的ibd文件

                                              —从每个ibd的第一个page获取space id//fsp_header_get_space_id

                                              —打开文件,创建文件节点加入到fil_system中//fil_load_single_table_tablespace

–>srv_force_recovery小于6(SRV_FORCE_NO_LOG_REDO)时,需要从double write buffer中检查page,如果数据文件中的page是损坏的,则从double write buffer中恢复(调用函数trx_sys_doublewrite_init_or_restore_pages)

       >>>将日志记录拷贝到recv_sys->buf中//recv_sys_add_to_parsing_buf
       >>>recv_parse_log_recs–解析日志记录(recv_parse_log_rec),获取其mtr 类型,space id,page no以及指向日志记录的指针,并将其存储到hash中(recv_add_to_hash_table),用于后续的merge.这里会针对一个mtr有一条还是多条日志记录分别作处理。
      >>>如果存储在hash table中的记录超出限制,则调用recv_apply_hashed_log_recs来应用这些日志  //稍后细述如何应用

h.recv_synchronize_groups(up_to_date_group);//将最新日志组中的信息()

i.其他…

15.dict_boot();//向dict_sys中加载系统表
16.trx_sys_init_at_db_start() //获取undo

a.读取ibdata中的第FSP_TRX_SYS_PAGE_NO个Page,从该page的第TRX_SYS偏移量开始表示
      sys_header = trx_sysf_get(&mtr);

b.trx_rseg_list_and_array_init->trx_rseg_create_instance

遍历128(TRX_SYS_N_RSEGS)个回滚段,读取sys_header存储的每个回滚段slot的page no

如果该回滚段slot没有被使用,即page_no = FIL_NULL,则设置sys->rseg_array[n] = NULL

否则从sys_header头读取该回滚段slot所对应的space id(trx_sysf_rseg_get_space),并在内存中创建回滚段对象(trx_rseg_mem_create),读取所有undo page的信息

>>初始化当前回滚段的trx_rseg_t信息,分配内存,并将其加入到trx_sys->rseg_list中

>>读取回滚段头部数据到内存rseg_header = trx_rsegf_get_new(space, zip_size, page_no, mtr);

>>sum_of_undo_sizes = trx_undo_lists_init(rseg);

根据rseg_header初始化undo log list,并将读取的undo log加入到 rseg->insert_undo_list和rseg->insert_undo_cached中

具体没有深入进去看。

>>获取回滚段trx_rseg_t的last_trx_no、last_del_marks、last_page_no、last_offset,并封装成rseg_queue_t存储到binary heap中,这样便于根据事务id进行排序


c.基于undo log list,构建需要回滚的事务,并加入到trx_sys->trx_list中(trx_list_insert_ordered)
对于处于prepare状态的事务,需要用户在重启后手动commit或rollback掉

d.最后创建purge_sys子系统 //trx_purge_sys_create

17.fsp_header_get_free_limit();//在5.6已经移除了,貌似没啥用,会做一次checkpoint

18.recv_recovery_from_checkpoint_finish()
执行完剩下的redolog(recv_apply_hashed_log_recs,之前是hash 满了才执行)
如果打开了innodb_recovery_stats选项,还会打印一些recovery统计信息、binlog和relay log 信息等
然后设置recv_recovery_on = FALSE;
最后调用trx_rollback_or_clean_recovered(FALSE);  //FALSE表示只回滚ddl操作,DML随后通过独立线程来完成

19.  dict_check_tablespaces_and_store_max_id

检查每个表的tablespace id,如果之前做过crash recovery,需要检查是否和数据词典的一致(fil_space_for_table_exists_in_mem)
如果是正常shutdown,则正常打开表(fil_open_single_table_tablespace)
最后设置fil_system->max_assigned_id

20.recv_recovery_rollback_active();
a.row_merge_drop_temp_indexes(); //删除未创建完成的索引
b.row_mysql_drop_temp_tables(); //删除临时表
c.创建一个单独的线程来对从undo构建的事务做回滚或清理,回调函数为trx_rollback_or_clean_all_recovered

至此recovery的流程算是完成了……剩下的就是创建后台线程等工作

上一篇:ORA-00603 ORA-01092 ORA-600 kcbzib_kcrsds_1


下一篇:mysql – 处理删除/插入/选择一个巨大的表