InnoDB 的 Redo Log 分析
MySQL版本: 8.0.15
Redo Log 用来记录每次数据操作,用于 Crash 之后做 Recover 恢复操作,而每一条 Redo Log 都是由 mini-transaction 原子提交的.
Redo Log的数据结构
这并不是单个 Redo Log 的数据结构,而是管理 Redo Log 元信息,Redo Log Buffer等操作的系统单元.
1 | include/log0types.h |
mini-transaction
mini-transaction 具体流程
1 | mtr_t mtr |
mini-transaction 的数据结构
1 | struct mtr_t { |
其中m_memo中元素是mtr_memo_slot_t, 记录加锁的对象和加锁的类型.
1 |
|
mini-transaction 的start()
1 |
|
不同的 mini-transaction 如何互斥?
在操作数据前,会根据锁类型,加不同类型的锁,之后将object和锁类型存入m_memo:
1 | mtr_memo_push(mtr, object, type); |
commit完成之后调用release_latches(RELEASE_ALL)将数据上的锁释放.
mini-transaction 插入数据
byte *mlog_open(mtr_t *mtr, ulint size): 打开mtr的m_logmlog_write_initial_log_record_low()函数向m_log中写入type,space id,page no,并增加m_n_log_recs的数量mtr->get_log()->push()按不同的类型写数据mlog_close(): 更新m_log中的位置
mini-transaction 的 commit 过程
commit过程将mini-transaction的m_log数据拷贝到Redo Log Buffer中. 将m_state设置为MTR_STATE_COMMITTING后,调用mtr_t::Command::execute():
mtr_t::Command::execute()
-
prepare_write(): 根据 mtr 的类型m_impl->m_log_mode, 计算 Redo Log 的长度. 假如 Redo Log 记录数目n_recs为1时,设置Flag为MLOG_SINGLE_REC_FLAG, Log记录不止一条时,Flag置为MLOG_MULTI_REC_END. -
假如Redo Log的长度不为0时,
log_buffer_reserve():-
自增Redo Log Buffer中的
sn, 由sn_lock锁保护.sn是一个全局维护的递增编号, 代表不包括 Redo Log Block 头部和尾部的序列号. -
获得handler,计算写入 Redo Log 的
start_lsn和end_lsn,即实际写入的数据大小,lsn代表包括LOG_BLOCK_HDR_SIZE和LOG_BLOCK_TRL_SIZE的 Redo Log 序号,sn`仅考虑 Redo Log 数据内容部分的序号. 转换关系如下:1
2
3
4constexpr inline lsn_t log_translate_sn_to_lsn(lsn_t sn) {
return (sn / LOG_BLOCK_DATA_SIZE * OS_FILE_LOG_BLOCK_SIZE +
sn % LOG_BLOCK_DATA_SIZE + LOG_BLOCK_HDR_SIZE);
} -
假如需要扩展 Redo Log Buffer 的空间长度, 即
end_lsn大于sn_limit_for_end.log_wait_for_space_after_reserving()会进行扩展以及一系列的参数检查. -
因为全局的 Redo Log Buffer 是环形的,假如写的长度超过了 Redo Log Buffer 则需要回环,所以需要
log_wait_for_space_in_log_buf(log, start_sn)等待start_sn之前的Redo Log已经被写入.1
2
3
4
lsn = log_translate_sn_to_lsn(end_sn + OS_FILE_LOG_BLOCK_SIZE - buf_size_sn);
wait_stats = log_write_up_to(log, lsn, false); -
这里可能会和 Redo Log Buffer 允许空洞产生歧义,需要注意的是 Redo Log Buffer 允许的空洞是
write_lsn之后的 Redo Log Buffer 允许空洞,现在的情况是因为一条 Redo Log 的长度超过了Redo Log Buffer的剩余长度需要回环,所以在此之前的Redo Log必须保证写入完成. -
log_write_up_to()需要wait在log_t中的write_events. 当log.write_lsn.load() >= lsn, 即对应于Redo Log Buffer中的slot空间已经完成了写入则被唤醒. -
log_buffer_resize_low()会Resize设置Redo Log Buffer的长度, 释放旧长度的Redo Log Buffer空间,重新分配新长度的Redo Log Buffer空间,并且重新拷贝Redo Log内容. -
对m_log中的每一个512字节的block调用
mtr_write_log_t()(需要注意的是mtr_write_log_t()是运算符()的重载)log_buffer_write()使用memcpy()写Redo Log Buffer.log_buffer_write_completed()更新log_t中的recent_written,即(start_lsn,end_lsn)组成的list.
-
调用
add_dirty_blocks_to_flush_list().
-
-
log_wait_for_space_in_log_recent_closed()查看recent_closed链表是否存在符合该 Redo Log 规则的 space. -
假如Redo Log的长度为0时:
- 直接调用
add_dirty_blocks_to_flush_list().
- 直接调用
-
add_dirty_blocks_to_flush_list():- 假如产生了 Redo Log,则将数据页的
newest_modification修改为end_lsn. - 假如该Block是第一次被修改,就需要插入Buffer Pool的
flush_list. 将涉及修改的数据页添加到 Buffer Pool 的flush_list(buf_flush_insert_into_flush_list()).(利用block->page.oldest_modification来判断是否为第一次修改)
- 假如产生了 Redo Log,则将数据页的
-
log_buffer_close(): 更新log_t中的recent_closed链表. -
release_resources()释放资源, 将m_state置为MTR_STATE_COMMITTED
Redo Log Buffer
Redo Log Buffer是一段内存区域用来存放需要写入ib_logfile的数据. Redo Log Buffer 的大小buf_size可以通过innodb_log_buffer_size来控制, 默认16MB.
Redo Log Buffer的Resize过程

Redo Log Buffer是我们通常所说的回环Buffer, 而在Resize的过程中将log.write_lsn和end_lsn直接的Redo Log拷贝至一个临时的Buffer,然后新建一个new_size的Buffer, 将tmp_buf的数据原路拷贝.
Redo Log模块的线程
log_writer
完成 Redo Log Buffer 的写入, 即写入ib_logfile文件. (log/log0write.cc)
-
log_writer线程wait在一个限定的condition,即直到满足log.write_lsn.load() < log.recent_written.tail()时调用log_writer_write_buffer()进行Redo写入. 指定的condition函数会递增log.recent_written.tail.log.write_lsn代表当前写入的lsn位置,log.recent_written.tail()返回的是 Redo Log Buffer 中最大的不存在空洞的lsn. -
具体写入流程在
log_files_write_buffer(), 首先计算写入在文件的真实偏移:1
2
3
const auto real_offset =
log.current_file_real_offset + (start_lsn - log.current_file_lsn); -
计算目前的
ib_logfile文件是否有足够的空间满足该条Redo Log的写入:- 假如目前的
ib_logfile已经写满,则需要调用start_next_file()直接切换下一个文件. - 假如目前的
ib_logfile还拥有空闲的空间,则需要将Redo Log分两次写入,但本次写入仅填充目前的ib_logfile的剩余空间.
- 假如目前的
-
MySQL8.0在写入Redo Log的过程中引入了
write ahead buffer避免小于 512 bytes的IO造成read on write现象: -
write_blocks()来调用fil_redo_io()来完成文件写入(写入操作系统的Page Cache), 每次写入都是512 Bytes对齐(OS_FILE_LOG_BLOCK_SIZE) -
更新
log.write_lsn -
调用
notify_about_advanced_write_lsn()唤醒对应slot正在wait的线程, 这里其实就是 mini-transaction 的 commit 阶段写入 Redo Log Buffer 中,需要等待log.write_lsn.load() >= lsn的部分. -
唤醒Redo Log的Flush线程(
os_event_set(log.flusher_event))
上图表示Redo Log Buffer,这里需要考虑的是
log.recent_written.tail()也是由log_writer线程来更新的,因为mtr的commit过程根据lsn计算拷贝至Redo Log Buffer的位置,这里是允许空洞的,所以为了能Flush至文件时能Batch无空洞写入,这里由log.recent_written的tail来保证之前tail之前的Buffer是不存在空洞的.
引入write_ahead_buffer的目的是为了避免小IO造成的read on write. 下面为compute_how_much_to_write()的代码片段, 判断是否使用write_ahead_buffer:
1 |
|

上图举例write_ahead_buffer的一次写入过程: 假如需要在real_offset即34274304位置开始写入buffer_size为437大小的数据,假定目前write_ahead_buffer已经被上一次的写入写满,所以本次写入需要重新滑动.
- 将
real_offset向下取整srv_log_write_ahead_size求得last_wa和next_wa的位置, 即last_wa和next_wa的区间为本次的write_ahead_buffer. - 将数据
buffer后面的部分均以0x00填充直到next_wa, 即本次写入的数据为437 + 75 + 512 = 1024大小。75 + 512的部分为log_writer的预写(write ahead). write_ahead_buffer重新滑动后,写入完成后会更新log.write_ahead_end_offset:
1 | static inline void update_current_write_ahead(log_t &log, uint64_t real_offset, size_t write_size) { |
所以log_writer线程的每次写入都是OS_FILE_LOG_BLOCK_SIZE对齐写入,并且大小不会超过srv_log_write_ahead_size.
log_flusher
将 Redo Log Buffer 中的日志进行 Flush, 这里进行的是 Redo Log 的刷脏,与数据脏页的 Flush 无关,数据脏页的 Flush 由 Buffer Pool 刷脏线程处理.
-
log_fluser根据srv_flush_log_at_trx_commit来选择不同的wait方式:-
假如
srv_flush_log_at_trx_commit=1即1
os_event_wait_time_low(log.flusher_event, flush_every_us - time_elapsed_us, 0);
-
否则:
1
const auto wait_stats = os_event_wait_for(log.flusher_event, max_spins, srv_log_flusher_timeout, stop_condition);
-
-
假如
last_flush_lsn < log.write_lsn.load(),即需要进行刷盘. -
fil_system->flush_file_redo()进行文件刷盘. -
更新
log.flushed_to_disk_lsn. -
唤醒wait在该
slot[last_flush_lsn, flush_up_to_lsn]的用户线程.
log_closed
更新log.recent_closed的tail.
log_checkpointer
进行checkpoint的线程
- 更新
available_for_checkpoint_lsn, 即目前可以安全进行checkpoint的lsn. - 扫描所有Buffer Pool的
flush_list,获取最旧的一条Redo Log的lsn(bpage = UT_LIST_GET_LAST(buf_pool->flush_list). 这里最旧的lsn并不代表lsn是最小的,因为插入flush_list是允许并发插入的,所以无法保证flush_list中的Redo Log按照lsn的顺序排列. flush_list中最旧的lsn减去recent_closed的长度,然后与上次checkpoint的lsn进行比较, 选较大的lsn_t lwm_lsn = (std::max(checkpoint_lsn, lsn - lag)).- 与
recent_closed.tail比较(const lsn_t dpa_lsn = log.recent_written.tail()),选较小的(lwm_lsn = std::min(lwm_lsn, dpa_lsn). - 与
log.flushed_to_disk_lsn比较,选较小的(std::min(lwm_lsn, flushed_lsn)). - 更新
log.available_for_checkpoint_lsn. - 计算
current_lsn, 与log.available_for_checkpoint_lsn比较,假如在此期间又有脏页被刷入flush list, 则一并进行预Flush. 之后更新log.available_for_checkpoint_lsn. - 检查是否需要checkpoint
log_checkpoint(log)进行checkpoint, 其中就是调用接口将checkpoint的信息写入指定的文件.
recent_written 与 recent_closed 的作用
recent_written
- MySQL 8.0通过直接计算每一条 Redo 在 Redo Log Buffer 的 offset 来并发插入 Redo Log Buffer, 所以这里是允许 Redo Log Buffer 存在空洞的,而写入
ib_logfile不允许,所以利用recent_written.tail来保证在此 lsn 之前的 Redo Log Buffer 是不存在空洞的,从而完成ib_logfile的完整写入.
recent_closed
- 为了能安全的进行 checkpoint,需要选择一个数据已经被 Flush 的 Redo Log 的lsn,所以选择所有 Buffer Pool 的
flush_list中头部最小的一个 Dirty Page 的 lsn, 减去recent_closed的capacity(),可以认为是一个安全的checkpoint_lsn.
Redo Log 中 Record 的格式
MLOG_REC_INSERT的Redo格式

Q&A
-
Redo Log 文件大小设置过小造成性能抖动?
假如 Redo Log 文件大小设置过小,会造成频繁的 checkpoint, 而 checkpoint 的推进前提是对应 Buffer Pool 的脏页已经完全落盘,所以 Redo Log 的落盘会间接推动脏页的 Buffer Pool 的落盘, 从而影响系统的 IO,造成性能抖动.
2020 Leviathan