MySQL并行复制 - saviochen/mysql_docs GitHub Wiki

1 MySQL的复制架构

master将数据修改记录在binlog文件中,slave通过change master to命令申请主从复制时,启动IO线程连接到master,master端dump线程负责将binlog发送给从库IO线程。slave IO线程接到binlog写到slave本地的relay log中,slave的sql线程负责将relay log中的日志 apply到从库。

从架构图中可以看到,最早的主从复制架构中,slave只有一个sql线程负责重放relay log,也就是master库上所有的并发操作在slave上都只有一个线程执行,slave的relay log回放速度很容易在主库压力大时称为瓶颈,导致主从复制延迟。

2 Schema并行

MySQL官方在5.6中引入了一个比较简单并行复制方案,如果事务分别属于不同的schema,并且不是ddl语句并且不跨schema操作,那么就可以并行回放,否则需要等所有并行回放的workers执行完后再执行当前日志中内容。但在实际应用中,单库多表才是更常见的情况,schema级别的并行实际收益比较有限。

3 Logical Clock并行

虽然5.6中的并行复制在大多数应用场景中对回放速度的提升不大,但是该架构却成为了后来MySQL并行复制的基础——即在Slave上并行回放RelayLog,SQL线程负责判断能否并行回放,并分配给Work线程回放。

5.6 中引入Group Commit技术,这是为了解决事务提交的时候需要fsync导致并发性不够而引入的。简单来说,就是由于事务提交时必须将Binlog写入到磁盘上而调用fsync,这是一个代价比较高的操作,事务并发提交的情况下,每个事务各自获取日志锁并进行fsync会导致事务实际上以串行的方式写入Binlog文件,这样就大大降低了事务提交的并发程度。5.6中采用的Group Commit技术将事务的提交阶段分成了 Flush, Sync, Commit 三个阶段,每个阶段维护一个队列,并且由该队列中第一个线程负责执行该步骤,这样实际上就达到了一次可以将一批事务的Binlog fsync到磁盘的目的,这样的一批同时提交的事务称为同一个Group的事务。Group Commit 虽然是属于并行提交的技术,但是却意外的解决了从机上事务并行回放的一个难题:既如何判断哪些事务可以并行回放。如果一批事务是同时Commit的,那么这些事务必然不会互斥的持有锁,也不会有执行上的相互依赖,因此这些事务必然可以并行的回放。

因此MySQL 5.7 中引入了新的并行回放类型, 由参数 slave_parallel_type决定,默认值DATABASE将会采用5.6版本中的SCHEMA级别的并行回放,设置为LOGICAL_LOCK则会采用基于GroupCommit的并行回放,同一个Group内的事务将会在Slave上并行回放。为了标记事务所属的组,MySQL 5.7 版本在产生 Binlog 日志时会有两个特殊的值记录在Binlog Event中, last_committed和sequence_number, 其中 last_committed 指的是该事务提交时,上一个事务提交的编号,sequence_number是事务提交的序列号,在一个Binlog文件内单调递增。如果两个事务的 last_committed 值一致,这两个事务就是在一个组内提交的。

4 Writeset并行

在5.7中基于逻辑时钟 Logical_Clock 的并行复制仍然有不尽人意的地方,必须是在主上并行提交的事务才能在从上并行回放,如果主上并发压力不大,那么就无法享受到并行复制带来的好处。5.7 中引入了binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 两个参数,通过让Binlog在执行fsync 前等待一小会来提高Master上组提交的比率。但是无论如何,从上并行回放的速度还是取决于主上并行提交的情况。

MySQL 8.0中引入了一种新的机制来判断事务能否并行回放,通过Writeset检测事务在运行过程中是否存在写冲突来决定从机上的回放顺序,这使得从机上的并发程度不再依赖于主机。使用Writeset有一定的限制:DDL不可以使用writeset;没有主键或者非空唯一键的表或者外键表,事务的依赖获取会退化到COMMIT_ORDER的方式。

在开启了gtid,且binlog_format为row,且开启writeset并行复制的实例上,对包含主键或者唯一键的行修改时,会按照db,table,index,value按照一定规则拼接后计算hash(如果有外键还会加入外键的信息),事务提交时会到历史hash集合std::map<hash, sequence number>中寻找是否历史事务中是否修改过该行,如果没修改,那么本事务的last_committed设置为m_writeset_history_start(上一轮历史hash集合集中提交时的sequence number)。如果当前修改的行的hash出现在历史hash集合时,并且找到的修改并非本事务而为(对应的sequence number比当前sequence number小),则将当前事务的last_committed设置为找到的sequence number。当历史hash集合大小超过binlog_transaction_dependency_history_size时,清空历史hash集合,将清空时的sequence number设置为m_writeset_history_start。

Writeset并行复制中引入参数 binlog_transaction_depandency_tracking 用于控制如何决定事务的依赖关系。该值有三个选项:默认的 COMMIT_ORDERE 表示继续使用5.7中的基于组提交的方式决定事务的依赖关系;WRITESET 表示使用写集合来决定事务的依赖关系;还有一个选项 WRITESET_SESSION 表示使用 Writeset 来决定事务的依赖关系,但是同一个Session内的事务不会有相同的 last_committed值。

writeset尽可能降低了每个事务的last_committed,使得binlog在从库并行回放时可更高度地并行。