innodb重做日志实现原理(下)

1.概述

上一篇介绍了重做日志写入相关原理,本文主要介绍如何从磁盘进行重做日志的恢复。做好数据的恢复,才能保证节点故障不会丢数据。

2.实现细节

每次innodb启动的时候都会尝试进行重做日志的恢复。

We always try to do a recovery, even if the database had been shut down normally: this is the normal startup path

核心就是通过检查点从磁盘中加载数据到内存。

log_checkpointer线程负责检查点的写入,有相应的判断算法, 本质就是已经持久化到磁盘的脏页对应的lsn会被写入检查点,如果系统故障,我们从此对应lsn恢复即可保证数据不丢失。

3.源码解析

fil_write_flushed_lsn_to_data_files

数据库正常结束之前,会调用该方法,将lsn写入表空间,该lsn主要用来判断是否需要进行数据的的恢复。如果正常结束,lsn和重做日志检查点一致(正常shutdown会将buffer pool刷新到磁盘并且更新检查点),就不需要进行数据恢复,如果不一致,则说明数据库异常关闭,则需要进行数据的恢复。

recv_recovery_from_checkpoint_start

数据恢复主要通过此方法实现

  1. 首先创建并初始化recv_sys数据结构,该数据结构主要用来数据的恢复。所有等待恢复的日志数据最终都先加载到redolog buf,再解析buf到recv_sys的哈希表中。最终通过哈希表存储的日志数据,来进行数据的恢复。为什么要用hash?以为对于相同页的数据,方便查找。
1
2
3
4
cpp复制代码if (type == LOG_CHECKPOINT) {
    recv_sys_create();
    recv_sys_init(FALSE, buf_pool_get_curr_size());
}

哈希表结构,n_cells为槽数量,array为槽数据,每个槽存放一个链表,解决hash冲突。链表的每个node存储对应块的日志信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cpp复制代码struct hash_table_struct {
    ibool        adaptive;/* TRUE if this is the hash table of the
                adaptive hash index */
    ulint        n_cells;/* number of cells in the hash table */
    hash_cell_t*    array;    /* pointer to cell array */

    ulint        n_mutexes;/* if mutexes != NULL, then the number of
                mutexes, must be a power of 2 */
    mutex_t*    mutexes;/* NULL, or an array of mutexes used to
                protect segments of the hash table */
    mem_heap_t**    heaps;    /* if this is non-NULL, hash chain nodes for

                external chaining can be allocated from these
                memory heaps; there are then n_mutexes many of
                these heaps */
    mem_heap_t*    heap;
    ulint        magic_n;

};
  1. 查找最大的checkpoint

redolog-group.png
如上图,因为innodb会保存两个checkpoint,所以需要从所有group找到最大的那个。该方法比较简单,其实就是遍历所有group,从文件读取checkpoint信息到对应的checkpoint_buf。然后对数据进行一致性check。找到最大的checkpoint,返回该checkpoint对应的group和field。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cpp复制代码for (field = LOG_CHECKPOINT_1; field <= LOG_CHECKPOINT_2; field += LOG_CHECKPOINT_2 - LOG_CHECKPOINT_1) {
            log_group_read_checkpoint_info(group, field);
            if (!recv_check_cp_is_consistent(buf)) {
                goto not_consistent;
            }
            group->state = LOG_GROUP_OK;
            group->lsn = mach_read_from_8(buf + LOG_CHECKPOINT_LSN);
            group->lsn_offset = mach_read_from_4(buf + LOG_CHECKPOINT_OFFSET);
            checkpoint_no = mach_read_from_8(buf + LOG_CHECKPOINT_NO);
            if (ut_dulint_cmp(checkpoint_no, max_no) >= 0) {
                *max_group = group;
                *max_field = field;
                max_no = checkpoint_no;
            }
        not_consistent:
            ;
}
  1. log_group_read_checkpoint_info,根据返回的group和field,读取该checkpoint的信息。
1
2
3
4
5
6
7
8
9
10
cpp复制代码void log_group_read_checkpoint_info(
    log_group_t*    group,    /* in: log group */
    ulint        field)    /* in: LOG_CHECKPOINT_1 or LOG_CHECKPOINT_2 */

{
    log_sys->n_log_ios++;
    fil_io(OS_FILE_READ | OS_FILE_LOG, TRUE, group->space_id,
            field / UNIV_PAGE_SIZE, field % UNIV_PAGE_SIZE,
            OS_FILE_LOG_BLOCK_SIZE, log_sys->checkpoint_buf, NULL);
}
  1. recv_group_scan_log_recs

该方法主要是根据读取到的信息扫描并加载group保存的日志信息,最终插入到recv_sys的hash表中。整个流程如下:

(1)调用log_group_read_log_seg读取group日志到log_sys->buf

1
2
3
4
5
6
7
8
9
10
11
cpp复制代码while (!finished)
{
    end_lsn = ut_dulint_add(start_lsn, RECV_SCAN_SIZE);
    log_group_read_log_seg(LOG_RECOVER, log_sys->buf, group, start_lsn, end_lsn);
    finished = recv_scan_log_recs(TRUE,
    (buf_pool->n_frames - recv_n_pool_free_frames) * UNIV_PAGE_SIZE,
    TRUE, log_sys->buf,
    RECV_SCAN_SIZE, start_lsn,
    contiguous_lsn, group_scanned_lsn);
    start_lsn = end_lsn;
}

(2)调用recv_scan_log_recs,循环处理log_sys->buf中所有log_block。

每次循环的逻辑如下:

先对日志的校验操作。如果校验不成功则恢复失败。对于校验通过的log_block,调用recv_sys_add_to_parsing_buf将log_block的buf数据拷贝到recv_sys的buf中。

recv_sys_add_to_parsing_buf方法逻辑:

主要是计算拷贝的start_offset和end_offset,然后进行调用memcpy方法拷贝。因为lsn包括了头部和尾部的数据,但是拷贝的时候只需要数据部分,所以需要进一步计算才可以。

(3)调用recv_parse_log_recs解析所有日志,并将日志存储到hash table。

主要就是遍历所有日志,根据日志规则解析出日志的type,space,page_no,然后调用recv_add_to_hash_table初始化hash结构并插入hash表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cpp复制代码old_lsn = recv_sys->recovered_lsn;
len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body);
if (recv_sys->found_corrupt_log) {
recv_report_corrupt_log(ptr,
type, space, page_no);
}

ut_a(len != 0);
ut_a(0 == ((ulint)*ptr & MLOG_SINGLE_REC_FLAG));
recv_sys->recovered_offset += len;
recv_sys->recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len);
if (type == MLOG_MULTI_REC_END) {
/* Found the end mark for the records */
break;
}
if (store_to_hash) {
recv_add_to_hash_table(type, space, page_no, body, ptr + len, old_lsn, new_recovered_lsn);
}
ptr += len;
  1. recv_apply_hashed_log_recs

这个方法将hash table中的数据刷新到page中,进行日志的恢复。遍历所有的hashtable,对于在内存中的page,直接调用recv_recover_page进行恢复,对于不在内存中的页调用recv_read_in_area方法。

(1)先说recv_read_in_area这个方法

这个方法主要就是遍历该页所相邻的32个页,如果此页不在内存中,则将页编号其加入到page_nos数组,然后异步加载页并刷新数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cpp复制代码static ulint recv_read_in_area(
ulint space, /* in: space */
ulint page_no)/* in: page number */
{
recv_addr_t* recv_addr;
ulint page_nos[RECV_READ_AHEAD_AREA];
ulint low_limit;
ulint n;
low_limit = page_no - (page_no % RECV_READ_AHEAD_AREA);
n = 0;
for (page_no = low_limit; page_no < low_limit + RECV_READ_AHEAD_AREA;page_no++) {
recv_addr = recv_get_fil_addr_struct(space, page_no);
if (recv_addr && !buf_page_peek(space, page_no)) {

mutex_enter(&(recv_sys->mutex));

if (recv_addr->state == RECV_NOT_PROCESSED) {
recv_addr->state = RECV_BEING_READ;

page_nos[n] = page_no;
n++;
}

mutex_exit(&(recv_sys->mutex));
}
}
buf_read_recv_pages(FALSE, space, page_nos, n);
return(n);
}

(2)recv_recover_page

页的恢复逻辑。启动mini事务,设置为非log模式,也就是恢复时候不需要再记录重做日志。

核心就是调用recv_parse_or_apply_log_rec_body。该方法是根据重做日志的类型进行不同的恢复操作。细节后续会说。写入完成之后,更新page的checksum以及lsn。并将其插入到flush列表等待刷新,最终提交mini事务。

总结

文章串了一下整个恢复的流程,根据检查点从redolog文件记载道redo log的buf,然后读取buf到recv_sys中。整个恢复操作围绕recv_sys开展,对于在内存中的页,直接刷新数据,对于不在内存的页异步去做。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%