大厂如何基于binlog解决多机房同步mysql数据(二)?
前言
在前一篇文章中,老顾介绍了如何基于binlog实现多机房mysql同步的方案;在这篇文章最后遗留了回环问题,今天这篇文章我们就来看看如何解决回环的问题。
问题本质
所有产生的回环问题,就是北京DB产生了binlog;更新到了上海DB,上海DB又产生了binlog,北京的订阅服务又订阅到了此binlog;但此时由上海DB产生的binlog应该要过滤掉,而不是执行。
问题的本质只要我们能够过滤掉回环过来的binlog或者即使执行了回环过来的binlog也不会产生数据的更新即可。
我们先来看看,现在大家都用了哪些方案去解决此类问题。
往目标库插入不生成binlog
此方案的想法是就是让上海DB不产生binlog;设置参数set sql_log_bin=0,这样就不会产生binlog。
我们先看一下当前的binlog;
show binlog events
插入一条语句,再次查看binlog;的确没有产生新的binlog事件
通过这种方式,貌似可以解决数据回环问题。目标库不产生binlog,就不会被北京订阅组件同步到binlog。
但是这种方案是不可行的,因为我们上海的DB也是集群部署,如:一主多从;如果我们往master节点插入数据,如果不产生binlog,那么slave节点也无法同步数据。这个不是我们想要的。
控制binlog同步方向
既然不产生binlog不能解决问题。那我们控制回环过来binlog不执行,插入某个库之前,我们先判断这条记录是不是原本就是这个库产生的,如果是,那么就抛弃,也可以避免回环问题。
那我们看看如何给binlog加个标记,标记出来源哪个mysql集群。
ROW模式下记录sql
mysql主从同步,binlog复制一般有3种模式。STATEMENT,ROW,MIXED。默认情况下,STATEMENT模式只记录SQL语句,ROW模式只记录字段变更前后的值,MIXED模式是二者混合。binlog同步一般使用的都是ROW模式,高版本Mysql主从同步默认也是ROW模式。
我们想采取的方案是,在执行的SQL之前加上一段特殊标记,表示这个SQL的来源。例如
/*IDC1:DB1*/insert into users(name) values("gujiachun")
其中/IDC1:DB1/是一个注释,表示这个SQL原始是在IDC1的DB1中产生的。之后,在同步的时候,解析出SQL中的IDC信息,就能判断出是不是自己产生的数据。
然而,ROW模式下,默认只记录变更前后的值,不记录SQL。所以,我们要通过一个开关,让Mysql在ROW模式下也记录INSERT、UPDATE、DELETE的SQL语句。具体做法是,在mysql的配置文件中,添加以下配置:
binlog_rows_query_log_events =1
这个配置可以让mysql在binlog中产生ROWS_QUERY_LOG_EVENT类型的binlog事件,其记录的就是执行的SQL。
通过这种方式,我们就记录下的一个binlog最初是由哪一个集群产生的,之后在同步的时候,判断目标机房和当前binlog中包含的机房相同,则抛弃这条数据,从而避免回环。
这种思路,功能上没问题,但是在实践中,确实非常麻烦。首先,让业务对执行的每条sql都加上一个这样的标识,几乎不可能。另外,如果忘记加了,就不知道数据的来源了。
如果采用这种方案,可以考虑在数据库访问层中间件层面添加支持在sql之前增加/../的功能,统一对业务屏蔽。即使这样,也不完美,不能保证所有的sql都通过中间件来来写入,例如DBA的一些日常运维操作,或者手工通过mysql命令行来操作数据库时,肯定会存在没有添加机房信息的情况。
总的来说,这个方案没有落地执行。
通过附加表记录binlog产生源集群信息
这种方案目前很多公司使用。大致思路是,在db中都加一张额外的表,例如叫direction,记录一个binlog产生的源集群的信息。例如
CREATE TABLE `direction` (
`idc` varchar(255) not null,
`db_cluster` varchar(255) not null,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
idc字段用于记录某条记录原始产生的IDC,db_cluster用于记录原始产生的数据库集群(注意这里要使用集群的名称,不能是server_id,因为可能会发生主从切换)。
假设用户在IDC1的库A插入的一条记录(也可以在事务中插入多条记录,单条记录,即使不开启事务,mysql默认也会开启事务):
BEGIN;
insert into users(name) values("gujiachun”);
COMMIT;
那么A库数据binlog通过更新数据同步到目标库B时,可以提前对事务中的信息可以进行一些修改,如下所示:
BEGIN;
#往目标库同步时,首先额外插入一条记录,表示这个事务中的数据都是A产生的。
insert into direction(idc,db_cluster) values("IDC1”,"DB_A”)
#插入原来的记录信息
insert into users(name) values("tianshouzhi”);
COMMIT;
之后B库的数据往A同步时,就可以根据binlog中的第一条记录的信息,判断这个记录原本就是A产生的,进行抛弃,通过这种方式来避免回环。这种方案已经已经过很多的公司的实际验证。
通过GTID
Mysql 5.6引入了GTID(全局事务id)的概念,极大的简化的DBA的运维。在数据同步的场景下,GTID依然也可以发挥极大的威力。
GTID 由2个部分组成:
server_uuid:transaction_id
其中server_uuid是mysql随机生成的,全局唯一。transaction_id事务id,默认情况下每次插入一个事务,transaction_id自增1。我们看看如何避免回环、数据重复插入的问题。
GTID提供了一个会话级变量gtid_next,指示如何产生下一个GTID。可能的取值如下:
•AUTOMATIC:自动生成下一个GTID,实现上是分配一个当前实例上尚未执行过的序号最小的GTID。
•ANONYMOUS:设置后执行事务不会产生GTID,显示指定的GTID。
默认情况下,是AUTOMATIC,也就是自动生成的,例如我们执行sql:
insert into users(name) values("tianbowen”);
产生的binlog信息如下:
可以看到,GTID会在每个事务(Query->...->Xid)之前,设置这个事务下一次要使用到的GTID。
从源库订阅binlog的时候,由于这个GTID也可以被解析到,之后在往目标库同步数据的时候,我们可以显示的的指定这个GTID,不让目标自动生成。也就是说,往目标库,同步数据时,变成了2条SQL:
SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1’
insert into users(name) values("tianbowen")
由于我们显示指定了GTID,目标库就会使用这个GTID当做当前事务ID,不会自动生成。同样,这个操作也会在目标库产生binlog信息,需要同步回源库。再往源库同步时,我们按照相同的方式,先设置GTID,在执行解析binlog后得到的SQL,还是上面的内容
SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1'
insert into users(name) values("tianbowen")
由于这个GTID在源库中已经存在了,插入记录将会被忽略,演示如下:
注意这里,对于一条insert语句,其影响的记录函数居然为0,也就会插入并没有产生记录,也就不会产生binlog,避免了循环问题。
如何做到的呢?mysql会记录自己执行过的所有GTID,当判断一个GTID已经执行过,就会忽略。通过如下sql查看:
上述value部分,冒号":"前面的是server_uuid,冒号后面的1-5,是一个范围,表示已经执行过1,2,3,4,5这个几个transaction_id。这里就能解释了,在GTID模式的情况下,为什么前面的插入语句影响的记录函数为0了。
显然,GTID除了可以帮助我们避免数据回环问题,还可以帮助我们解决数据重复插入的问题,对于一条没有主键或者唯一索引的记录,即使重复插入也没有,只要GTID已经执行过,之后的重复插入都会忽略。
当然,我们还可以做得更加细致,不需要每次都往目标库设置GTID_NEXT,这毕竟是一次网络通信。在往目标库插入数据之前,先判断目标库的server_uuid是不是和当前binlog事务信息携带的server_uuid相同,如果相同,则可以直接丢弃。查看目标库的gtid,可以通过以下sql执行:
GTID应该算是一个终极的数据回环解决方案,mysql原生自带,比添加一个辅助表的方式更轻量,开销也更低。需要注意的是,这倒并不是一定说GTID的方案就比辅助表好,因为辅助表可以添加机房等额外信息。在一些场景下,如果下游需要知道这条记录原始产生的机房,还是需要使用辅助表。
总结
讲到这里关于多机房同步mysql数据的方案已经介绍完整;当然具体用哪些开源组件在这些方面做的比较好。建议的首选,是canal/otter组合。
canal的作用就是类似于前面所述的binlog订阅组件,拉取解析binlog。otter是canal的客户端,专门用于进行数据同步,类似于前文所讲解的更新数据组件。并且,canal的最新版本已经实现了GTID。
当然现在最新的canal客户端,提供了client.adapter同步组件,只要简单的配置,就能达到数据同步。小伙伴们可以自行尝试。以后老顾也会介绍canal的实战。谢谢!!!