数据库内核月报 - 2018 / 06

MariaDB · 特性分析 · 基于GTID的复制分析

Replication为数据库提供了高可用性和可扩展性解决方案,基于GTID的Replication能够更好的处理复制重连和切换Server复制。本文将主要分析MariaDB基于GTID的Replication设计与实现,并与MySQL做对比。

MariaDB的GTID

MariaDB GTID定义与实现

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相关参数

  • 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(&gtid_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的不同

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(&gtid_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不同

MySQL可以用MASTER_AUTO_POSITION为1,自动使用retrieved_set来作为位点标识。当retrieved_set集合并不是Master端子集的时候并不会导致IO线程报错。所以上述master_pos在MariaDB中的问题并不会出现在MySQL中。

MariaDB并发复制

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,就依然会并发执行这些操作。

与MySQL的不同

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还是有着很不一样的地方。