InnoDB中Redo log设计与实现(一)
简介
首先来看redolog的定义:
A disk-based data structure used during crash recovery, to correct data written by incomplete transactions.
在redolog中最核心的两个概念,一个是lsn,一个是sn,先来看lsn.
Acronym for “log sequence number”. This arbitrary, ever-increasing value represents a point in time corresponding to operations recorded in the redo log.
Lsn比较好理解,就是表示已经写入到redolog的字节数,这个值会随着不断写入而不断的变大.
在看sn之前,我们需要先了解一下redolog基本的写入方式.
在InnoDB中,最小的写入单位是512字节,也就是一个block(OS_FILE_LOG_BLOCK_SIZE), 每一个block都会包含一个12字节的header(LOG_BLOCK_HDR_SIZE),以及4字节的footer(LOG_BLOCK_TRL_SIZE)组成.
由上面所知,我们此时其实需要两个”lsn”,一个是真正应该写入文件的lsn,一个是去掉header和footer的lsn.而sn就是后一个,也就是去掉header和footer的lsn.
源码分析
在InnoDB中redolog的数据结构就是struct log_t,而初始化则是在log_sys_init中进行.
RedoLog主要是用来修改内存中的页以及将每次对于页的修改写入到磁盘以便于recover的时候可以恢复.
RedoLog的初始化流程是这样子的.
- log_sys_init
- log_start
- log_start_background_threads
因此我们就按照这个顺序来分析代码
初始化
所有的初始化都是在log_sys_init这个函数中进行,接下来我会分开来介绍这个函数的实现,首先来看第一部分.
1 | /* Initialize simple value fields. */ |
然后我们来一个个看每个域的含义.
首先是dict_persist_margin
periodical_checkpoints_enabled 这个变量如果被打开则InnoDB将会在每innodb_log_checkpoint_every ms执行一次checkpoint.
format,这个域表示了redolog的格式,这里主要是为了兼容之前的MySQL版本,这个format将会保存在LOG_HEADER_FORMAT中.
然后我们来看InnoDB都有几种format.
1 | /** Supported redo log formats. Stored in LOG_HEADER_FORMAT. */ |
可以看到当前的MySQL 8.0只支持到5.7.9这个版本.
spaceid,之前介绍tablespace的时候我们知道redolog也是一个tablespace,因此这里他需要设置spaceid.
State,这个表示当前log的状态,这里只有两个状态,一个是正常一个是corrupted.
n_log_ios_old用于统计信息,表示上次打印统计信息的时候的io次数.
last_printout_time上次打印统计信息的时候的时间.
然后是第二部分
1 | log.file_size = file_size; |
- file_size表示每个redolog的文件大小(srv_log_file_size)
- n_files表示redolog的个数(srv_n_log_files).
- files_real_capacity则是总的redolog的文件大小.
第三部分
1 | log.current_file_lsn = LOG_START_LSN; |
- current_file_lsn表示当前的lsn,而current_file_real_offset则表示当前lsn下在redolog的偏移.
这里这两个区别在于,有可能会有多个redolog,而lsn是全局的,因此我们需要通过lsn来得到当前的lsn所在的redolog file的偏移.
1 | #define OS_FILE_LOG_BLOCK_SIZE 512 |
- log_files_update_offsets 用来update对应的offset.
第四部分,这部分主要是用来初始化event,这里有这么多event,主要是因为redolog会有好几个线程(后续介绍),然后这些线程之间会有交互,因此这些event就是用来做这些事情.
1 | log.checkpointer_event = os_event_create("log_checkpointer_event"); |
第五部分是初始化sn_lock,这个log主要是保护sn.
1 | log.sn_lock.create( |
第六部分是初始化每个线程使用的锁.
1 | mutex_create(LATCH_ID_LOG_CHECKPOINTER, &log.checkpointer_mutex); |
第七部分是初始化每个线程需要使用的buffer.
- log.buf是上层应用写入到InnoDB的第一站,每一次mtr提交都是先写入到这里.
- srv_log_buffer_size
- log.write_ahead_buf ?
- srv_log_write_ahead_size
- log. checkpoint_buf 主要是用于checkpoint
- OS_FILE_LOG_BLOCK_SIZE
- 可以看到大小也就是redo log中一个block的大小.
1 | /* Allocate buffers. */ |
最后一部分是计算buf大小以及checkpoint相关.
1 | log_calc_buf_size(log); |
根据checkpoint来再次初始化对应的数据
首先是初始化一些统计信息,write_to_file_requests_total表示redolog对于io的请求数量,而write_to_file_requests_interval表示平均的请求数量(ms)。
1 | log.write_to_file_requests_total.store(0); |
然后是初始化一些lsn的信息.
- recovered_lsn表示recover的lsn.
- last_checkpoint_lsn表示最新的进行过checkpoint的lsn.
- next_checkpoint_no表示下一个checkpoint number.
- available_for_checkpoint_lsn表示可以进行checkpoint的lsn
1 | log.recovered_lsn = start_lsn; |
紧接着就是根据传递进来的start_lsn来得到正确的偏移,这里start_lsn有两种偏移需要处理,一种是start_lsn刚好是一个新的block的开始,那么此时lsn则需要跳过block头,还有一种是start_lsn刚好是处于一个block的结尾(不包括footer),那么此时就需要跳过footer+header.
1 | if ((start_lsn + LOG_BLOCK_TRL_SIZE) % OS_FILE_LOG_BLOCK_SIZE == 0) { |
然后就是更新两个关键的数据结构recent_written以及recent_closed,这里简单的介绍下
1 | log.recent_written.add_link(0, start_lsn); |
最后则是写入第一个block,这里可以看到block的写入是直接写到log.buf中.这里之所以需要写入第一个block主要是为了补齐,因为在redolog中写入最小单位就是block,因此这里start_lsn如果不是512对其,那么需要跳过非对其的位置到log.buf,
1 | lsn_t block_lsn; |
下面就是第一个block的内存内容.
header_no 表示当前的redolog的no. 4个字节
1
2
3
4
5
6
7inline void log_block_set_hdr_no(byte *log_block, uint32_t n) {
ut_a(n > 0);
ut_a(n < LOG_BLOCK_FLUSH_BIT_MASK);
ut_a(n <= LOG_BLOCK_MAX_NO);
mach_write_to_4(log_block + LOG_BLOCK_HDR_NO, n);
}flush_bit. 表示是否flush,这里需要注意,最终flush_bit是保存在header_no的最高位.
1 | inline void log_block_set_flush_bit(byte *log_block, bool value) { |
- 数据长度。2个字节
1 | inline void log_block_set_data_len(byte *log_block, ulint len) { |
- 最后是设置rec group。也是2个字节.?
1 | inline void log_block_set_first_rec_group(byte *log_block, uint32_t offset) { |
启动后台工作线程
log_start_background_threads这个函数主要就是用来启动后台线程.
设置对应的标记位置.
1
2
3
4
5
6
7
8log.closer_thread_alive.store(true);
log.checkpointer_thread_alive.store(true);
log.writer_thread_alive.store(true);
log.flusher_thread_alive.store(true);
log.write_notifier_thread_alive.store(true);
log.flush_notifier_thread_alive.store(true);
log.should_stop_threads.store(false);启动线程
这里一共有6个线程.
- log_checkpointer 做checkpoint的线程
- log_closer 关闭整个写入操作的线程
- log_writer 从log.buf写入到磁盘的线程
- log_flusher 刷新redolog到磁盘的线程(sync).
- log_write_notifier 唤醒等到写入的线程
- log_flush_notifier 唤醒等待刷新的线程
1 | os_thread_create(log_checkpointer_thread_key, log_checkpointer, &log); |