Skip to the content.

(转载)MySQL主从复制idempotent模式以及同步错误处理预案

seanlook.com 原文链接 http://seanlook.com/2018/03/11/mysql-replication-error-and-idempotent/

好像链接变为 http://xgknight.com/2018/03/11/mysql-replication-error-and-idempotent/

1. slave_exec_mode 参数作用

slave_exec_mode 可以在主从复制中遇到 duplicate-key 和 no-key-found 错误时,自动覆盖或者略过binlog里面这个row_event,避免报错停止复制。

这个参数原本是解决像 NDB Cluster 多节点写入冲突的情况,也可以在普通主从、双主、环形复制等情况下解决冲突,保持幂等性。幂等性怎么定义,感兴趣的可以阅读The differences between IDEMPOTENT and AUTO-REPAIR mode)。

set global slave_exec_mode=IDEMPOTENT (可以动态修改)使从库运行在 幂等模式,对1062,1032等不同的错误类型,有不同的处理:

  1. write_row event 遇到主键冲突或唯一索引冲突,这一行被覆写(delete + insert)。 delete时候不是full value match,仅需要主键或唯一索引找到记录则删除
  2. delete_row event 遇到记录不存在,忽略这一行
  3. update_row event 修改唯一索引导致的冲突,忽略这一行

注意:

2. slave-skip-errors

这个参数不能在线修改,只能加到配置文件里面或者启动的时候带上--slave-skip-errors=1032,1062。除非你真的理解它skip掉了什么,否则不建议使用。

讲一个我所遇到的坑。在我们的一个分库项目中,需要把一个database里面的数据拆成32份,于是做了个主从,把从库里面不需要的那份删除,但复制过来肯定会报 HA_ERR_KEY_NOT_FOUND 错误,于是这也是所期望的,就设置了--slave-skip-errors=1032

但接下来就出现 1062:HA_ERR_FOUND_DUPP_KEY 错误!从库只会删数据,不会写入和更新,怎么会出现重复数据?读者不妨试想一下为什么。

这里做个说明:

① insert into t values (1, 'a'), (2, 'b'), (3, 'c');

② begin;
③ delete from t where id=1;
④ delete from t where id in (1, 2, 3);
⑤ insert into t where (3, 'c'), (4, 'd'), (5, 'e');
⑥ update t set ... id=1;
⑦ commit;

slave-skip-errors 参数作用的是 statement,上面的slave_exec_mode作用的是row 比如上面那段sql在RBR复制到从库时发现④的 id=2 不存在:

相关验证可以看后文。

3. sql_slave_skip_counter

MySQL主从复制出现异常的时候,如不及时处理,延迟的时间会越来越长,所以有时候哪怕允许极少量的数据不一致,也要让数据继续同步,往往会用到 sql_slave_skip_counter 参数来跳过异常事件。 用法:

mysql> show slave status\G -- 1062可以看到是哪条记录重复

mysql> slave stop;
mysql> set GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
mysql> slave start;

4. GTID复制异常处理

主从开启了GTID(select @@gtid_mode),就不能再用 sql_slave_skip_counter 来跳过错误,需要注册一个空gtid event来代替原本执行报错的event。比如:

mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 10.153.173.149
                  Master_User: replicator
                  Master_Port: 3027
                Connect_Retry: 60
              Master_Log_File: mysql-bin.014670
          Read_Master_Log_Pos: 181716556
               Relay_Log_File: slave-relay.028871
                Relay_Log_Pos: 166693104
        Relay_Master_Log_File: mysql-bin.014670
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
                   Last_Errno: 1032
                   Last_Error: Could not execute Update_rows event on table mysql.user; Can't find record in 'user', Error_code: 1032; handler error HA_ERR_KEY_NOT_FOUND; the event's master log mysql-bin.014670, end_log_pos 166693925
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 166692941
              Relay_Log_Space: 688
              Until_Condition: None
               Master_SSL_Key: 
        Seconds_Behind_Master: NULL
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 1032
               Last_SQL_Error: Could not execute Update_rows event on table mysql.user; Can't find record in 'user', Error_code: 1032; handler error HA_ERR_KEY_NOT_FOUND; the event's master log mysql-bin.014670, end_log_pos 166693925 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 1088575531
                  Master_UUID: 108f89d5-d74f-11e7-942f-7cd30ac4755e
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
           Master_Retry_Count: 86400
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: 108f89d5-d74f-11e7-942f-7cd30ac4755e:8077-122925776
            Executed_Gtid_Set: 108f89d5-d74f-11e7-942f-7cd30ac4755e:1-122925773,
8b101f33-f327-11e7-89c3-7cd30ac333bc:1-1425,
fba62795-d74e-11e7-942e-7cd30ac4e7fc:1-630905526
                Auto_Position: 0
1 row in set (0.00 sec)

跳过处理:

mysql> stop slave;
mysql> set gtid_next='108f89d5-d74f-11e7-942f-7cd30ac4755e:122925774';
mysql> begin; commit;  -- empty trx
mysql> set gtid_next='AUTOMATIC';  -- auto position
mysql> start slave;

上面 gtid_next 的值 108f89d5-d74f-11e7-942f-7cd30ac4755e:122925774 是个会话级变量。

5. pt-slave-restart

pt-slave-restart 可以快速方便的恢复主从复制错误,并且支持普通 file:postion 和 GTID 模式。

修复的原理就是运行上面的 sql_slave_skip_countergtid_next,只是它可以自动的帮DBA识别错误码,或者匹配error_msg,stop/start slave,并且默认情况下它是一直运行 检测+修复。

pt-slave-restart --user=dbuser --password=xxxx --socket=/var/lib/mysql/mysql.sock --error-numbers=1032,1677,1051

几点说明一下:

6. 手动处理复制错误并修复

这种处理思路是写程序实现,遇到1032错误,在主库Binlog里面解析出before image,在从库插入,再stop/start slave;遇到1062错误,在从库删除这条数据(可以根据主库binlog after image取数据,也可以根据duplicate key中提示的重复记录),再stop/start/slave。

不需要skip操作,也不需要后续修复数据(只是不会因为有跳过event而产生不一致),如果从主库拿binl log或者从库拿relay log有困难,也可使用 pymysql-replication 来伪装成从库拿到出错的 binlog postion 的内容,解析再用。

当然为保险起见,已经出现不一致的还是要 pt-table-checksum 跑一下。

7. 附: 测试 slave_skip_errors, slave_exec_mode

CREATE TABLE `t_repl_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(30) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

insert into t_repl_test values(1,'a',10), (2,'b',20), (3,'c',30), (4,'d',40),(5,'e',50);

# 初始化测试数据
# master:
delete from t_repl_test where id=2;

# slave:
delete from t_repl_test where id=3; insert into t_repl_test values(2,'b',20);

每次测试前,数据都初始化成下面的:

master slave
mysql> select * from t_repl_test;+—-+——+——+| id | name | age |+—-+——+——+| 1 | a | 10 || 3 | c | 30 || 4 | d | 40 || 5 | e | 50 |+—-+——+——+* mysql> select from t_repl_test;+—-+——+——+| id | name | age |+—-+——+——+| 1 | a | 10 || 2 | b | 20 || 4 | d | 40 || 5 | e | 50 |+—-+——+——+

7.1 delete 测试

1. slave_skip_errors=1032,1062 slave:

mysql> select @@slave_skip_errors, @@slave_exec_mode;
+---------------------+-------------------+
| @@slave_skip_errors | @@slave_exec_mode |
+---------------------+-------------------+
| 1032,1062           | STRICT            |
+---------------------+-------------------+
1 row in set (0.00 sec)
master(每轮测试都执行) slave
mysql> begin; mysql> delete from t_repl_test where id in (1,3,4); mysql> delete from t_repl_test where id in (5); mysql> commit; mysql> select * from t_repl_test;

在从库,1和5被删除,4被跳过了,skip_error=1032作用在statement上,并且已经部分成功了的statement 不会回滚。

2. slave_exec_mode=IDEMPOTENT 复原。不是设置skip, 设置idempotent, slave:

mysql> select @@slave_skip_errors, @@slave_exec_mode;
+---------------------+-------------------+
| @@slave_skip_errors | @@slave_exec_mode |
+---------------------+-------------------+
| OFF                 | IDEMPOTENT        |
+---------------------+-------------------+

mysql> select * from t_repl_test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | b    |   20 |
+----+------+------+

这次1, 4, 5都被删除,也就是4是一个 statement 里面某一个row_event,没有受到 id=3 error 1032的影响。

注意 如果slave同时设置 slave_skip_errors 和 slave_exec_mode,那么优先生效的是 slave_skip_errors。

7.2 insert

1. slave_skip_errors=1032,1062 slave_exec_mode=STRICT

master(每轮测试都执行)

master(每轮测试都执行) slave
mysql> begin;mysql> insert into t_repl_test values(6,’f’,60),(2,’bb’,200),(7,’g’,70);Query OK, 3 rows affected (0.00 sec)mysql> insert into t_repl_test values(8,’h’,80);Query OK, 1 row affected (0.01 sec)mysql> commit; mysql> select * from t_repl_test;+—-+——+——+| id | name | age |+—-+——+——+| 1 | a | 10 || 2 | b | 20 || 4 | d | 40 || 5 | e | 50 || 6 | f | 60 || 8 | h | 80 |+—-+——+——+

6成功,2和7失败,8成功。与delete作用范围一致。

2. slave_skip_errors=OFF slave_exec_mode=IDEMPOTENT slave:

mysql> select @@slave_skip_errors, @@slave_exec_mode;
+---------------------+-------------------+
| @@slave_skip_errors | @@slave_exec_mode |
+---------------------+-------------------+
| OFF                 | IDEMPOTENT        |
+---------------------+-------------------+

mysql> select * from t_repl_test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | a    |   10 |
|  2 | bb   |  200 |
|  4 | d    |   40 |
|  5 | e    |   50 |
|  6 | f    |   60 |
|  7 | g    |   70 |
|  8 | h    |   80 |
+----+------+------+

6, 7, 8 都插入成功,id=2的id=2被更新。所以从库在 idempotent 模式下遇到1062,是replace操作。

3. slave_skip_errors=OFF slave_exec_mode=IDEMPOTENT unique_key 再来看一个好玩的(id是主键,name是唯一索引): 从库应用relay log遇到 Duplicate entry 错误有不同处理动作。

master

mysql> select  from t_repl_test;
+—-+——+——+
| id | name | age  |
+—-+——+——+
|  1 | a    |   10 |
|  3 | c    |   30 |
|  4 | d    |   40 |
+—-+——+——+
3 rows in set (0.00 sec)

mysql> insert into t_repl_test values(9,’b’,200);
Query OK, 1 row affected (0.00 sec)

mysql> update t_repl_test set name=’e’ where id=4;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select  from t_repl_test;
+—-+——+——+
| id | name | age  |
+—-+——+——+
|  1 | a    |   10 |
|  3 | c    |   30 |
|  4 | e    |   40 |
|  9 | b    |  200 |
+—-+——+——+
4 rows in set (0.00 sec)

slave

mysql> select  from t_repl_test;
+—-+——+——+
| id | name | age  |
+—-+——+——+
|  1 | a    |   10 |
|  2 | b    |   20 |
|  4 | d    |   40 |
|  5 | e    |   50 |
+—-+——+——+
4 rows in set (0.00 sec)








mysql> select  from t_repl_test;
+—-+——+——+
| id | name | age  |
+—-+——+——+
|  1 | a    |   10 |
|  4 | d    |   40 |
|  5 | e    |   50 |
|  9 | b    |  200 |
+—-+——+——+
4 rows in set (0.00 sec)

第一条 insert 值在从库上 name=b 已经存在,违反唯一约束,所以被 replace 掉了。 第二条 update 值在从库上 name=e 已经存在,违反唯一约束,在从库 被忽略 了。看从从库的imdepotent错误日志:

2018-02-02 14:50:35 24325 [Warning] Slave SQL: Could not execute Update_rows event on table d_ec_crmlog.t_repl_test; 
Duplicate entry 'e' for key 'uk_name', Error_code: 1062; handler error HA_ERR_FOUND_DUPP_KEY; 
the event's master log mysql-bin.000015, end_log_pos 27072, Error_code: 1062

为什么会有这个行为,可以从源码里面找到答案:

Write_rows_log_event::do_before_row_operations()
  if ((slave_exec_mode == SLAVE_EXEC_MODE_IDEMPOTENT) ||
      (m_table->s->db_type()->db_type == DB_TYPE_NDBCLUSTER))
  {
    /*
      We are using REPLACE semantics and not INSERT IGNORE semantics
      when writing rows, that is: new rows replace old rows.  We need to
      inform the storage engine that it should use this behaviour.
    */
    
    /* Tell the storage engine that we are using REPLACE semantics. */
    thd->lex->duplicates= DUP_REPLACE;
    
    /*
      Pretend we're executing a REPLACE command: this is needed for
      InnoDB and NDB Cluster since they are not (properly) checking the
      lex->duplicates flag.
    */
    thd->lex->sql_command= SQLCOM_REPLACE;
    /* 
       Do not raise the error flag in case of hitting to an unique attribute
    */
    m_table->file->extra(HA_EXTRA_IGNORE_DUP_KEY);
    ...
  }