MySQL 日志篇:Redo 相关线程

在 MySQL 中,用户线程开启事务更改数据时,系统内部会生成相应的 Redo Record。为了保证事务的持久性,这些 Redo Record 需要以 Redo Log 的形式在事务提交之前写入磁盘 (也称为“落盘”)。

为了提高事务的吞吐率 (单位时间内系统处理的事务数量),在不违背 Redo Log 落盘必须先于事务提交的前提下,需要尽可能快地使 Redo Log 落盘。

MySQL 8.0 使用多个线程协同工作来完成 Redo Log 的落盘。本文将介绍这些 Redo 相关线程。如果还不了解 LSN、Log Buffer、Redo 文件格式等概念,推荐阅读 MySQL 日志篇:Redo Log BufferMySQL 日志篇:Redo 文件和自适应检查点

Redo 相关线程总览

MySQL 8.0 中与 Redo Log 相关的线程如下图所示 (图片来自 MySQL 官方文档)。

其中:

  • log writerlog flusherlog write notifierlog flush notifier 是 Redo 相关线程 (这里没有画出 log checkpointerlog files governor 线程);
  • buf_ready_for_write_lsnwrite_lsnflushed_to_disk_lsn 是不同的 LSN,分别由特定的线程读取和更新;
  • Log Buffer 是在内存中存放 Redo Log 的容器,Recent Written Buffer 用来维护 Redo Log 写入 Log Buffer 的进度,Redo Log 最终被写入磁盘中的 Redo 文件;
  • write eventflush event 用来使线程等待或唤醒线程。

一个用户事务在更改数据时,MySQL 内部会创建若干个 mini-transaction (简称为 mtr) 在数据页上原子地执行更改动作。随之生成的 Redo Record 会存放在 mtr 的内部缓冲区中,mtr 提交时会将其内部缓冲区中的 Redo Record 一次性写入 Log Buffer,并向前推进 buf_ready_for_write_lsn

write_lsn 落后于 buf_ready_for_write_lsn 时,log writer 线程首先把 Log Buffer 中连续的 Redo Log 写入操作系统的 Page Cache,然后向前推进 write_lsn,最后唤醒 log flusherlog write notifier 线程。

flushed_to_disk_lsn 落后于 write_lsn 时,log flusher 线程通过执行 fsync() 函数强制将 Page Cache 中的数据页刷入磁盘,此时 Redo Log 才真正落盘。随后,log flusher 线程向前推进 flushed_to_disk_lsn 并唤醒 log flush notifier 线程。

最后,log write notifier/log flush notifier 线程会唤醒用户线程。用户线程被唤醒后比较自身事务的 commit_lsnwrite_lsn/flushed_to_disk_lsn,如果 commit_lsn 小于或等于 write_lsn/flushed_to_disk_lsn,那么事务对应的 Redo Log 已经写入 Page Cache/磁盘,此时可以提交事务。如果 commit_lsn 仍大于 write_lsn/flushed_to_disk_lsn,那么用户线程继续等待下一次唤醒。MySQL 使用 innodb_flush_log_at_trx_commit 参数控制是由 log write notifier 线程还是由 log flush notifier 线程唤醒用户线程,即事务提交的时机。

log writer 线程

下图从代码的角度展示了 log writer 线程的执行细节。

log writer 线程的执行流程如下:

  1. 调用 log_advance_ready_for_write_lsn() 函数向前推进 buf_ready_for_write_lsn,从而确定 Log Buffer 中已写完的连续区域 1 \textcolor{red}{^1} 1
  2. 调用 log_writer_wait_on_checkpoint() 函数检查 Redo 文件中是否有足够的空闲空间,若空闲空间不足则等待;
  3. 调用 compute_how_much_to_write() 函数计算本次要写的 Redo Log 的长度 2 \textcolor{red}{^2} 2,同时决定是否需要使用 Write-Ahead Buffer;
  4. 调用 prepare_full_blocks() 函数填入 Log Buffer 中每个 Redo Block 的头部和尾部 3 \textcolor{red}{^3} 3
  5. 若要使用 Write-Ahead Buffer,则调用 copy_to_write_ahead_buffer() 函数将 Redo Log 从 Log Buffer 复制到 Write-Ahead Buffer;
  6. 调用 write_blocks() 函数把 Log Buffer 或 Write-Ahead Buffer 中的 Redo Log 写入 Page Cache;
  7. 向前推进 write_lsn
  8. 调用 notify_about_advanced_write_lsn() 函数唤醒 log flusher 线程和 log writer notifier 线程;
  9. 调用 log_update_buf_limit() 函数向前推进 log.buf_limit_sn 4 \textcolor{red}{^4} 4
  10. 调用 update_current_write_ahead() 函数更新 log.write_ahead_end_offset

1 \textcolor{red}{1} 1:若遇到了 Recent Written Buffer 中的“空洞”或者已经向前推进了 4 KB 的距离,则停止向前推进 buf_ready_for_write_lsn

2 \textcolor{red}{2} 2:这里限制一次只能写一个 Redo 文件。若 Redo Log 的长度超出当前 Redo 文件的空闲空间大小,则多出的 Redo Log 被推迟到下次。

3 \textcolor{red}{3} 3:除了每个 Redo Block 头部的 first_rec_group 字段,该字段由用户线程填入。

4 \textcolor{red}{4} 4:用户线程把 mtr 中的 Redo Record 复制到 Log Buffer 时,实际要通过 log.buf_limit_sn 确定 Log Buffer 是否有足够的空闲空间。

这里不会再继续深入代码细节,有兴趣的小伙伴可以自行阅读 MySQL 源码。下面将聚焦于 log writer 线程使用的 Write-Ahead Buffer。

Write-Ahead Buffer 有点像 Log Buffer,但它们的职责不同:

  • Log Buffer 负责存放 Redo Log,用户线程作为“生产者”向 Log Buffer 写入 Redo Log,log writer 线程作为“消费者”从 Log Buffer 取出 Redo Log;
  • Write-Ahead Buffer 负责辅助 log writer 线程将 Redo Log 写入 Page Cache,用于写不完整的 Redo Block 以及解决 Read-on-Write 问题。

写不完整的 Redo Block

log writer 线程写 Redo Log 时,在 write_lsnbuf_ready_for_write_lsn 区间内的最后一个 Redo Block 可能还未被写满,如下图所示。

此时对于最后一个 Redo Block:

  1. 将其在 write_lsnbuf_ready_for_write_lsn 区间内的部分复制到 Write-Ahead Buffer 中;
  2. 用 0 填充该 Redo Block 剩余的部分;
  3. 填充该 Redo Block 的头部和尾部。

这种情况下,借助 Write-Ahead Buffer 可以写完 write_lsnbuf_ready_for_write_lsn 区间内的 Redo Log (即使最后一个 Redo Block 还未写满),从而尽快唤醒正在等待的用户线程。

解决 Read-On-Write 问题

Write-Ahead Buffer 的另一个作用是解决 Read-on-Write 问题。Read-on-Write 问题发生在写文件时:若要写的数据量小于 4 KB,并且对应的数据页还不在 Page Cache 中,则操作系统首先将该数据页从磁盘读入 Page Cache,然后用新数据覆盖对应区域,最后把该数据页从 Page Cache 写回磁盘。如下图所示,这会导致多出一次读操作。

只要出现以下情况之一,就不会出现 Read-on-Write 问题:

  • 情况一:要写的数据页已经在 Page Cache 中;
  • 情况二:要写的数据刚好是一个完整的数据页。

因此,解决 Read-on-Write 问题实际就是依照上述情况去写 Redo Log,这需要借助 Write-Ahead Buffer 和 log.write_ahead_end_offset 变量。

log writer 线程使用 log.write_ahead_end_offset 变量确认当前正在写的数据页。每次开始写一个新的 Redo 文件时,将 log.write_ahead_end_offset 置为 0;每次开始写一个新的数据页时,将 log.write_ahead_end_offset 加 4 KB。log writer 线程写 Redo Log 时,先调用 log.m_current_file.offset() 函数基于起始 LSN 5 \textcolor{red}{^5} 5 算出其在 Redo 文件中的偏移量,再根据偏移量、Redo Log 长度和 log.write_ahead_end_offset 决定如何写 Redo Log:

  • 若偏移量小于 log.write_ahead_end_offset,则当前数据页还未写完 (情况一,当前数据页已在 Page Cache 中):
    • 若偏移量加上 Redo Log 长度 ≤ \le log.write_ahead_end_offset
      • 且 Redo Log 长度 ≥ \ge 512,则本次只写完整的 Redo Block,最后一个未写满的 Redo Block 放到下次写;
      • 且 Redo Log 长度 < < < 512,则将 Redo Log 复制到 Write-Ahead Buffer 中,补齐到 512 字节后再写。
    • 若偏移量加上 Redo Log 长度已超过 log.write_ahead_end_offset,则本次只写满当前数据页,剩下的 Redo Log 放到下次写。
  • 若偏移量等于 log.write_ahead_end_offset,则当前数据页已经写完 (情况二,下一数据页不在 Page Cache 中):
    • 若偏移量加上 Redo Log 长度 ≥ \ge 4 KB,则本次只写 4 KB 大小的 Redo Log,剩下的 Redo Log 放到下次写;
    • 若偏移量加上 Redo Log 长度 < < < 4 KB,则将 Redo Log 复制到 Write-Ahead Buffer 中,补齐到 4 KB 后再写。

5 \textcolor{red}{5} 5:起始 LSN 等于 write_lsn - write_lsn % 512,即按 Redo Block 大小对齐。

上述使用 Write-Ahead Buffer 的两种情况如下图所示。

注意,写第一个 Redo Block 时,由于对应的数据页还不在 Page Cache 中,因此实际写了 4 KB 的数据。写第二个 Redo Block 时,由于对应的数据页已经在 Page Cache 中,因此实际写了 512 B 的数量。

log write notifier 线程

log write notifier 线程负责唤醒等待 Redo Log 写到 Page Cache 的用户线程。用户线程在提交事务前需要等待 write_lsn 追上 commit_lsn (commit_lsn 标识该事务对应的 Redo Log 的终止 LSN)。MySQL 基于 commit_lsn 把多个用户线程放到不同的 slot 中 6 \textcolor{red}{^6} 6,每个 slot 都有自己的 write event,位于相同 slot 中的用户线程都在该 slot 的 write event 上等待被唤醒。log write notifier 线程被 log writer 线程唤醒后,先根据老的 write_lsn 和新的 write_lsn 确定本次向前移动所经过的 slot,再唤醒这些 slot 中所有的用户线程。

6 \textcolor{red}{6} 6:将 commit_lsn 除以 512 后再对 slot 个数取模 (即, s l o t = c o m m i t _ l s n − 1 512   %   s l o t _ c o u n t \mathrm{slot} = \frac{commit\_lsn-1}{512}\ \%\ \mathrm{slot\_count} slot=512commit_lsn1 % slot_count),就得到用户线程被放入的 slot 位置 (参见 log_compute_wait_event_slot() 函数)。

注意,用户线程可能被误唤醒。以下图中的红色实心圆标识的用户线程为例,其 commit_lsn 仍大于新的 write_lsn。因此,用户线程被唤醒后还要再次检查新的 write_lsn 是否追上自身的 commit_lsn,若没有则继续等待。

log flusher 线程比较简单,只是当 write_lsn 超过 flushed_to_disk_lsn 时在当前打开的 Redo 文件上执行 fsync() 函数。log flush notifier 线程和 log write notifier 线程类似,只是把 write_lsn 换成了 flushed_to_disk_lsn。这里不再赘述。


欢迎关注微信公众号:fightingZh

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值