阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

MySQL8.0 redo日志系统优化

271次阅读
没有评论

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

背景

现在主流的数据库系统的故障恢复逻辑都是基于经典的 ARIES 协议,也就是基于 undo 日志 +redo 日志的来进行故障恢复。redo 日志是物理日志,一般采用 WAL(Write-Ahead-Logging)机制,所以也称 redo 日志为 wal 日志,redo 日志记录了所有数据的变更,undo 日志是逻辑日志,记录了所有操作的前镜像,方便异常时进行回滚。用户在提交事务时,只要确保写 redo 日志成功即可,并不需要对应的数据页也实时落盘,这套机制的基本思想是利用空间换时间,用户事务的更新实际上在数据页和 redo 日志中记录了两份,传统的数据库存储引擎都是基于 B +Tree 来组织数据页,因此刷数据页是离散小块 IO,而写 redo 是顺序 IO,对磁盘介质更友好,而且 OLTP 场景下,业务对 RT(ResponseTime)也比较敏感,所以这套机制非常流行。

redo 日志是保证数据不丢的关键因素,而且每个事务在提交时,都需要写 redo 日志,可想而知这块资源竞争是非常激烈的。这个问题是所有基于 WAL 机制的数据库系统个的共性问题,下文的讨论以 MySQL 为例,并以此说明 MySQL8.0 在这块的优化。

最初的 redo 日志机制

在 MySQL 的日志系统中,这里讨论的是 InnoDB 引擎,mtr(mini-transaction)是最小事务单位,一个用户事务会对应若干个 mtr,mtr 保证内部操作的原子性,比如 B +Tree 分裂操作,必需在一个 mtr 中。用户执行操作时,会同时更新数据页和写 redo 日志,mtr 是 redo 日志的载体,存在每个会话的私有变量中。mtr 提交时,会将本地 redo 日志拷贝到全局的 log_buffer 中,为了保证日志有序性,需要加锁来访问 log_buffer,这把锁就是 log_sys_t::mutex,所以这个锁竞争非常激烈。在这个锁保护下,除了要将本地日志拷贝到全局 buffer,还需要将数据页加入了 flush_list,供后台线程刷脏,辅助数据库检查点持续往前推进。检查点一方面能控制全局的 redo 日志文件大小,让日志具备循环复用的能力;另一方面,也能提高故障恢复速度。因为故障恢复的本质就是利用落盘的 redo 日志来恢复没有落盘的数据页。所以最开始 (MySQL5.1) 只有一把锁,大并发场景下,这个锁竞争非常激烈,MySQL 在多核系统下也无法提升性能。

拆分 log_sys_t::mutex

既然锁竞争压力大,那么最直观的想法就是拆锁。首先按功能拆分,刚刚说到,在 mutex 保护下,做了两件事,一件是拷贝本地日志到全局 log_buffer;另一件事是将事务修改的 page 加入到 flush_list。日志系统将这两件事解耦,引入了 log_sys_t::flush_order_mutex,减少 log_sys_t::mutex 的持锁时间。将本地日志拷贝到 log_buffer 后,就可以释放 log_sys_t::mutex,这样拷贝日志的线程和处理 flush_list 的线程就可以并发起来。

除了这个,日志系统还引入了双 log_buffer 机制,这个主要是为了解决全局 log_buffer 的读写并发问题。一个 buffer 用于拷贝日志到 log_buffer,另一个 log_buffer 则用于读取,写入到日志文件。当需要读 log_buffer 时,则可以切换 log_buffer 的角色,这样就消除了写日志文件带来的访问 log_buffer 锁竞争。

但是,拆分完锁后,多个用户线程仍然需要在 log_sys_t::mutex 保护下,串行写 log_buffer,由于 memcpy 操作比较重,所以这个锁竞争仍然非常激烈,需要优化。

消除 log_sys_t::mutex

为了解决 log_sys_t::mutex 并发问题,MySQL 8.0 引入了新的 log_buffer 机制,借助于 lock_free 的 link_buf 数据结构,利用原子变量来进行预占位,这样多个线程就能并发写 redo,这种机制带来了一个空洞问题,因为写日志速度不一样,可能导致后占位 (lsn 较大) 的线程先写完。但是我们写日志线程肯定是不能将带有空洞的 buffer 写到日志文件,因此会维护一个滑动窗口,即最小的连续的 lsn:buf_ready_for_write_lsn。写日志线程会不断的检查 link_buf 变量 recent_written,然后写日志,推进 buf_ready_for_write_lsn。

MySQL8.0 redo 日志系统优化

前面我们提到了系统中有两把锁,log_sys_t::mutex 和 log_sys_t::flush_order_mutex,通过占位方式,解决了写 log_buffer 的问题,那么如何解决将脏页有序加入到 flush_list 的问题呢?MySQL 8.0 实现中仍然借助于 link_buf 数据结构,原来要求是加入 flush_list 的数据页的 oldest_modification_lsn 一定是递增的。这里顺便说下 oldest_modification_lsn 的含义,oldest_modification_lsn 是指 page 第一次被修改后,mtr 在 log_sys_t 中分配的 lsn,即使这个 page 在 flush 下去之前,又在内存中被修改过 N 次,仍然以第一次修改的 lsn 为准,这样做的目的是,确保数据页内存的修改与检查点推进能对应上,避免检查点推进了,但对应的脏页可能还未刷盘,造成数据丢失问题。

由于是并发乱序写 log_buffer,那么无法保证按 lsn 递增有序加入到 flush_list,也就无法推进检查点。MySQL 8.0 通过限制大于一定阀值 L 的 lsn 加入到 flush_list 做为权衡,假设当前 flush_list 的 lsn 最大值为 M,那么只有在 M 值与当前线程 lsn 相差范围在 L 以内时,才将脏页写入 flush_list。同样的,推进 M,也依赖于 link_buf 变量 recent_closed。这种策略本质上放宽了之前对于 flush_list 中对于 LSN 全局有序的限制到 L 范围内的有序。

MySQL8.0 redo 日志系统优化

除了日志系统变成 lock free,MySQL8.0 还将写日志线程从用户线程中拆分出来,有单独的 log writer 线程和 log flusher 后台线程来处理写日志和 sync 日志。原来的写日志方式是,大家随机 group-commit,由一个线程负责 write/flush 日志,其它线程等待,这种模式下 group 的大小比较随机。拆分后,处理更灵活,batch 大小也更好控制,而且对于 flush_log_at_trx_commit!= 1 的场景,只需要等 log writer 的通知即可。

总结

从 MySQL 日志系统优化的演进过程来看,始终是围绕锁 log_sys_t::mutex 展开。从最初的按功能拆分出 log_sys_t::flush_order_mutex,到按读写拆分实现为双 log_buffer,以及最新的 MySQL 8.0 利用无锁机制彻底去掉这把锁,显然 MySQL 的并发能力是越来越强的。这种优化“套路”其实是比较朴素通用的,对于一个新的系统,通过一把大锁能简化并发逻辑,优先保证正确性。在系统慢慢演进过程中,我们可以按功能拆分锁,缓解锁冲突压力;如果某把锁处于核心链路,而且又成为瓶颈,那么再想办法继续拆,或者实现为无锁,彻底解决并发冲突问题,目的只有一个就是充分利用多核 CPU 资源,然线程多干活,减少响应时间的同时,拉高吞吐量,而不是都等待空闲着。文章中并没有涉及更多关于 MySQL8.0 日志系统优化的细节,官方文档已经写的足够好,大家可以详细看看。

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-22发表,共计3076字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中