MySQL 8.0 InnoDB无锁化设计的日志系统( 三 )


在这种设计模式下,用户线程只负责写日志到log_buffer中,日志的刷新和落盘是完全异步的,根据innodb_flush_log_at_trx_commit定义的不同行为,用户线程在事务提交时需要等待日志写入操作系统缓存或磁盘 。
在8.0之前,是由用户线程触发fsync或者等先提交的线程执行fsync( Group Commit行为),而在MySQL 8.0中,用户线程只需要等待flushed_to_disk_lsn足够大即可 。

MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
8.0中采用了一个分片的消息队列来通知用户线程,比如用户线程需要等待flushed_to_disk_lsn >= X那么就会加入到X所属的消息队列 。分片可以有效降低消息同步损耗及一次需要通知的线程数 。
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
在8.0中,由后台线程log_flush_notifier通知等待的用户线程,用户线程、log_writer、log_flusher、log_flush_notifier四个线程之间的同步关系为 。
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
8.0中为了避免用户线程在陷入等待状态后立即被唤醒,用户线程会在等待前做自旋以检查等待条件 。8.0中新增加了两个Dynamic Variable: innodb_log_spin_cpu_abs_lwm 和innodb_log_spin_cpu_pct_hwm控制执行自旋操作时CPU的水位,以免自旋操作占用了太多的CPU 。
3、flush list 并发控制以及check point 推进
回到上面的MTR提交的代码,可以看到在将Redo Log写入全局的log buffer中以后,mtr立即开始了将脏页加入到flush list的步骤,其过程分为三个函数调用 。
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
这里同样是通过一个Link_Buf类型的无锁结构recent_closed来跟踪处理flush list并发写入状态 。
假设MTR在提交时产生的redo log的范围是[start_lsn, end_lsn],MTR在将这些redo对应的脏页加入到某个flush list后,立即将start_lsn到end_lsn这段标记在recent_closed结构中 。recent_closed同样在内部维护了变量M,M对应着一个LSN,表示所有小于该LSN的脏页都加入到了flush list中 。
而与redo log写入不同的是,MTR在写入flush list之前,需要等待M值与start_lsn相差不是太多才可以写入 。这是为了将flush list上的空洞控制在一个范围之内,这个过程的示意图如下:
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
MTR在写入到flush list之前,需要等待M值与start_lsn的相差范围是一个常数L,这个常数度量了flush list中的无序度,它使得checkpoint的确定变得简单(实际代码中,L值就是recent_closed内部容量大小) 。
从上面的代码可以看到,在8.0中实际上加入到flush list的行为并不是完全并发的,但也不是5.7中完全串行的,而是被控制到一个范围L之内的并行写入 。
由于MTR需要等待条件start_lsn - M < L成立才能加入到flush list , 反过来说,对于flush list中的每个Page ,如果其对应的修改的LSN为Ln,那么可以断定Ln - L对应的Page一定已经加入到了flush list中,而且一定在当前Page之前(因为Page添加时的检查条件Ln-L < M,M之前是无空洞连续的LSN) 。
也就是说,在延续原有的按flush list的顺序刷新脏页到磁盘的策略不变的情况下,只需要将Checkpoint的推进由原来的Page对应的LSN改成LSN-L即可 。
MySQL 8.0中实际实现的时候,Checkpoint推进仍然是按照Page对应的LSN写入的,只不过Recover的时候从Checkpoint - L开始执行,这两张方式实际上是等效的 。
不过在MySQL 8.0中,Recover阶段从Checkpoint - L的地方开始,可能会遇到Checkpoint -L是某个Redo的中间位置而不是开始位置的情况,所以要对一些边界情况做一些额外的工作才行 。
四、总结
对于InnoDB存储引擎,Redo Log的处理是实现事务持久性的关键,在MySQL 5.7及以前,通过两个全局锁,实际上使MTR的提交过程串行化保证了RedoLog以及脏页处理的正确性,这使得MTR的提交过程因为锁竞争的缘故无法充分的发挥多核的优势 。
8.0中通过引入的Link_buf 数据结构将整个模块变成了Lock_free的模式,必然会带来性能上的提升 。
参考