麒麟v10 上部署 TiDB v5.1.2 生产环境优化实践
1059
2023-04-23
MySQL Binlog 组提交实现
本文代码分析基于 MySQL 8.0.29
1.背景
MySQL 提交流程有两个问题需要解决:
1.1. 提交写两份日志的性能问题
为了保证事务的持久性和原子性,事务提交完成前,其日志(WAL)必须持久化。对于 MySQL 来说,需要保证事务提交前,redo log 落盘。虽然日志顺序写的性能,已经高于数据文件随机写的性能,但是如果每次事务提交,都需将 redo log 刷盘,效率较低。同时 MySQL 还要写 binlog,相当于每次事务提交需要两次 IO,很容易成为性能瓶颈。
为了解决上述性能问题,经过 MySQL 5.6/5.7/8.0 的不断优化,引入组提交技术和流水线技术。
1.2. redo log/binlog 的原子性和一致性
原子性比较好解决,MySQL 利用一个内部 2PC 机制实现 redo log 和 binlog 的原子提交,其中2PC 的协调者由 binlog 承担。
内部两阶段提交的流程简单描述为:
Prepare 阶段 :(1)InnoDB 将回滚段上的事务状态设置为 PREPARED;(2)将 redolog 写文件并刷盘;
2. 提交流水线
为解决上节提到的两个问题,经过 5.6/5.7/8.0 的逐步优化,两阶段提交的逻辑优化为:
每个 stage 一个队列,第一个进入该队列的线程成为 leader,后续进入的线程会阻塞直至完成提交。leader 线程会领导队列中的所有线程执行该 stage 的任务,并带领所有 follower 进入到下一个 stage 去执行,当遇到下一个 stage 为非空队列时,leader 会变成 follower 注册到此队列中。
而 redo log 刷盘从 Prepare 阶段移动到 flush stage,这样 leader 也可以将多个事务的 redo log 合并刷盘。同样 sync stage 的 leader 可以将多个事务的 binlog 合并刷盘。
每一个 stage 都是加锁的,保证 binlog 与 redo log 写入顺序是一致的。
总结下来,这套优化主要带来了两个好处:
Commit 阶段流水化作业,stage 内批处理,stage 之间可以并发,大大提升了写的并发度,进而提高吞吐与资源利用率。
redo log / binlog 合并刷盘,大幅减少 IO 次数。
3. 代码实现
3.1 Prepare
协调者的 Prepare 调用存储引擎的 ha_prepare_low 即可,下面这段注释说的很清楚,此时不持久化 InnoDB redo log。
3.2 Commit
MySQL 8.0.29 后将原来 slave 并行回放过程抽象成新的 stage0(原来这个流程也是有的,只是没有抽象为 stage0),其工作是协调多个回放线程的回放顺序,让事务提交顺序与主库一致。以下代码只有备库回放会走到。
stage 转换函数
事务提交三个 stage 之间的转换,都用的是 MYSQL_BIN_LOG::change_stage 函数,其主要逻辑是调用了 Commit_stage_manager::enroll_for。该函数在 8.0.29 版本里,加了很多WL#7846 处理逻辑,帮助备库在不开 binlog,但是并行回放的情况下,依旧可以和主库保持相同的提交序,这一部分我会从下面的核心代码里删除,感兴趣的朋友可以看下WL#7846 。
enroll_for 主要做了以下几件事:
1.判断自己是不是入队的第一个,如果是则为 leader,否则为 follower,enroll_for 的返回值为 true 则为 leader。
2.释放上个阶段持有的锁,先入队新的 stage,再释放上一个 stage 的锁,保证事务执行的顺序在每个 stage 相同,保证事务的正确性。注意:BINLOG_FLUSH_STAGE 没有上一个阶段的锁,入参 stage_mutex 为 nullptr。
3.follower 会阻塞等待在 m_stage_cond_binlog 条件变量上。
4.Leader 持有本阶段的锁(enter_mutex)。
Stage 1 -- BINLOG_FLUSH_STAGE
事务 flush 到 binlog (不 sync) ,代码中的解释:
/* Stage #1: flushing transactions to binary log While flushing, we allow new threads to enter and will process them in due time. Once the queue was empty, we cannot reap anything more since it is possible that a thread entered and appointed itself leader for the flush phase. */
1.change_stage,进入 BINLOG_FLUSH_STAGE 状态。
(3,4,5 在 process_flush_stage_queue 完成)
3.拿到 flush queue 的 head,清空 flush queue,以便新的线程进入作为 leader。调用 ha_flush_logs(true) 批量刷 redo log。
4.依次调用 MYSQL_BIN_LOG::flush_thread_caches 将每个事务缓存在 binlog_cache_mngr 里的信息 flush 到 binlog(cache)。调用路径:
MYSQL_BIN_LOG::flush_thread_caches|--binlog_cache_mngr::flush|----binlog_stmt_cache_data::flush|----binlog_trx_cache_data::flush|------binlog_cache_data::flush |--------MYSQL_BIN_LOG::write_transaction
5.判断是否需要 rotate。
6.将 binlog 写到 binlog 文件(不 sync),flush_cache_to_file
process_flush_stage_queue 执行 3-5 步,事务 redo 刷盘,将事务的信息写到 binary log
Stage 2 -- SYNC_STAGE
BINLOG_FLUSH_STAGE 阶段的 leader 带着一个链表进入 SYNC_STAGE 阶段,首先依旧调用 change_state 函数,可能成为该阶段的 leader,也可能成为 follower,因为此时 LOCK_sync 可能正在被做 sync 的线程持有。多个 flush queue 会因为等待锁而合并成一个 sync queue。
Sync 的后续流程:
1.判断本次要不要 sync
一个 SYNC_STAGE 的 leader 通过参数判断,本次是否需要 sync。sync_counter 变量代表进入 SYNC_STAGE 但是没有真正 sync 的 leader 的个数。当 MySQL 配置参数 sync_binlog 设置大于 1 时,并不是每个 leader 执行到这里都会 sync。
get_sync_period() 获得的值,即是 sync_binlog 参数的值。
注意,当 sync_binlog == 0 时,每个 leader 线程都要等待。当 sync_binlog == 1 时,同样每个 leader 线程都要等待,因为每个 leader 都要 sync。当 sync_binlog > 1 时,一部分 leader 线程就不用等待,接着执行,反正也不会 sync。
2.调用 sync_binlog_file 去 sync binlog。sync_binlog_file 中实现只有当 sync_period > 0 && ++sync_counter >= sync_period 时才真正 sync。
Stage 3 -- COMMIT_STAGE
依次将 redolog 中已经 prepare 的事务在引擎层提交,该阶段不用刷盘,因为 flush 阶段中的 redolog 刷盘已经足够保证数据库崩溃时的数据安全了。
COMMIT_STAGE 的主要工作包括:
2.唤醒所有等待的 follower,完成提交。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。