您当前的位置:首页 > 电脑百科 > 数据库 > MYSQL

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

时间:2020-01-21 15:44:49  来源:  作者:

作者介绍

张永翔,现任网易云RDS开发,持续关注MySQL及数据库运维领域,擅长MySQL运维,知乎ID:雁南归。

MySQL 8.0中一个重要的新特性是对Redo Log子系统的重构,通过引入两个新的数据结构recent_written和recent_closed,移除了之前的两个热点锁:log_sys_t::mutex和log_sys_t::flush_order_mutex。

这种无锁化的重构使得不同的线程在写入redo_log_buffer时得以并行写入,但因此带来了log_buffer不再按LSN增长的顺序写入的问题,以及flush_list中的脏页不再严格保证LSN的递增顺序问题。

本文将介绍MySQL 8.0中对log_buffer相关代码的重构,并介绍并发写log_buffer引入问题的解决办法。

一、MySQL Redo Log系统概述

Redo Log又被称为WAL ( Write Ahead Log),是InnoDB存储引擎实现事务持久性的关键。

在InnoDB存储引擎中,事务执行过程被分割成一个个MTR (Mini TRansaction),每个MTR在执行过程中对数据页的更改会产生对应的日志,这个日志就是Redo Log。事务在提交时,只要保证Redo Log被持久化,就可以保证事务的持久化。

由于Redo Log在持久化过程中顺序写文件的特性,使得持久化Redo Log的代价要远远小于持久化数据页,因此通常情况下,数据页的持久化要远落后于Redo Log。

每个Redo Log都有一个对应的序号LSN (Log Sequence Number),同时数据页上也会记录修改了该数据页的Redo Log的LSN,当数据页持久化到磁盘上时,就不再需要这个数据页记录的LSN之前的Redo日志,这个LSN被称作Checkpoint。

当做故障恢复的时候,只需要将Checkpoint之后的Redo Log重新应用一遍,便可得到实例Crash之前未持久化的全部数据页。

InnoDB存储引擎在内存中维护了一个全局的Redo Log Buffer用以缓存对Redo Log的修改,mtr在提交的时候,会将mtr执行过程中产生的本地日志copy到全局Redo Log Buffer中,并将mtr执行过程中修改的数据页(被称做脏页dirty page)加入到一个全局的队列中flush list。

InnoDB存储引擎会根据不同的策略将Redo Log Buffer中的日志落盘,或将flush list中的脏页刷盘并推进Checkpoint。

在脏页落盘以及Checkpoint推进的过程中,需要严格保证Redo日志先落盘再刷脏页的顺序,在MySQL 8之前,InnoDB存储引擎严格的保证MTR写入Redo Log Buffer的顺序是按照LSN递增的顺序,以及flush list中的脏页按LSN递增顺序排序。

在多线程并发写入Redo Log Buffer及flush list时,这一约束是通过两个全局锁log_sys_t::mutex和log_sys_t::flush_order_mutex实现的。

二、MySQL 5.7中MTR的提交过程

在MySQL 5.7中,Redo Log写入全局的Redo Log Buffer以及将脏页添加到flush list的操作均在mtr的提交阶段中完成,简化后的代码为:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

MySQL官方博客中有一张图可以很好的展示了这个过程:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

三、MySQL 8中的无锁化设计

从上面的代码中可以看到,在有多个MTR并发提交的时候,实际在这些MTR是串行的完成从本地日志Copy redo到全局Redo Log Buffer以及添加Dirty Page到Flush list的。这里的串行操作就是整个MTR 提交过程的瓶颈,如果这里可以改成并行,想必可以提高MTR的提交效率。

但是串行化的提交可以严格保证Redo Log的连续性以及flush list中Page修改LSN的递增,这两个约束使得将Redo Log和脏页刷入磁盘的行为很简单。只要按顺序将Redo Log Buffer中的内容写入文件,以及按flush list的顺序将脏页刷入表空间,并推进Checkpoint即可。

当MTR不再以串行的方式提交的时候,会导致以下问题需要解决:

  • MTR串行的copy本地日志到全局Redo Log Buffer可以保证每个MTR的日志在Redo Log Buffer中都是连续的不会分割。当并行copy日志的时候,需要有额外的手段保证mtr的日志copy到Redo Log Buffer后仍然连续。MySQL 8.0中使用一个全局的原子变量log_t::sn在copy数据前为MTR在Redo Log Buffer中预留好需要的位置,这样并行copy数据到Redo Log Buffer时就不会相互干扰。

  • 由于多个MTR并行copy数据到Redo Log Buffer,那必然会有一些MTR copy的快一些,有些MTR copy的比较慢,这时候Redo Log Buffer中可能会有空洞,那么就需要一种方法来确定Redo Log Buffer中的哪些内容可以写入文件。MySQL 8.0中引入了新的数据结构Link_buf解决了这个问题。

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统
  • 并行的添加脏页到flush list会打破flush list中每个数据页对应LSN的单调性约束,如果仍然按flush list中的顺序将脏页落盘,那如何确定Checkpoint的位置?

下面本文将分别讨论以上三个问题:

1、MTR复制日志到Redo Log Buffer的无锁化

在MySQL 8.0中, MTR的提交部分可以用如下伪代码表示:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

同5.7的代码相比,最明显的区别就是移除了log_sys->mutex锁和log_sys->flush_order_mutex锁,而实现Redo Log无锁化的关键在于 log_buffer_reserve(*log_sys, len) 这个函数, 其中关键的代码只有两句:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

可以看到,这里是通过一个原子操作std::atomic<uint64>.fetch_add(log_len)实现在Copy Redo之前在全局Redo Log Buffer中预分配空间,实现并行写入而不冲突。

2、Log Buffer空洞问题

预分配的方式可以使多个MTR不冲突的copy数据到Redo Log Buffer,但由于有些线程快一些,有些线程慢一些,必然会造成Redo Log Buffer的空洞问题,这个使得Redo Log Buffer刷入到磁盘的行为变得复杂。

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

如上图所示,Redo Log Buffer中第一个和第三个线程已经完成了Redo Log的写入,第二个线程正在写入到Redo Log Buffer中,这个时候是不能将三个线程的Redo都落盘的。MySQL 8.0中引入了一个数据结构Link_buf解决这个问题。

Link_buf实际上是一个定长数组,并保证数组的每个元素的更新是原子性的,并以环形的方式复用已经释放的空间。

Link_buf用于辅助表示其他数据结构的使用情况,在Link_buf中,如果一个索引位置i对应的值为非0值n,则表示Link_buf辅助标记的那个数据结构,从i开始后面n个元素已被占用。同时Link_buf内部维护了一个变量M表示当前最大可达的LSN,Link_buf的结构示意图如下所示:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

在接口层面,Link_buf实际上定义了3个有效的行为:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

Redo Log Buffer内部维护了两个Link_buf类型的变量recent_written和recent_closed来维护Redo Log Buffer和flush list的修改信息。

对于redo log buffer,buffer的使用情况和recent_written的对应关系如下图所示:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

buf_ready_for_write_lsn这个变量维护的是可以保证无空洞的最大LSN值,也就是recent_written->tail的结果,在这之前的Redo Log都是可以安全的持久化到磁盘上的。

当第一个空洞位置的数据被写入成功后,写入数据的mtr通过调用log.recent_written.add_link(start_lsn, end_lsn)将recent_written内部状态更新为如下图所示的样子:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

这部分代码在log0log.cc文件的log_buffer_write_completed方法中。

每次修改recent_written后,都会触发一个独立的线程log_writer向后扫描recent_written并更新buf_ready_for_write_lsn 值(调用recent_written->advance_tail()方法)。log_writer线程实际上就是执行日志写入到文件的线程。由log_writer线程扫描后的recent_written变量内部如下图所示:

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

这样就很好的解决了MTR并发写入log_buffer造成的空洞问题。通过新引入的Link_buf类型的数据结构,可用很方便的知道哪一部分的Redo Log可以执行写入磁盘的操作。

关于更多落盘的细节

在MySQL 8中,Redo log的落盘过程交由两个独立的线程完成,分别 log_writer和log_flusher,前者负责将Redo Log Buffer中的数据写入到OS Cache中, 后者负责不停的执行fsync操作将OS Cache中的数据真正的写入到磁盘里。

两个线程通过一个全局的原子变量log_t::write_lsn同步,write_lsn表示当前已经写入到OS Cache的Redo log最大的LSN。

源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

log buffer中的redo log的落盘不需要由用户线程关心,用户线程只需要在事务提交的时候,根据innodb_flush_log_at_trx_commit定义的不同行为,等待log_writer或log_flusher的通知即可。

log_writer线程会在监听到recent_written被修改后,log_buffer中大于log_t::write_lsn小于buf_ready_for_write_lsn的redo log刷入到 OS Cache 中,并更新log_t::write_lsn。

log_flusher线程则在监听到write_lsn更新后调用一次fsync并更新flushed_to_disk_lsn,该变量保存的是最新fsync到文件的值。

源码解读: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的模式,必然会带来性能上的提升。

参考

  • MySQL8.0: 重新设计的日志子系统

    https://yq.aliyun.com/articles/592215?utm_content=m_49932

  • MySQL 8.0: New Lock free, scalable WAL design

    https://mysqlserverteam.com/mysql-8-0-new-lock-free-scalable-wal-design/

  • MySQL Source Code Documentation/InnoDB Redo Log

    https://dev.mysql.com/doc/dev/mysql-server/8.0.11/PAGE_INNODB_REDO_LOG.html

  • InnoDB的Redo Log分析

    http://www.leviathan.vip/2018/12/15/InnoDB%E7%9A%84Redo-Log%E5%88%86%E6%9E%90/

  • MySQL · 引擎特性 · WAL那些事儿

    http://mysql.taobao.org/monthly/2018/07/01/



Tags:MySQL 8.0   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
这篇文章主要给大家介绍了关于MySQL 8.0新特性之隐藏字段的相关资料,文中通过示例代码介绍得非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编...【详细内容】
2021-07-12  Tags: MySQL 8.0  点击:(92)  评论:(0)  加入收藏
从2018年4月19日,第一个MySQL 8.0 GA版本8.0.11版本发布至今,已经过去有近3年的时间。最近三周姜老师“喜提“MySQL 8.0.23,并做了大量的测试与验证,深深地体会到MySQL 8.0的强...【详细内容】
2021-04-15  Tags: MySQL 8.0  点击:(134)  评论:(0)  加入收藏
Access denied for user &#39;root&#39;@&#39;localhost&#39; (using password: YES) 这个错误在网上搜一下,能看到非常多的此类问题的解决方法,但 MySQL 版本不一样,导致同一...【详细内容】
2020-10-20  Tags: MySQL 8.0  点击:(342)  评论:(0)  加入收藏
有人说目前为止8.0是最好的版本,我们来看看在运维方面MySQL 8.0带来了哪些便捷命令。自MySQL 8.0 GA版本发布以来,MySQL生态发生了很大的变化,推出了很多功能 ,有人说目前为止8....【详细内容】
2020-08-20  Tags: MySQL 8.0  点击:(65)  评论:(0)  加入收藏
MySQL 8.0.21 版本已于昨日发布(dev.mysql.com),开始对一些术语如 Master / Slave 等做了替换。下面是来自官方团队对此版本的重点功能解读。更详细的内容请参考: https://dev.m...【详细内容】
2020-07-15  Tags: MySQL 8.0  点击:(81)  评论:(0)  加入收藏
MySQL 8.0中一个重要的新特性是对Redo Log子系统的重构,通过引入两个新的数据结构recent_written和recent_closed,移除了之前的两个热点锁:log_sys_t::mutex和log_sys_t::flush_order_mutex。...【详细内容】
2020-01-21  Tags: MySQL 8.0  点击:(53)  评论:(0)  加入收藏
作者:胡呈清整理 MySQL 8.0 文档时发现一个变更:默认字符集由 latin1 变为 utf8mb4。想起以前整理过字符集转换文档,升级到 MySQL 8.0 后大概率会有字符集转换的需求,在此正好分...【详细内容】
2019-12-27  Tags: MySQL 8.0  点击:(105)  评论:(0)  加入收藏
本文介绍 MySQL 8.0 shell 子模块 Util 的两个导入特性 importTable/import_table(JS和python 版本的命名差异)、importJson/import_json的使用方法。其中 import_table 是通过传统 MySQL 协议来通信,Import_json 是通...【详细内容】
2019-09-25  Tags: MySQL 8.0  点击:(154)  评论:(0)  加入收藏
创建用户并授权创建用户CREATE USER &#39;custom&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;password&#39;; mysql 8.0 默认身份验证插件为caching_sha2_password,导致很...【详细内容】
2019-07-04  Tags: MySQL 8.0  点击:(459)  评论:(0)  加入收藏
▌简易百科推荐
作者:雷文霆 爱可生华东交付服务部 DBA 成员,主要负责Mysql故障处理及相关技术支持。爱好看书,电影。座右铭,每一个不曾起舞的日子,都是对生命的辜负。 本文来源:原创投稿 *爱可生...【详细内容】
2021-12-24  爱可生    Tags:MySQL   点击:(6)  评论:(0)  加入收藏
生成间隙(gap)锁、临键(next-key)锁的前提条件 是在 RR 隔离级别下。有关Mysql记录锁、间隙(gap)锁、临键锁(next-key)锁的一些理论知识之前有写过,详细内容可以看这篇文章...【详细内容】
2021-12-14  python数据分析    Tags:MySQL记录锁   点击:(17)  评论:(0)  加入收藏
binlog 基本认识 MySQL的二进制日志可以说是MySQL最重要的日志了,它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二...【详细内容】
2021-12-14  linux上的码农    Tags:mysql   点击:(13)  评论:(0)  加入收藏
为查询优化你的查询 大多数的MySQL服务器都开启了查询缓存。这是提高性最有效的方法之一,而且这是被MySQL的数据库引擎处理的。当有很多相同的查询被执行了多次的时候,这些查...【详细内容】
2021-12-09  元宇宙iwemeta    Tags:mysql   点击:(15)  评论:(0)  加入收藏
测试的目的和原因,公司有很多程序员,每个程序员对数据库和表结构都有自己的理解。而且每个程序员的理解往往是以效率考虑。既然都是为了效率考虑,那么我就来测试一下究竟哪种使...【详细内容】
2021-12-08  吴彬的分享    Tags:Mysql数据库   点击:(14)  评论:(0)  加入收藏
当你们考虑项目并发的时候,我在部署环境,当你们在纠结使用ArrayList还是LinkedArrayList的时候,我还是在部署环境。所以啊,技术不止境,我在部环境。今天这篇文章缕一下在同一台服...【详细内容】
2021-12-08  秃头码哥    Tags:MySQL数据库   点击:(16)  评论:(0)  加入收藏
对于数据分析来说,MySQL使用最多的是查询,比如对数据进行排序、分组、去重、汇总及字符串匹配等,如果查询的数据涉及多个表,还需要要对表进行连接,本文就来说说MySQL中常用的查询...【详细内容】
2021-12-06  笨鸟学数据分析    Tags:MySQL   点击:(19)  评论:(0)  加入收藏
在学习SQL语句之前,首先需要区分几个概念,我们常说的数据库是指数据库软件,例如MySQL、Oracle、SQL Server等,而本文提到的数据库是指数据库软件中的一个个用于存储数据的容器。...【详细内容】
2021-11-24  笨鸟学数据分析    Tags:SQL语句   点击:(23)  评论:(0)  加入收藏
概述以前参加过一个库存系统,由于其业务复杂性,搞了很多个应用来支撑。这样的话一份库存数据就有可能同时有多个应用来修改库存数据。比如说,有定时任务域xx.cron,和SystemA域...【详细内容】
2021-11-05  Java云海    Tags:分布式锁   点击:(31)  评论:(0)  加入收藏
MySQL的进阶查询 一、 按关键字排序 使用ORDERBY语句来实现排序排序可针对一个或多个字段ASC:升序,默认排序方式 【升序是从小到大】DESC:降序 【降序是从大到小】ORDER BY的...【详细内容】
2021-11-05  Java热点    Tags:SQL语句   点击:(27)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条