Author: 勉仁
Replication为数据库提供了高可用性和可扩展性解决方案,基于GTID的Replication能够更好的处理复制重连和切换Server复制。本文将主要分析MariaDB基于GTID的Replication设计与实现,并与MySQL做对比。
GTID(Global transaction ID)表示作为一个执行单元的event group,可以理解为一个“事务”(虽然也包含的非事务的DML和DDL操作)。当event group从Master复制到Slave执行后,GTID是被保留的,用来在有复制关系的server中识别对应的GTID。
使用GTID可以方便的一个Slave切换到从另一个Master Server复制,因为GTID在有复制关系的Server中是被唯一标识的。而且执行的GTID信息记录在事务表中,可以做到slave的状态是crash-safe。
在MariaDB中,GTID由三部分组成,三部分之间用’-‘分隔,例如0-161002-1:
struct rpl_gtid
{
uint32 domain_id;
uint32 server_id;
uint64 seq_no;
};
第一个是无符号32位整数的domain_id,在多源复制、多主拓扑复制中每个会写入的Master通常需要设置不同的Domain_id。
第二个是无符号32位整数的server_id,集群中每个server需要设置唯一的server id,标识最初写入binlog的server,用来避免循环复制。
第三个是服务号64位整数的seq_no,seq_no在每个写入server产生binlog中是递增的。
本文为分析MariaDB复制,创建了互为主备的两个server。
server1: server_id 161002 domain_id 0
server2: server_id 161003 domain_id 0
均配置log_slave_updates
gtid_slave_pos
只读变量。用来表示各个复制domain,最后复制到该server上执行的GTID,例如”0-161002-3”。
当master_use_gtid=slave_pos,可以通过设置该全局变量来决定slave server复制的位点。
gtid_binlog_pos
该变量是一个只读参数,表示各个domain,最后写入到binlog中的GTID。
如下面例子,分别用domain_id为0、1创建表t、t1之后,gtid_binlog_pos为’0-161002-1,1-161002-1’。
> select @@global.gtid_binlog_pos;
+--------------------------+
| @@global.gtid_binlog_pos |
+--------------------------+
| |
+--------------------------+
select @@global.gtid_domain_id;
+-------------------------+
| @@global.gtid_domain_id |
+-------------------------+
| 0 |
+-------------------------+
> create table t(c1 int);
> select @@global.gtid_binlog_pos;
+--------------------------+
| @@global.gtid_binlog_pos |
+--------------------------+
| 0-161002-1 |
+--------------------------+
> set gtid_domain_id=1;
> create table t1 (c1 int);
> select @@global.gtid_binlog_pos;
+--------------------------+
| @@global.gtid_binlog_pos |
+--------------------------+
| 0-161002-1,1-161002-1 |
+--------------------------+
gtid_binlog_state
动态变量。该系统变量是由各个domain和server最后执行的GTID信息组成。这里和gtid_binlog_pos不同的是,这里除了domain_id,不同的server_id也会单独记录。
例如在上述复制关系中,在server_id为161003中创建表t2,可以看到复制并在server 161002中应用后,gtid_binlog_pos记录了domain_id分别为0、1最后应用的GTID: ‘0-161003-2,1-161002-1’, 而gtid_binlog_state就考虑了不同server ‘0-161002-1,0-161003-2,1-161002-1’。
> create table t2(c1 int);
server_id 161002
> select @@global.gtid_binlog_pos, @@global.gtid_binlog_state;
+--------------------------+----------------------------------+
| @@global.gtid_binlog_pos | @@global.gtid_binlog_state |
+--------------------------+----------------------------------+
| 0-161003-2,1-161002-1 | 0-161002-1,0-161003-2,1-161002-1 |
+--------------------------+----------------------------------+
同时gtid_binlog_pos是只读变量,reset master后,可以通过gtid_binlog_state的设置来恢复server中已经写入binlog的信息。
gtid_current_pos
只读变量。该变量记录了各个domain在该server上执行的最后一个GTID,包含了该server写入和通过复制关系执行的。通过复制关系执行的即使未写入binlog(log_slave_update为0),该变量也会记录对应的GTID信息。
例如下面例子,在161003中log_slave_update=0,这个时候在161002中执行写入,可以看到161003中gtid_current_pos为’0-161002-3,1-161002-1’,而gtid_binlog_pos没有变。
161002
> insert into t values(2);
161003
show variables like "%gtid%";
+------------------------+----------------------------------+
| Variable_name | Value |
+------------------------+----------------------------------+
| gtid_binlog_pos | 0-161003-2,1-161002-1 |
| gtid_binlog_state | 0-161002-1,0-161003-2,1-161002-1 |
| gtid_current_pos | 0-161002-3,1-161002-1 |
实际该变量是把gtid_binlog_pos合并到gtid_slave_pos中,如果同一个server_id在gtid_slave_pos,且gtid_binlog_pos的sequence number,就使用gtid_binlog_pos的值。否则就用gtid_slave_pos。
uchar *
Sys_var_gtid_current_pos::global_value_ptr(THD *thd, const LEX_CSTRING *base)
{
String str;
char *p;
str.length(0);
if (rpl_append_gtid_state(&str, true) ||
!(p= thd->strmake(str.ptr(), str.length())))
{
my_error(ER_OUT_OF_RESOURCES, MYF(0));
return NULL;
}
return (uchar *)p;
}
/*
Format the current GTID state as a string, for returning the value of
@@global.gtid_slave_pos.
If the flag use_binlog is true, then the contents of the binary log (if
enabled) is merged into the current GTID state (@@global.gtid_current_pos).
*/
int
rpl_append_gtid_state(String *dest, bool use_binlog)
{
int err;
rpl_gtid *gtid_list= NULL;
uint32 num_gtids= 0;
if (use_binlog && opt_bin_log &&
(err= mysql_bin_log.get_most_recent_gtid_list(>id_list, &num_gtids)))
return err;
err= rpl_global_gtid_slave_state->tostring(dest, gtid_list, num_gtids);
my_free(gtid_list);
return err;
}
gtid_strict_mode
GTID strict mode打开的时候,一些会导致binlog在server间一些场景会额外抛错出来。例如:
1、如果一个slave尝试复制比当前binlog中对应replication domain的sequence number还要小的GTID时候会报错出来。
2、尝试手动设置一个比当前sequence number还要小的GTID(通过@@SESSION.gtid_seq_no)时候报错。
3、如果slave的开始GTID位点在master中无法找到对应binlog,那么即使存在更大sequence number的GTID也会报错出来。
gtid_domain_id
Global、Session变量,用来决定新产生GTID的domain id。
last_gtid
最后一个写入binlog的事务或者Statement的GTID。
server_id
Global、Session变量。用来设置GTID中的server id。
gtid_seq_no
Session变量。用来设置即将写入GTID的sequence number。
gtid_ignore_duplicates
当设置后,多源复制中从不同Master中复制到的GTID,只有一个会被应用,其他的会忽略。这样对于指定domain的复制,只有一个sequence number用来决定一个给定的GTID是否被应用了。
MySQL的GTID是server_uuid和transaction_id组成,没有用于多源复制的domain_id概念。
GTID = source_id:transaction_id
在MariaDB中GTID相关执行信息都是用position,即单个GTID信息表示。在MySQL中使用了表达信息更多的GTID Set,用GTID集合来表示已经执行的GTID信息。例如某个server上gtid_executed为’528c2958-6966-11e8-8cd1-7cd30ac42730:1-9’。
DBA可以在MySQL上RESET MASTER后,设置gtid_purged来重置gtid_executed信息,gtid_purged也是GTID集合。在MariaDB中,RESET MASTER可以通过gtid_binlog_state来重置gtid信息,也可以动态设置gtid_slave_pos。
MySQL可以设置gtid_next来确定session中下一个执行的GTID。但在session中不可以设置server_id,MySQL这是一个全局变量。在MariaDB中,可以在session上设置gtid_domain_id、server_id、gtid_seq_no来指定下一个GTID信息。
可以说MariaDB与MySQL GTID相比多了domain的概念,在server已执行GTID信息表示上,MariaDB使用的是最后一个执行的GTID来表示,而MySQL使用的是GTID Set。
MariaDB除了使用指定master binlog文件名和位点的方式开始replication外,还支持通过master_user_gtid来使master自动寻找binlog位点开始同步。
master_user_gtid的指定支持current_pos和slave_pos两种方式,其实位点分别使用gtid_current_pos和gtid_slave_pos。
MariaDB在start_slave_threads的时候会获取要使用的GTID位点到mi->gtid_current_pos中,当使用current_pos方式时候,和对应变量意义一样,会包含binlog写入的GTID信息。当使用slave_pos就只用复制来的slave position来定位。然后通过设置IO线程和Master连接的slave_connect_state变量来告诉Master初始位点。
int start_slave_threads()
{
if (mi->using_gtid != Master_info::USE_GTID_NO &&
!mi->slave_running && !mi->rli.slave_running)
{
error= rpl_load_gtid_state(&mi->gtid_current_pos, mi->using_gtid ==
Master_info::USE_GTID_CURRENT_POS);
}
}
/*
Load the current GTID position into a slave_connection_state, for use when
connecting to a master server with GTID.
If the flag use_binlog is true, then the contents of the binary log (if
enabled) is merged into the current GTID state (master_use_gtid=current_pos).
*/
int
rpl_load_gtid_state(slave_connection_state *state, bool use_binlog)
{
int err;
rpl_gtid *gtid_list= NULL;
uint32 num_gtids= 0;
if (use_binlog && opt_bin_log &&
(err= mysql_bin_log.get_most_recent_gtid_list(>id_list, &num_gtids)))
return err;
err= state->load(rpl_global_gtid_slave_state, gtid_list, num_gtids);
my_free(gtid_list);
return err;
}
static int get_master_version_and_clock(MYSQL* mysql, Master_info* mi)
{
query_str.append(STRING_WITH_LEN("SET @slave_connect_state='"),
system_charset_info);
if (mi->gtid_current_pos.append_to_string(&query_str))
{
}
}
从表达意义上,MariaDB的gtid_current_pos和MySQL的gtid_executed更像,表达了执行过的GTID,而不只是复制来的。而且gtid_current_pos可以在server没有做过slave, slave_pos为空的情况下自动开始复制。
但是MariaDB的gtid_current_pos仅带有每个domain上的位点信息,主要是sequence number。如果Master和Slave搭建的是双向同步通道,当Master和Slave之间复制关系中断一下,在恢复之前Master上有写入,那么就会出现当Master上再次start slave,这个时候搭建复制关系的时候sequence number比对端大,就会出现复制中断。宕机HA切换后,新Master上有写入或者老的Master有数据未同步到,再和重新启动的slave之间搭建复制关系也很容易出错。
例如在双向复制建立的情况下,当数据完全同步后,做如下操作,就会看到对应报错信息。
server_id 161003
stop slave;
server_id 161002
stop slave;
insert into t values(10);
start slave;
这时候show slave status可以看到报错
Got fatal error 1236 from master when reading data from binary log: 'Error: connecting slave requested to start from GTID 0-161002-2, which is not in the master's binlog'
如果使用slave_pos,就不会出现上述问题。
MySQL可以用MASTER_AUTO_POSITION为1,自动使用retrieved_set来作为位点标识。当retrieved_set集合并不是Master端子集的时候并不会导致IO线程报错。所以上述master_pos在MariaDB中的问题并不会出现在MySQL中。
MariaDB中仅记录了最后一个应用的binlog position信息,在并发复制的时候,MariaDB设计的是会保证同一个domain下sequence number的单调递增。其并发也是在这一前提下实现的。
并发参数slave_parallel_mode的可以配置的值有none, minimal, conservative, optimistic, aggressive。
none
取消并发复制。
minimal
只有在事务commit阶段并发。
conservative
在主库group commit的事务可以在slave并发执行。
optimistic
乐观的并发方式,尽可能的并发执行事务,对于发生冲突的事务做回滚操作,然后让前面的事务执行完,再重试后面的事务。在已知冲突事务比较少的时候使用。
aggressive
optimistic的时候,如果在master上发生锁等待,或者设置了skip_parallel_replication,那么在slave上执行的时候,事务时不会并发的。但如果设置为aggressive,就依然会并发执行这些操作。
MySQL5.7中可以通过设置slave_parallel_type指定并发方式为DATABASE或者LOGICAL_CLOCK。
指定DATABASE方式,即按照事务涉及到的DB Name做并发,可以很容易根据该模式改为基于TableName。
指定LOGICAL_CLOCK方式,可以按照主库执行时候存在加锁重叠,即在主库执行明确不会有冲突的情况就可以并发。可以参考月报MySQL 5.7 LOGICAL_CLOCK 并行复制原理及实现分析。
同时MySQL中并发执行的时候,在binlog中记录的event group顺序是可以和主库不一样的,gtid_executed在中间时刻可能存在空洞。
MariaDB虽然在replication中基本命令和MySQL兼容,但在GTID内部设计和相关实现上和MySQL还是有着很不一样的地方。