数据库内核月报 - 2014 / 12

MySQL · 性能优化 · 并行复制外建约束问题

背景

mysql 主备同步是通过binlog来进行的,备库的 IO 线程从主库拉取binlog,SQL线程将拉取的binlog应用到备库,在5.6之前,备库只有一个线程应用binlog,主库的更新量大,且备库的执行效率低时,就会造成了大量从主库拉取的binlog来不及执行,因此造成了主备延迟问题。为了解决主备延迟,需要提高备库的执行效率,阿里MySQL 设计并开发了并行复制功能,所谓并行复制,指的是应用binlog的线程数量是多个的,而不是原生的单个线程,经过测试可以极大的提高复制性能(有3X的性能提升),在并行复制中,一个 IO 线程,一个分发线程,多个sql_thread,分发线程读取relay log,并将读取的relay log 分发给多个sql_thread, 从而实现并行化的效果。

原理

分发线程的分发原理是依据当前事务所操作的表的名称来进行分发的,如果事务是跨表的(一个事务更新多张表),则需要等待已分配的该表相关的事务全部执行完毕,才会继续分发,其分配行为的伪码可以简单的描述如下:

get_slave_worker
if (contains_partition_info(log_event))
table_name= get_db_name(log_event);
entry {table_name, worker_thread, usage} = map_table_to_worker(table_name);
while (entry->usage > 0)
wait();
return worker;
else if (last_assigned_worker)
return last_assigned_worker;
else
push into buffer_array and deliver them until come across a event that have partition info

问题描述(testcase):

drop table t2 if exists t2;
drop table t1 if exists t1;
create table t1(c1 int primary key, c2 int);
create table t2(c1 int primary key, c2 int , foreign key (c2) references t1(c1));
insert into t1 values(1,1);
insert into t1 values(2,2);
insert into t2 values(1,1);
insert into t2 values(2,2);

以下两个语句在备库的执行顺序不同,结果会不同

delete from t2 where c2=1; (语句1)

update t1 set c1=3 where c1=1;(语句2)

如果语句2先于语句1在备库执行,则会报外建约束错误,因为在上述的分发原理中没有考虑到外建约束问题,这种情况下,只有串行化处理了,当然,你可以执行:set global foreign_key_checks=off;然后start slave;在类似语句执行完后,再恢复foreign check,但是这样做真正安全吗?答案是不一定的……

情况1:

create table t1(c1 int primary key, c2 int);

create table t2(c1 int primary key, c2 int , foreign key (c2) references t1(c1));

在这种定义下,如果不检测foreign key,则不会有问题,因为对t1, t2的操作都会记录binlog;

情况2:

create table t1(c1 int primary key, c2 int);

create table t2(c1 int primary key, c2 int , foreign key (c2) references t1(c1) on delete cascade);

在这种定义下,如果不检测foreign key,则会有问题,因为对表t1的操作会影响t2表,在检测foreign key的时候,会进行相应的cascade操作,如果不检测foreign key,则不进行级联操作,这种问题一旦发生,则会引起主备不一致问题。

解决方法

5.6 并行复制没有此问题,5.6中在检测到foreign key的事件时,会等待已经分发的所有binlog都已执行完再执行,因此解决了此问题。

改进方案 这个方案虽然能解决问题,但是若系统中只要出现一个外键关系,并且持续有更新,会导致持续性的回退到单线程方案,那么多线程复制的效果就会大打折扣。实际上这个做法比较极端,改进的方案是,遇到有foreign key 的表,应该将其分发到依赖他的表的同一个sql thread 中。这样执行这些事务时,其他表的并行复制仍能继续。