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

背景

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

然而日志因为要保证顺序性,须要锁来保护全部日志拷贝到buffer都是有序的,引入了一个严重的锁竞争点,特别是在多核场景下,这里的竞争会很是明显,没法发挥出多核心的优点。算法

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

具体的,咱们能够对应到几个模块:数据库

- 拷贝到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及以前版本已经彻底不一样了:oop

  • 日志能够并发拷贝,但会存在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的安全边界)

隐藏参数

如上所述,这里引入了多个后台线程来增长系统的并发度,而在内部也有大量参数来对系统进行调整,以得到最优性能,但为了不引发用户困惑,有一些参数是被隐藏的(在定义时经过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

本文做者:zhaiwx_yinfeng

原文连接

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索