MySQL8.0 redo log优化概述和线程模型介绍

71次阅读
没有评论

共计 6230 个字符,预计需要花费 16 分钟才能阅读完成。

本篇内容介绍了“MySQL8.0 redo log 优化概述和线程模型介绍”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让丸趣 TV 小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

1 MySQL redo_log 简要回顾

在 MySQL 5.7 中写性能将受限于 redo_log 的同步操作,特别是在多 cpu,存储设备很快的情况下,MySQL 5.7 redo_log 的设计无法有效利用存储设备性能,因此需要重新设计。MySQL 5.7 瓶颈在于 mtr 将在把 log 写到 log buffer 时加 log_sys_t::mutex 锁,之后该 mtr 把 dirty page 加入 flush list 中时,为了保证全局有序,会加 log_sys_t::flush_order_mutex 锁。如果其他用户线程的 mtr 要把 log 写到 log buffer,需要等待 log_sys_t::mutex,同时为了保证全局 lsn 有序,其他用户线程的 mtr 即使要往其他 flush list 中加数据也需要等待 log_sys_t::flush_order_mutex 锁,这两把锁是 MySQL 5.7 中 redo_log 的主要性能瓶颈。

2 redo log 优化概述

目前,redo log 是无锁全异步设计,其流程架构图如下所示:

如上图所示,redo log 的异步工作线程为 4 个,另 2 个异步辅助线程:分别是:log_writer, log_flusher, log_flush_notifier, log_write_notifier, log_checkpointer,log_close,log_flush_notifier /log_write_notifier 为图中 log notifier 线程组,辅助线程为 log_checkpointer, log_closer。

log_writer:负责将日志从 log buffer 写入磁盘,并推进 write_lsn(原子数据)
log_flusher:负责 fsync,并推进 flushed_to_disk_lsn(原子数据)
log_write_notifier:监听 write_lsn,唤醒等待 log 落盘的用户线程(根据 flush_log_at_trx_commit 设置,用户 commit 操作会等待 write_lsn 推进)
log_flush_notifier:监听 flushed_to_disk_lsn,唤醒等待 log fsync 的用户线程。

log_closer:1、在正常退出时清理所有 redo_log 相关 lsn\log buffer 相关数据结构;2、定期清理 recent_closer 的过老数据(recent_closer 所用之后详述)

log_checkpointer:定期做 checkpoint 检查,根据 flush list 刷 dirty page 情况推进 check point,释放 log buffer 等

1 线程同步数据结构

异步线程之间通过 2 种数据结构进行数据同步:原子读写(atomic)和 Link_buf。原子读写是 c ++11 针对多核 cpu 的一个新特性,在不加锁的情况下,对一个 64 位数据的原子读写。

Link_buf 是 MySQL 新实现的数据结构,逻辑上是循环数组,数组下标表示 start lsn,数据内容是 lsn 长度,数据类型为原子类型,如果数据内容(lsn 长度)为 0 表示这个 slot 为空。每个通过合理的 std::atomic_thread_fence(std::memory_order_release) / std::atomic_thread_fence(std::memory_order_acquire) 操作,保证对里面不同 slot 的 lsn 读写的无锁并发。

3 mtr 流程

3.1 流程

为了解决多个用户线程的 mtr 对 redo log 的争抢,MySQL 引入了 Link_buf 的两个实例:recent_write / recent_close. 这两个实例的数据类型是上文介绍的 Link_buf,不同 mtr 线程和 log_writer 线程可以无锁对 recent_write / recent_close 不同 slot 进行读写,recent_write / recent_close 链表长度固定,由 innodb_log_recent_closed_size 和 innodb_log_recent_written_size 制定。mtr 的流程如下所示:

mtr 通过 sn 和 log_len 获得写入的

start_lsn/end_lsn,
log_buffer_reserve(*log_sys, len);

将 mtr 的每个 block 都 memcpy 到 log_buffer

推进 recent_write 的 lsn 位置到 end_lsn
m_impl- m_log.for_each_block(write_log);

查看 recent_close 是否有空间,如果 recent_close 由于最早 lsn 对应的 slot 为空(表示该 slot 等待数据填充),而没有空间存储目前 mtr 对应的 lsn,那么,当前 mtr 需要等待,直到 recent_close 最早的 slot 推进
log_wait_for_space_in_log_recent_closed(*log_sys, handle.start_lsn);

将 dirty block 添加到 flush list
add_dirty_blocks_to_flush_list(handle.start_lsn, handle.end_lsn);

将当前 lsn 写入到 recent_close
log_buffer_close(*log_sys, handle);

3.2 分析

各用户线程的 mtr 之间对 log buffer、flush list 的争抢冲突,通过 recent_write / recent_close 的读写而化解。

1、多个 mtr 根据步骤 1 中分配的 lsn,可以并发的写入 log_buffer 而不产生冲突。但由于并发写入,获得较小 lsn 的 mtr(较早申请 lsn 的 mtr)不一定可以较早的进行 memcpy,因此在某些时间点 log_buffer 会出现空洞(hole)。对于空洞的解决后面详述。
2、各 mtr 作为生产者将数据写入 log_buffer, log_writer 作为消费者将数据将数据取出,log_writer 对数据 log_buffer 读取位置的获取通过 recent_write 获得,由于 recent_write 的无锁设计,mtr 与 log_writer 之间也不会有上锁等待过程。
log_writer 必须保证连续日志写入,但在 1 中分析,并发 mtr 或导致 log_buffer 空洞。因此 recent_write 提供方法 advance_tail_until,该方法使得推进数组到第一个 slot 存储值为 0 的地方,该 slot 的下标对应 lsn 就是第一个空洞出现的位置。log_writer 将 write_lsn(上次写盘位置) 到该 slot 对应的 lsn 之间的日志都从 log_buffer 写入磁盘。这套机制保证 log_writer 连续日志写入,取代 MySQL 5.7 中 log_sys_t::mutex 锁。
3、由于多个 mtr 并发写入,加入各 flush list 中的数据不在全局有序仅仅每个 flush 局部有序,因此需要确定一个 lsn 可以保证该 lsn 之前的所有 dirty page 都 flush 结束。针对这个限制,各 mtr 线程之间通过 recent_close 同步。由于 recent_close 长度固定,如果 recent_close 中最小的 lsn 与当前申请的 lsn 间距大于 len(recent_close),则需要等待,recent_close 最小 lsn 的推进有 log_closer 进行(后面详述)。因此,可以保证最大可能乱序长度为 len(recent_close),一旦 block 进入 flush list 其对应的 lsn 减去 len(recent_close) 位置的 lsn 一定已经刷盘,可以在该点进行 checkpoint。这套机制保证可以找到一个较新 lsn(较大 lsn),同时保证 checkpoint 点的新鲜程度,此机制用来取代 MySQL 5.7 中 log_sys_t::flush_order_mutex 锁。

4 redo log 线程模型

redo log 线程模型如下图所示:

4.1 log_writer

log_writer 负责将数据从 log_buffer 刷入 disk,流程如下:

推进 recent_write 的 tail 至最大的连续 lsn,并获取 lsn 的值(ready_lsn)
/* Advance lsn up to which data is ready in log buffer. */
(void)log_advance_ready_for_write_lsn(log);
ready_lsn = log_buffer_ready_for_write_lsn(log);

数据落盘,将 write_lsn 至 ready_lsn 之间的日志从 log_buffer 刷入 FS cache 中
write_blocks(log, write_buf, write_size, real_offset);

推进 write_lsn,将 write_lsn 推进到 ready_lsn
const lsn_t new_write_lsn = start_lsn + lsn_advance;
ut_a(new_write_lsn log.write_lsn.load());
log.write_lsn.store(new_write_lsn);

log_writer 整个流程仅通过 recent_write 同步 log_buffer 的连续日志位置,采取 spin lock + pthread_cond_timedwait 方式轮询 recent_write。之前 MySQL(5.7)的设计写盘操作是 mtr 中同步进行的,写策略比较单一,mtr 写入长度(对于文件系统过大或者过小)不合适也必须进行写入,如果连续写入小数据,会造成严重的 IO 浪费。修改后的写入策略可以进一步优化,写入块大小、需不需要做 batch 都可以在 log_writer 中优化。

4.2 log_flusher

log_flusher 负责将数据 fsync 到磁盘,推进 flush_up_to_lsn。log_flusher 与 log_writer 之间仅通过 write_lsn 同步刷盘位置,两个线程按照各自速度进行刷盘与 fsnyc,他们之间的刷盘数据同步在 OS/FS 层进行,没有用户态的锁。

4.3 log_notifier

log_notifier 包括 log_write_notifier 和 log_flush_notifier,这两个线程定期轮询(为了加速也会被唤醒)各自关心的 lsn 位置:log_write_notifier 关心 write_lsn,log_flush_notifier 关心 flush_up_to_lsn。并将根据最新的 lsn 值唤醒等待的用户线程。用户线程 commit 等操作时会等在 log_write_up_to 函数上,当 flush_up_to_lsn/write_lsn 推进到等待位置时,log_write_up_to 返回。

4.4 log_closer

log_closer 作用有两个:
定期推进 recent_close,清除 recent_close 中已经放入 flush list 的连续 lsn 片段,保证 mtr 不会因为第 4 步 recent_close 没空间而持续等待;
当数据库正常退出时,做收尾工作,将 redo_log 中的日志放到 flush list 中。

4.5 新增参数

与 innodb_log_writer_spin_delay/innodb_log_writer_timeout 作用相同,closer 线程对应 inno_log_closer_spin_delay/innodb_log_closer_timeout 两个参数调节轮询速度。

4.6 log_checkpointer

log_checkpointer 监控所有 flush list,选择所有 flush lists 中刷盘的最小(最老)的 lsn,将此 lsn 与 flushed_to_disk_lsn 等进行比较之后,设置新的 last_checkpoint_lsn。这样做节省在主进程中进行 checkpoint,将原来在主进程 7 秒做一次 checkpoint 改变成在 log_checkpointer1 秒做一次,一定程度节省数据库崩溃恢复的时间。

4.7 参数

1. innodb_log_xxx_spin_delay innodb_log_xxx_timeout

对于除 log_checkpointer 以外的 log 线程,每个线程都设置了自己的轮询参数:innodb_log_xxx_spin_delay 和 innodb_log_xxx_timeout

控制 log 线程轮询频率的函数为:

template typename Condition
inline static Wait_stats os_event_wait_for(os_event_t event,
  uint64_t spins_limit,
  uint64_t timeout,
  Condition condition = {})

log 线程先等待 innodb_log_xxx_spin_delay(spins_limit 参数) 个 cpu pause 指令,如果事件没有到来(condition 函数返回 false),开始等待特定信号量 event,睡眠 k 个 innodb_log_xxx_timeout(timeout 参数)us,k 随着连续等待次数的增加按照 2 的指数上升,总体睡眠时间以 100ms 为上限。

参数名称列表如下

2. innodb_log_spin_cpu_abs_lwm innodb_log_spin_cpu_pct_hwm
用 innodb_log_spin_cpu_abs_lwm 和 innodb_log_spin_cpu_pct_hwm 进行两个参数控制 os_event_wait_for 中自旋锁的使用。innodb_log_spin_cpu_abs_lwm 表示使用自旋锁的下门限,cpu 低于这个门限,表示系统空闲,不需要使用自旋锁,cpu 高于 innodb_log_spin_cpu_pct_hwm 表示系统特别繁忙,也不许使用自旋锁。innodb_log_spin_cpu_abs_lwm 表示一个 cpu 的使用率,比如 40core cpu,innodb_log_spin_cpu_abs_lwm 为 80,在 MySQLd 上 cpu 总使用率小于 80%,不进行自旋。innodb_log_spin_cpu_pct_hwm 表示所有 cpu 的总使用率,比如 40core cpu,innodb_log_spin_cpu_pct_hwm 为 50,在 cpu 使用率大于 2000% 时,不进行自旋。

5 结语

redo_log 的优化将之前锁冲突化解,用户线程的“等锁 - 写”机制转化为“写缓冲 - 查看写盘”机制。用户线程不进行刷盘操作,由后台线程统一刷盘,用户线程在写缓冲后就可以做其他操作,达到日志写盘和 mtr 做其他事情的并行,提升效率。

“MySQL8.0 redo log 优化概述和线程模型介绍”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注丸趣 TV 网站,丸趣 TV 小编将为大家输出更多高质量的实用文章!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-28发表,共计6230字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)