[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的流程算是完成了……剩下的就是创建后台线程等工作

时间: 2024-08-04 12:43:36

[MySQL学习] Innodb崩溃恢复流程的相关文章

[MySQL 学习] Innodb Optimistic Update流程

更新一条聚集索引记录,接口函数是btr_cur_optimistic_update,这里的更新不涉及到标记删除/插入(二级索引更新或更新主键值,row_upd->row_upd_clust_rec_by_insert->btr_cur_del_mark_set_clust_rec->btr_rec_set_deleted_flag) a.首先判断记录更新是否改变了大小或者需要外部存储,调用函数row_upd_changes_field_size_or_external b.如果a返回的是

[MySQL 学习] Innodb Optimistic Insert流程

通常情况下,插入一条数据的接口函数为btr_cur_optimistic_insert,这时候不需要进行索引树分裂,先来看看这里怎么处理压缩表数据吧 btr_cur_optimistic_insert a. 计算该Page上还能写入的最大空闲空间大小 max_size = page_get_max_insert_size_after_reorganize(page, 1); 以及这条逻辑记录(dtuple_struct)转换成物理记录的大小 rec_size = rec_get_converte

MySQL · 引擎特性 · InnoDB 崩溃恢复过程

在前面两篇文章中,我们详细介绍了 InnoDB redo log 和 undo log 的相关知识,本文将介绍 InnoDB 在崩溃恢复时的主要流程. 本文代码分析基于 MySQL 5.7.7-RC 版本,函数入口为 innobase_start_or_create_for_mysql,这是一个非常冗长的函数,本文只涉及和崩溃恢复相关的代码. 在阅读本文前,强烈建议翻阅下面两篇文章: 1. MySQL · 引擎特性 · InnoDB undo log 漫游 2. MySQL · 引擎特性 · I

MySQL · 引擎特性 · InnoDB崩溃恢复

前言 数据库系统与文件系统最大的区别在于数据库能保证操作的原子性,一个操作要么不做要么都做,即使在数据库宕机的情况下,也不会出现操作一半的情况,这个就需要数据库的日志和一套完善的崩溃恢复机制来保证.本文仔细剖析了InnoDB的崩溃恢复流程,代码基于5.6分支. 基础知识 lsn: 可以理解为数据库从创建以来产生的redo日志量,这个值越大,说明数据库的更新越多,也可以理解为更新的时刻.此外,每个数据页上也有一个lsn,表示最后被修改时的lsn,值越大表示越晚被修改.比如,数据页A的lsn为100

[MySQL学习] Innodb锁系统(3)关键结构体及函数

1.锁对象的定义: 关键结构体: UNIV_INTERN lock_sys_t* lock_sys = NULL; lock_sys是一个全局变量,用于控制整个Innodb锁系统的全部锁结构,其对应的结构体为lock_sys_t,该结构体只包含两个成员: struct lock_sys_struct{     hash_table_t* rec_hash;     ulint rec_num; }; 从函数lock_rec_create可以很容易看出这两个变量的作用: quoted code:

[MySQL学习] Innodb change buffer(2) 相关函数及流程

简单的代码跟踪,顺便弄清了之前一直困惑的bp->watch的用途.... //////////////////////////////// A.相关结构体 在介绍ibuf在Innodb中的使用前,我们先介绍下相关的结构体及全局变量. 我们知道通过Ibuf可以缓冲多种操作类型,每种操作类型,在内部都有一个宏与之对应: IBUF_OP_INSERT IBUF_OP_DELETE_MARK IBUF_OP_DELETE 至于对update操作的缓冲,由于二级索引记录的更新是先delete-mark,再

[MySQL学习] Innodb锁系统(4) Insert/Delete 锁处理及死锁示例分析

A.INSERT 插入操作在函数btr_cur_optimistic_insert->btr_cur_ins_lock_and_undo->lock_rec_insert_check_and_lock这里进行锁的判断,我们简单的看看这个函数的流程: 1.首先先看看欲插入记录之后的数据上有没有锁,    next_rec = page_rec_get_next_const(rec);    next_rec_heap_no = page_rec_get_heap_no(next_rec);  

[MySQL 学习] Innodb锁系统(2)关键函数路径

前提: 以下分析基于标准的配置选项: tx_isolation = REPEATABLE-READ innodb_locks_unsafe_for_binlog = OFF lock->type_mode用来表示锁的类型,实际上lock->type_mode包含了几乎所有锁的模式信息,例如锁类型判断是X锁还是S锁 lock->type_mode &LOCK_TYPE_MASK LOCK_MODE_MASK 0xFUL 用于表示锁模式掩码 LOCK_TYPE_MASK 0xF0UL

[MySQL 学习] Innodb锁系统(1)之如何阅读死锁日志

前言: 最近经常碰到死锁问题,由于对这块代码不是很熟悉,而常持有对文档怀疑的观点.决定从几个死锁问题着手,好好把Innodb锁系统的代码过一遍. 以下的内容不敢保证完全正确.只是我系统学习的过程. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 最近有同学发现,走二级索引删除数据时,两条delete