【华为云MySQL技术专栏】MySQL的WriteSet并行复制介绍
1. MySQL并行复制方案演进
MySQL的主备复制是基于binlog日志。早期版本中,只有单线程复制,即IO线程负责接收主库的binlog,并将其写入到备库的relay log中,SQL线程负责读取relay log中的event(日志事件),再在备库上进行回放。
一般来说,主备之间的复制瓶颈在于SQL线程回放的速度比主库的写入速度慢,导致主备延迟过大,进而影响备节点数据的实时性和备节点的读业务,最终只能将业务切换到主节点上执行,否则会造成业务受损。
为了缓解这个问题,则需要提高SQL线程回放的并行度。因此,MySQL引入了并行复制。
MySQL 5.6 基于库级别的并行复制
在MySQL 5.6中,可以通过设置参数 `slave_parallel_workers` 来启用多个SQL线程并行复制。这种并行复制仅适用于多库场景,即当主节点并发对不同库进行写操作时,备库的复制速度会有显著提升。
然而,大多数情况下,主节点并发操作,都是单库多表的场景,因此这种并行复制方式就有很大的局限性,大部分场景下会退化为单线程回放,并不能提高回放速度。
MySQL 5.7 基于组提交的并行复制
在MySQL 5.7中,新增了slave_parallel_type参数来控制选择并行回放的方式,可选值分别为DATABASE,LOGICAL_CLOCK,DATABASE是基于库级别的并行复制。其中,LOGICAL_CLOCK是基于组提交的并行方式,LOGICAL_CLOCK方式更大的提供并行度。
组提交(group commit):为了解决写redo和binlog时频繁刷磁盘的问题,将事务进行分组,让多个事务的刷盘动作合并为一次刷盘,减少磁盘读写,提供数据库并发的性能。如果事务能够同时提交成功,则它们之间就不会存在锁冲突,因此,这些事务也可以在备机上并行执行。
在MySQL支持基于组提交的并行复制后,主备延迟有较大程度的改善,但是一定程度上会受到主节点的并发度的影响。当主节点的每次组提交的事务数较少的时候,binlog在备节点上的回放的并发度也会因此而降低,即使这些事务之间并没有任何冲突。例如:
Trx1 -----L----C---------------------------------->
Trx2 ---------------L----C------------------------>
Trx3 ------------------------L----C--------------->
Trx4 --------------------------------L----C------->
假定这些事务之间并没有任何冲突,由于它们的LOGICAL CLOCK周期没有重叠,在备机上也只能串行回放。
为了解决这个问题,MySQL 8.0.1引入了基于WriteSet的复制。
MySQL 8.0 基于WRITESET的并行复制
在MySQL 8.0中, WriteSet并行复制的基本思想是:不同事务的不同的记录不重叠,如果不同事务修改的记录不重叠,则这些事务都可在备节点上并行回放。因此,并行方式的粒度从事务组内并行到记录级别。在写binlog时,会将事务所修改的行的hash值添加到事务的写集合中,根据写集合计算事务间的依赖关系,在备机实现并行回放。
2. MySQL 8.0 基于WRITESET的并行复制详情
2.1 WriteSet源码分析
事务writeset更新入口在add_pke()函数中,其中,pke 是 primary key equivalent 的缩写。add_pke处理逻辑,如图1所示:
图1 add_pke处理逻辑
如果表中不存在索引,则直接结束。如果存在索引,则进行如下步骤:
步骤一,将数据库名和表名信息写入临时变量。
步骤二,循环扫描表中的每个索引:如果索引不是唯一索引,跳过本次循环,继续下一次。
步骤三,循环两种生成数据的方式(MySQL格式和字符串格式):
- 将索引名字写入到 `pke` 中,接着,再将临时变量信息写入到 `pke` 中。通过循环扫描索引中的每一个字段,将每个字段的信息写入到 `pke` 中。
- 如果字段扫描完成,将生成 `pke` 的哈希值,写入到写集合中。
事务依赖函数入口为get_dependency,根据参数binlog_transaction_dependency_tracking选择不同的依赖模式,处理流程是依次递增。
void Transaction_dependency_tracker::get_dependency()
{
switch (m_opt_tracking_mode) {
case DEPENDENCY_TRACKING_COMMIT_ORDER:
m_commit_order.get_dependency(thd, sequence_number, commit_parent);
break;
case DEPENDENCY_TRACKING_WRITESET:
m_commit_order.get_dependency(thd, sequence_number, commit_parent);
m_writeset.get_dependency(thd, sequence_number, commit_parent);
break;
case DEPENDENCY_TRACKING_WRITESET_SESSION:
m_commit_order.get_dependency(thd, sequence_number, commit_parent);
m_writeset.get_dependency(thd, sequence_number, commit_parent);
m_writeset_session.get_dependency(thd, sequence_number, commit_parent);
break;
default:
DBUG_ASSERT(0);
m_commit_order.get_dependency(thd, sequence_number, commit_parent);
}
}
WriteSet依赖模式关键代码:
void Writeset_trx_dependency_tracker::get_dependency() {
// 检查是否能使用writeset
bool can_use_writesets =…
bool exceeds_capacity = false;
if (can_use_writesets) {
int64 last_parent = m_writeset_history_start;
// 遍历一个事务所有修改的行的hash值
for (std::vector<uint64>::iterator it = writeset->begin();
it != writeset->end(); ++it) {
// 对每一个hash值都去history中寻找是否有对应的hash
Writeset_history::iterator hst = m_writeset_history.find(*it);
if (hst != m_writeset_history.end()) {
// 如果一个行的hash存在于history中且对应的事务先于当前事务
if (hst->second > last_parent && hst->second < sequence_number)
// 修改当前事务所依赖的事务的sequence number
last_parent = hst->second;
// 标记该行由当前事务修改
hst->second = sequence_number;
} else {
// 将hash值和事务的sequence number插入到history中
if (!exceeds_capacity)
m_writeset_history.insert(
std::pair<uint64, int64>(*it, sequence_number));
}
}
// 同时没有主键和非空唯一键的表不能使用writeset
if (!write_set_ctx->get_has_missing_keys()) {
// 当前事务所操作的table都有主键的前提下
// 取last parent和commit parent中的较小值
// 作为当前事务的commit parent (last_committed)
commit_parent = std::min(last_parent, commit_parent);
}
if (exceeds_capacity || !can_use_writesets) {
// 超过history最大值或者当前事务不能使用writeset则清空当前history
m_writeset_history_start = sequence_number;
m_writeset_history.clear();
}
}
}
使用WriteSet时,表必须要有主键或唯一键。如果表无主键或唯一键,则会回退到commit_order方式进行并行复制。
下面是无主键表和主键表生成的last_commited和sequence对比:
SET GLOBAL
binlog_transaction_dependency_tracking='writeset';
drop table if exists tb1001;
CREATE TABLE `tb1001` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`c1` int(11) DEFAULT NULL,
`c2` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
flush logs;
insert into tb1001(c1,c2)select 1,now();
insert into tb1001(c1,c2)select 1,now();
insert into tb1001(c1,c2)select 1,now();
insert into tb1001(c1,c2)select 1,now();
update tb1001 set c2=now() where id=2;
insert into tb1001(c1,c2)select 1,now();
# GTID last_committed=0 sequence_number=1
#GTID last_committed=1 sequence_number=2
#GTID last_committed=2 sequence_number=3
#GTID last_committed=3 sequence_number=4
# GTID last_committed=4 sequence_number=5
#GTID last_committed=5 sequence_number=6
# GTID last_committed=0 sequence_number=1
#GTID last_committed=1 sequence_number=2
#GTID last_committed=1 sequence_number=3
#GTID last_committed=1 sequence_number=4
# GTID last_committed=2 sequence_number=5
#GTID last_committed=1 sequence_number=6
除了表要有主键或者唯一键外,还需要合理设计主键。例如,使用随机散列生成的主键可能会导致哈希冲突率变高,从而降低并行度。
2.2 相关参数说明
根据数据库配置高低设置binlog_transaction_dependency_history_size,默认配置为25000。对于性能有富余的实例,可以适当调大该参数,比如提高至100000,以找到更小的 commit parent,提高备库的回放并行度。对于内存和CPU紧张的实例,最好避免在 WriteSet上消耗太多资源。
binlog_transaction_dependency_history_size过大,不光消耗更多内存,还会降低冲突查询的效率。
3. 总结
从MySQL 5.6到MySQL 8.0,并行复制技术经历了从库级别到WRITESET级别的演进,每一次改进都显著提升了复制性能和效率。在MySQL 8.0中,基于WRITESET的并行复制机制,通过更细粒度的并行控制,进一步优化了复制性能,提升了系统的并发处理能力。不过,要充分发挥并行复制的优势,需要根据具体场景合理配置相关参数,定期监控和优化系统性能。
参考资料
1. WL#7165: MTS: Optimizing MTS scheduling by increasing the parallelization window on master:https://dev.mysql.com/worklog/task/?id=7165
2. WL#9556: Writeset-based MTS dependency tracking on master:
https://dev.mysql.com/worklog/task/?id=9556
- 点赞
- 收藏
- 关注作者
评论(0)