New Lock free, scalable WAL design of MySQL 8


New Lock free, scalable WAL design of MySQL 8

0x00 引言

这里并不是一篇Paper,而是来自MySQL的一篇Blog,主要讨论了在MySQL中WAL的一些改进,主要是为了去除为了保存log中LSN严格有序和没有空洞的要求而引入的两个mutex。这个优化在多核和高速存储硬件的环境下多性能的提升比较大。前面的文章[2]也有关系优化WAL的一些思路。在MySQL中,日志的产生和写入磁盘可以看作是一个生产者、消费者模型。工作线程处理事务的时候产生日志数据,在系统Crash的情况下,这些日志会被使用用于恢复到Crash之前到状态。这里的优化点就是优化多个生产者情况下处理的性能。这里由于一般数据库系统和MySQL自己设计中,写入日志必须有这样的一些要求,

  • 在MysQL中,被修改过的Page为dirty page,这些Page保存在一个flush list中,且必须按照LSN递增的方式排列。
  • 在MySQL中,MTR是一个最小的事务处理单元,但不并不是只会包含一条日志。MySQL在这里的时候,为了保证一个MTR的日志作为一个整体被持久化,这里先将日志写入到线程本地的Cache中,然后拷贝到全局的Log Buffer中。这里持久化日志的日志,日志必须按照LSN的顺序排列。

    MySQL 8之前的设计这里利用了log_sys_t::mutex 和log_sys_t::flush_order_mutex两个mutex来保证这些要求。基本的操作如下,在MTR将dirty page添加到flush list中的时候,会持有log_sys_t::flush_order_mutex的锁。其它准备添加dirty page到flush list中的线程会等待这个mutex,这个线程会持有log_sys_t::mutex的锁,所以另外的准备写Log Buffer的线程必须等待这个log_sys_t::mutex。基本的思路如下,下面的flush list不只一个,但是都会在一个mutex的保护下面。移除这两个mutex带来的第一个问题就是flush list中diry page的顺序得不到保证,另外的一个问题是线程可以并发写Log Buffer的话,会存在Log Buffer中有空洞的问题。这个在前面的[2]中也讨论过。

mysql8-redo-old-design-flow

0x01 空洞问题处理

这里先谈了第二个问题如何处理。MySQL 8中新引入了一种叫做Link_buf的无锁数据结构,第二个问题的解决就利用了这个数据结构。这个数据结构可以看作是一个环形的数组。这个数组中的元素称之为slot,每个slot的更新都是原子的,环形的结构会循环使用。一个单独的线程遍历这个数据处理其中的元素,在遇到一个空的slot的时候会定下来等待。这里使用了两个这样的结构来处理,recent_written用于记录日志写入到Log Buffer中的完成状态,其维护的一个值M,表示来LSN小于这个M的日志记录都已经写入到了Log Buffer中。这里的处理和刷日志到磁盘的工作由一个线程来处理。在处理slot的数据的时候需要设置合适的内存屏障。

mysql8-link_buf

如下图所示,Log Buffer中的空洞数据被写入完成之后,线程可以根据下面第二个图中绿色框表示的那样已经知道的写入到Log Buffer的LSN向前推进,

mysql8-link_buf2

上图5后面中有2个为空洞的位置,导致buf_ready_for_write_lsn无法向前推进。在写完成之后,下面的数据结构更新,可以根据这个结果的信息更新buf_ready_for_write_lsn,

mysql8-redo-next-write-to-log-buffer-2

0x02 Flush List顺序问题处理

recent_closed则用来解决移除log_sys_t::flush_order_mutex会导致的问题。这里为例保证系统的正确性,flush list相关的两个要求如下:1. Checkpoint,为了保证Checkpoint的正确性,加入dirty page P1的上面的LSN小于P2,则必须保证P2不能在P1之前刷盘。2. FLushing,刷盘的时候最旧的dirty page必须先落盘。这两个要求感觉实际上就是同一个要求。recent_closed用来追踪最大的LSN(maximum LSN),也计为M,写入到flush list的情况。表示小于M的都已经添加到flush list中了。而当前线程处理的LSN要和M差距在一个定义的范围之内的时候,相关的page才能添加到flush list中,在需要的时候更新M的信息。

mysql8-redo-new-flush-lists-1

举个例子来说明这个设计。一个MTR在提交的时候,写入其日志到Log Buffer中,假设其日志的LSN在start_lsn 到 end_lsn之间,像前面一节说的那样,在完成recent_written的操作之后。在操作这部分的时候,如果有start_lsn – M < L,这个MTR的操作就必须被阻塞,一直等待到这个条件被满足。在添加到了flush list的时候,原来的设计保证了oldest_modification >= last_lsn,而现在的设计保证oldest_modification >= last_lsn – L,而其中的oldest_modification维护的是最早更改的信息,而last_lsn表示最早diry page的LSN信息。可以理解为这里flush不在时全局有序,而是无序的范围被限制在了L的范围内。这样在恢复的是需要一些额外的操作找到MTR恢复的起始点。这里的操作完成之后,start_lsn 到 end_lsn的信息被反馈给recent_closed,在这个recent_closed上面工作的log_closer线程就可以根据这个信息推进其维护的M信息。

0x03 整体设计

在上面两个基本的优化措施之后,这里引入了额外的一些改进,指定一些线程完成特定的任务,实际上就是一个原来的任务的拆解,形成了MySQL 8WAL的总体设计。

  • log_writer线程,负责将 log buffer写入到操作系统的page cache。在MySQL 8之前,写Log Buffer的动作是在需要写数据的时候发生的,而且是整个的Log Buffer写入。MySQL 8中的设计是有指定的log_writer线程操作,这样的话可以讲写数据的时间提前。在写入完成的时候,log_writer线程会负责write_lsn的信息。

    mysql8-log-writer

  • log_flusher线程,log_flusher线程在接受到log_writer线程更新的信息之后,使用fsync操作将数据落盘,并更新flushed_to_disk_lsn。在这样的设计下面,写入数据到Page Cache和实际的数据落盘是两个不同的线程完成的。它们有自己工作的“速度”。在一个事务提交的时候,这个事务的最后一个MTR,需要等待其end_lsn落盘。之前的设计这里的fsync操作是由用户线程触发,或者是等待一个全局的IO完成事件,有其它早提交的事务的fsync操作完成。MySQL 8的设计中,log_flusher负责将数据落盘,并更新flushed_to_disk_lsn信息。所以这里就只需要等待这个flushed_to_disk_lsn被更新到足够到即可。

    mysql8-waiting

  • 这里通知在等待的线程的工作是有另外一个指定的线程完成的,这个线程称之为log_flush_notifier线程。这里为了避免一些等待,引入了一种自旋一段时间的优化方式,在合适的情况下吗会自旋一段时间,不满足条件之后才会进入等待状态。并引入了innodb_log_spin_cpu_abs_lwm 和 innodb_log_spin_cpu_pct_hwm设置自旋的一些参数。

  • log_checkpointer用来处理新设计下吗checkpoint推进的问题。这个log_checkpointer线程会通过最老的在flush list中的页来决定如何推进checkpoint。

0x04 评估

这里的具体信息可以参看[1]。这里其它的一些MySQL的更改版本也有采用另外的一些思路优化这里。比如在写Log Buffer的时候通过读写锁实现避免空洞,添加到flush list中的dirty page先添加到一个临时的list中,后面在按序添加到flush list中等[3].

参考

  1. MySQL 8.0: New Lock free, scalable WAL design,https://mysqlserverteam.com/mysql-8-0-new-lock-free-scalable-wal-design/。
  2. Scalability of Write-ahead Logging on Multicore and Multisocket Hardware, VLDB ‘12.
  3. http://mysql.taobao.org/monthly/2018/07/01/