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

本文涉及的产品
RDS PostgreSQL Serverless,0.5-4RCU 50GB 3个月
推荐场景:
对影评进行热评分析
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
云数据库 RDS SQL Server,基础系列 2核4GB
简介: 背景 当前几乎所有的关系数据库都采用日志先行的方式,也就是所谓WRITE-AFTER-LOG(WAL),这是因为日志通常是顺序写的,并且写入量相比修改的数据通常要小很多。通过redo log来确保提交的事务必然具有持久性。

Update

2018/12/14: 增加log write ahead的内容
2018/6/19 官方发表了一篇博客介绍内核实现:
MySQL 8.0: New Lock free, scalable WAL design

背景

当前几乎所有的关系数据库都采用日志先行的方式,也就是所谓WRITE-AFTER-LOG(WAL),这是因为日志通常是顺序写的,并且写入量相比修改的数据通常要小很多。通过redo log来确保提交的事务必然具有持久性。(目前也有另外一种理论叫做Write Ahead Log, 由CMU的教授提出,主要适用于Nvme,这里在CMU的peloton项目里有个介绍

然而日志由于要保证顺序性,需要锁来保护所有日志拷贝到buffer都是有序的,引入了一个严重的锁竞争点,特别是在多核场景下,这里的竞争会非常明显,无法发挥出多核心的优势。

为了解决这个问题,MySQL8.0对日志系统进行了重新设计,将整个模块变成了lock-free的模式(小道消息,目前官方也在对事务模块和锁模块改造成lock-free模式,相信到时候InnoDB的扩展性必然会提升一大截, 未来可期!)

具体的,我们可以对应到几个模块:

- 拷贝到buffer: 每个mini transaction将自己的本地日志拷贝到全局Buffer中
- 写磁盘:包括写磁盘和调用fsync进行持久化
- 事务提交:当事务undo被标记为prepare(如果binlog打开) 或者commit时,需要确保日志被刷到磁盘,以确保事务的持久性
- Checkpoint: 定期对日志做checkpoint,减少崩溃恢复时日志的应用量

以下是对上述几个模块的简要介绍

实现

写log buffer

在5.7版本中,Innodb的log buffer实际上是分成了两个区域,轮换着来写,从而实现在写一个buffer 时,另外一个buffer依然可以继续往里面拷贝日志。但到了8.0版本,所有日志相关的mutex都已经移除了,划分缓冲区域也就没有必要了,而是将log buffer当做一个环来使用。

首先,持有一个s lock, 并通过原子操作获取当前mtr的start_lsn,和sn号(lsn减去log block头和尾的大小,表示有效日志量),这样相当于在顺序增加的lsn序列中保留了自己的一段范围(获得mtr_t::start_lsn 和mtr_t::end_lsn), 通过start_lsn取模log buffer size,得到其在log buffer中的位置,然后逐个block进行拷贝(log_buffer_write), 每写一个mtr log block,就将其start_lsn和end_lsn加入到log.recent_written中,维持了一个link结构, 一个mtr 可能会更新多次link_buf

(InnoDB里增加了一个叫link_buf的类,其具体的作用就是将不连续的变量维护成一个链表,举个简单的例子:

buf[lsn_1] = lsn_2
buf[lsn_2] = lsn_3
Buf[lsn_3] = 0
Buf[lsn_4] = lsn_5
Lsn_1 = 10
Lsn_2 = 100
Lsn_3 = 200
Lsn_4 = 300
Lsn_5 = 400
通过这种方式,实际可以追踪到所有并发写入到buffer的mtr范围,并快速检测到buffer中的hole,例如上例中,lsn_3 ~ lsn_4属于还没有写入日志的空洞
如上提到的log.recent_written, 可以确保写到磁盘的日志不存在空洞,如上例,只能写到lsn_3这个位置)

在拷贝完日志后,就需要将脏块加入flush list中。注意由于现在实现了完全并发,我们无法做到按照LSN顺序插入到flush list上,而有序性是用于保证checkpoint点的正确性。因此在这里同样也引入了另外一个link_buf,名为log.recent_closed,来辅助获取一个安全的checkpoint点。因此在加入flush list后,该mtr也会加入到recent_closed中(类似buf[mtr->start_lsn] - mtr->end_lsn)

注意log.recent_writtern 和log.recent_closed都是有空间限制的,如果超出其capability,就需要等待,但这种情况一般很少见

可以看到这里的代码和5.7及之前版本已经完全不同了:

- 日志可以并发拷贝,但会存在hole
- Flush list不再有序

我们之前惯用log_get_lsn或者直接log_sys->lsn来获得最新的lsn点,而在8.0版本,通过将log.sn转换成最新的lsn,但这个lsn点并不代表该点之前的日志都拷贝到buffer。之前我们提到在拷贝buffer之前需要加一个s_lock, 如果我们在持有x锁的前提下去取lsn,才能保证是最新的。

写磁盘

目前有两个后台线程来做日志持久化,一个是log_writer线程,一个是log_flusher线程,顾名思义,前者负责写日志到磁盘,后者负责fsync日志

Log_writer会根据log.recent_written中的记录找到安全的lsn, 将对应日志写磁盘,同时回收log.recent_written中的空间。 如果当前srv_flush_log_at_trx_commit设置为1的话,还回去唤醒log_flusher线程

log_flusher线程的主要工作是fsync日志文件,同时推进log.flushed_to_disk_lsn。随后尝试去唤醒等待的用户线程(如果只涉及一个event slot)或者唤醒log_flush_notifier线程。

Log_notifier线程专门用于唤醒等待日志写入的线程,根据上次flush的log lsn和当前flush lsn,来计算对应的event slot,并遍历数组唤醒等待的线程。

可以看到这里已经完全做到了异步化,再加上并发拷贝log buffer, 可以极大的发挥硬件性能。

事务提交

在innodb事务提交时,对应的Undo状态被修改后,需要调用log_write_up_to去确保日志已经写盘了。在5.7及之前版本中,该函数就是用于写日志到磁盘。而到了8.0版本,该函数只有唤醒后台线程及等待的逻辑。

一个有趣的问题是,由于目前用户线程仅需要等待唤醒,而无需去操作临界区域,我们可以在其退出innodb后再调用log_write_up_to 进行等待(参考bug#90641)

Checkpoint

由于现在脏页并不是按照LSN顺序写入的,因此选择一个安全的checkpoint点至关重要,这个工作主要由后台线程log_checkpointer来完成。

计算最老lsn的工作在log_get_available_for_checkpoint_lsn中完成:

- 首先找到log.recent_closed中的最小lsn,这个lsn点之前的page肯定已经加入到flush list上了
- 其次取出当前flush list中最后一个非临时表page的lsn,并取多个Buffer Pool中的最小值返回,然后减去一个安全的阈值(即log.recent_closed的最大空间)
- 上面两个值去最小的那个

很显然,为了避免扫描全部flush list链表,这里采用了乐观的算法,只要最大限度的保证做checkpoint的点是安全的即可。 这里引入的一个问题是,做checkpoint时可能是在一个mtr log的中间,在崩溃恢复时,可能需要对其定位的log block做特殊处理(在之前的版本中,可以确保checkpoint lsn是一个mtr log的安全边界),因为checkpoint要从正确的日志边界开始:

崩溃恢复入口函数:recv_recovery_begin(),几个相关变量:

  • recv_sys->parse_start_lsn
  • recv_sys->bytes_to_ignore_before_checkpoint

例如checkpoint lsn 在block内50的位置,block内若first_rec_group为非为30(表示一个完整日志的起始),就会设置parse_start_lsn在30这个位置,bytes_to_ignore_before_checkpoint = 50 - 30 = 20; 这部分日志在解析时需要忽略

如果从checkpoint lsn所在block内first_rec_group未设置,则继续向前找,直到一个日志的起始位置。
因此parse_start_lsn既可能在checkpoint lsn之前,也可能在之后。

Log Write Ahead

InnoDB为了避免拷贝更新产生的开销,支持对日志写扩展到某个指定的对其值,由于在新版本中,log buffer是环形使用的,可能无法使用log buffer来直接Padding,因此这块代码和5.7是有些不同的。

- innodb除了log_t::buf外,还有另外一个log_t::log.write_ahead_buf, 大小为srv_log_write_ahead_size,默认为8KB
- 变量log_t::write_ahead_end_offset, 是以srv_log_write_ahead_size对齐的, 用来维护一个write ahead区域,举个简单的例子,若当前ahead_end_offset = 32k。
- Log_writter的每次写入都不会超过srv_log_write_ahead_size, 也不会超出当前的write ahead区域
- 当需要写入的数据buffer小于一个block时,使用log ahead buffer, 但不一定会write ahead, 这样可以避免写的时候,被其他并发线程mtr_commit时修改
- 如果要写入的数据超过一个block,并且有未写满的block时,写把完整的block写入
- 看起来在一个write ahead区域,只会产生一次write ahead, 没有任何注释解释这个行为

Mark: 这是8.0.13的行为,据说下一个版本会有改动,到时候再更新下文档

隐藏参数

如上所述,这里引入了多个后台线程来增加系统的并发度,而在内部也有大量参数来对系统进行调整,以获得最优性能,但为了避免引起用户困惑,有一些参数是被隐藏的(在定义时通过PLUGIN_VAR_EXPERIMENTAL来控制)。

如果你想使用这些参数,需要自己去编译mysql代码,并在cmake时增加参数-DENABLE_EXPERIMENT_SYSVARS=1

如下,打开选项后和日志相关的参数包括:

+--------------------------------------+---------------+
| Variable_name                        | Value         |
+--------------------------------------+---------------+
| innodb_log_buffer_size               | 16777216      |
| innodb_log_checkpoint_every          | 1000          |
| innodb_log_checksums                 | ON            |
| innodb_log_closer_spin_delay         | 0             |
| innodb_log_closer_timeout            | 1000          |
| innodb_log_file_size                 | 2147483648    |
| innodb_log_files_in_group            | 8             |
| innodb_log_flush_events              | 2048          |
| innodb_log_flush_notifier_spin_delay | 0             |
| innodb_log_flush_notifier_timeout    | 10            |
| innodb_log_flusher_spin_delay        | 25000         |
| innodb_log_flusher_timeout           | 10            |
| innodb_log_group_home_dir            | /u01/my80/log |
| innodb_log_recent_closed_size        | 2097152       |
| innodb_log_recent_written_size       | 1048576       |
| innodb_log_spin_cpu_abs_lwm          | 80            |
| innodb_log_spin_cpu_pct_hwm          | 50            |
| innodb_log_wait_for_flush_spin_delay | 25000         |
| innodb_log_wait_for_flush_spin_hwm   | 0             |
| innodb_log_wait_for_flush_timeout    | 1000          |
| innodb_log_wait_for_write_spin_delay | 25000         |
| innodb_log_wait_for_write_timeout    | 1000          |
| innodb_log_write_ahead_size          | 8192          |
| innodb_log_write_events              | 2048          |
| innodb_log_write_max_size            | 4096          |
| innodb_log_write_notifier_spin_delay | 0             |
| innodb_log_write_notifier_timeout    | 10            |
| innodb_log_writer_spin_delay         | 25000         |
| innodb_log_writer_timeout            | 10            |
+--------------------------------------+---------------+
30 rows in set (0.01 sec)

通过这些参数,你可以对新的日志系统进行各种微调来获得最优性能。注意这里很多参数目前还看不到官方文档的描述,你可能需要结合代码来看。有一些比较有趣的参数例如innodb_log_spin_cpu_pct_hwm/lwm 可以控制user cpu超过多少百分比时,是否还允许用户线程继续spin loop

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
SQL 关系型数据库 MySQL
【揭秘】MySQL binlog日志与GTID:如何让数据库备份恢复变得轻松简单?
【8月更文挑战第22天】MySQL的binlog日志记录数据变更,用于恢复、复制和点恢复;GTID为每笔事务分配唯一ID,简化复制和恢复流程。开启binlog和GTID后,可通过`mysqldump`进行逻辑备份,包含binlog位置信息,或用`xtrabackup`做物理备份。恢复时,使用`mysql`命令执行备份文件,或通过`innobackupex`恢复物理备份。GTID模式下的主从复制配置更简便。
167 2
|
2月前
|
SQL 关系型数据库 MySQL
【MySQL】根据binlog日志获取回滚sql的一个开发思路
【MySQL】根据binlog日志获取回滚sql的一个开发思路
|
2月前
|
API C# 开发框架
WPF与Web服务集成大揭秘:手把手教你调用RESTful API,客户端与服务器端优劣对比全解析!
【8月更文挑战第31天】在现代软件开发中,WPF 和 Web 服务各具特色。WPF 以其出色的界面展示能力受到欢迎,而 Web 服务则凭借跨平台和易维护性在互联网应用中占有一席之地。本文探讨了 WPF 如何通过 HttpClient 类调用 RESTful API,并展示了基于 ASP.NET Core 的 Web 服务如何实现同样的功能。通过对比分析,揭示了两者各自的优缺点:WPF 客户端直接处理数据,减轻服务器负担,但需处理网络异常;Web 服务则能利用服务器端功能如缓存和权限验证,但可能增加服务器负载。希望本文能帮助开发者根据具体需求选择合适的技术方案。
67 0
|
2月前
|
C# Windows 监控
WPF应用跨界成长秘籍:深度揭秘如何与Windows服务完美交互,扩展功能无界限!
【8月更文挑战第31天】WPF(Windows Presentation Foundation)是 .NET 框架下的图形界面技术,具有丰富的界面设计和灵活的客户端功能。在某些场景下,WPF 应用需与 Windows 服务交互以实现后台任务处理、系统监控等功能。本文探讨了两者交互的方法,并通过示例代码展示了如何扩展 WPF 应用的功能。首先介绍了 Windows 服务的基础知识,然后阐述了创建 Windows 服务、设计通信接口及 WPF 客户端调用服务的具体步骤。通过合理的交互设计,WPF 应用可获得更强的后台处理能力和系统级操作权限,提升应用的整体性能。
64 0
|
2月前
|
存储 关系型数据库 MySQL
深入MySQL:事务日志redo log详解与实践
【8月更文挑战第24天】在MySQL的InnoDB存储引擎中,为确保事务的持久性和数据一致性,采用了redo log(重做日志)机制。redo log记录了所有数据修改,在系统崩溃后可通过它恢复未完成的事务。它由内存中的redo log buffer和磁盘上的redo log file组成。事务修改先写入buffer,再异步刷新至磁盘,最后提交事务。若系统崩溃,InnoDB通过redo log重放已提交事务并利用undo log回滚未提交事务,确保数据完整。理解redo log工作流程有助于优化数据库性能和确保数据安全。
211 0
|
2月前
|
存储 SQL 关系型数据库
MySQL事务日志奥秘:undo log大揭秘,一文让你彻底解锁!
【8月更文挑战第24天】本文深入探讨了MySQL中undo log的关键作用及其在确保事务原子性和一致性方面的机制。MySQL通过记录事务前的数据状态,在需要时能回滚至初始状态。主要介绍InnoDB存储引擎下的undo log实现,包括undo segment和record的结构,而MyISAM则采用redo log保障持久性而非一致性。通过一个简单的SQL回滚示例,展示了undo log如何在实际操作中发挥作用,帮助读者更好地理解并运用MySQL事务管理功能。
142 0
|
2月前
|
SQL 存储 关系型数据库
MySQL日志,你知多少?
MySQL日志,你知多少?
49 0
|
16天前
|
NoSQL 关系型数据库 MySQL
微服务架构下的数据库选择:MySQL、PostgreSQL 还是 NoSQL?
在微服务架构中,数据库的选择至关重要。不同类型的数据库适用于不同的需求和场景。在本文章中,我们将深入探讨传统的关系型数据库(如 MySQL 和 PostgreSQL)与现代 NoSQL 数据库的优劣势,并分析在微服务架构下的最佳实践。
|
18天前
|
存储 SQL 关系型数据库
使用MySQL Workbench进行数据库备份
【9月更文挑战第13天】以下是使用MySQL Workbench进行数据库备份的步骤:启动软件后,通过“Database”菜单中的“管理连接”选项配置并选择要备份的数据库。随后,选择“数据导出”,确认导出的数据库及格式(推荐SQL格式),设置存储路径,点击“开始导出”。完成后,可在指定路径找到备份文件,建议定期备份并存储于安全位置。
158 11
|
2月前
|
弹性计算 关系型数据库 数据库
手把手带你从自建 MySQL 迁移到云数据库,一步就能脱胎换骨
阿里云瑶池数据库来开课啦!自建数据库迁移至云数据库 RDS原来只要一步操作就能搞定!点击阅读原文完成实验就可获得一本日历哦~

相关产品

  • 云数据库 RDS MySQL 版
  • 下一篇
    无影云桌面