增强半同步会不会丢数据

只讨论5.7增强半同步和双1的情况

增强半同步会不会丢数据?

这里涉及两个过程:

  • 主库 Innodb与Binlog日志的2PC
  • 增强半同步

Innodb与Binlog日志的2PC

image-20200920221245427

在开启Binlog后, MySQL内部会自动将普通事务当做一个XA事务来处理:

  • 自动为每个事务分配一个唯一的ID
  • COMMIT会被自动的分成Prepare和Commit两个阶段
  • Binlog会被当做事务协调者(Transaction Coordinator), Binlog Event会被当做协调者日志

    分布式事务ID(XID)

使用2PC时, MySQL会自动的为每一个事务分配一个ID, 叫XID. XID是唯一的, 每个事务的XID都不相同. XID会分别被Binlog和InnoDB记入日志中, 供恢复时使用. MySQ内部的XID由三部分组成:

  • 前缀部分
    前缀部分是字符串”MySQLXid”
  • Server ID部分
    当前MySQL的server_id
  • query_id部分
    为了保证XID的的唯一性, 数字部分使用了query_id. MySQL内部会自动的为每一个语句分配一个query_id, 全局唯一.

事务的协调者Binlog

Binlog在2PC中充当了事务的协调者(Transaction Coordinator). 由Binlog来通知InnoDB引擎来执行prepare, commit或者rollback的步骤. 事务提交的整个过程如下:

  1. 协调者准备阶段(Prepare Phase)
    告诉引擎做Prepare, InnoDB更改事务状态, 并将Redo Log刷入磁盘.

    个人理解: innodb写prepare log, 事务标记为prepare状态, 并写入xid

  2. 协调者提交阶段(Commit Phase)
    2.1 记录协调者日志, 即Binlog日志.
    2.2 告诉引擎做commit.

    个人理解: 写Binlog event和xid, 写完后通知innodb commit, innodb写commit log, 事务标记为commit状态 (记得姜成尧说要写binlog file pos到redo)

注意:记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后, 这点至关重要.

协调者日志Xid_log_event

作为协调者, Binlog需要将事务的XID记入日志, 供恢复时使用. Xid_log_event有以下几个特点:

  • 仅记录query_id
    因为前缀部分不变, server_id已经记录在Event Header中, Xid_log_event中只记录query_id部分.

  • 标志事务的结束
    在Binlog中相当于一个事务的COMMIT语句.
    一个事务在Binlog中看起来时这样的:

    1
    2
    3
    Query_log_event("BEGIN");
    DML产生的events;
    Xid_log_event;
  • DDL没有BEGIN, 也没有Xid_log_event.

  • 仅InnoDB的DML会产生Xid_log_event

    因为MyISAM不支持2PC所以不能用Xid_log_event, 但会有COMMIT Event.

    1
    2
    3
    Query_log_event("BEGIN");
    DML产生的events;
    Query_log_event("COMMIT");

恢复(Recovery)

这个机制是如何保证MySQL的CrashSafe的呢, 我们来分析一下. 这里我们假设用户设置了以下参数来保证可靠性:
sync_binlog=1
innodb_flush_log_at_trx_commit=1

恢复前事务的状态

在恢复开始前事务有以下几种状态:

  • InnoDB中已经提交
    根据前面2PC的过程, 可知Binlog中也一定记录了该事务的的Events(我感觉说的应该是Xid_log_event). 所以这种事务是一致的不需要处理.
  • InnoDB中是prepared状态, Binlog中有该事务的Events(我感觉说的应该是Xid_log_event).
    需要通知InnoDB提交这些事务.
  • InnoDB中是prepared状态, Binlog中没有该事务的Events(我感觉说的应该是Xid_log_event).
    因为Binlog还没记录, 需要通知InnoDB回滚这些事务.
  • Before InnoDB Prepare
    事务可能还没执行完, 因此InnoDB中的状态还没有prepare. 根据2PC的过程, Binlog中也没有该事务的events(我感觉说的应该是Xid_log_event). 需要通知InnoDB回滚这些事务.

恢复过程

从上面的事务状态可以看出: 恢复时事务要提交还是回滚, 是由Binlog来决定的.

  • 事务的Xid_log_event存在, 就要提交.
  • 事务的Xid_log_event不存在, 就要回滚.

恢复的过程非常简单:

  • 从Binlog中读出所有的Xid_log_event
  • 告诉InnoDB提交这些XID的事务
  • InnoDB回滚其它的事务

了解了MySQL关于Innodb与Binlog的两阶段提交机制后,就可以更深入去探究MySQL在故障恢复时的处理过程。 在MySQL启动时,首先会初始化存储引擎,如本例中的InnoDB引擎,然后InnoDB引擎层会读取redolog进行InnoDB层的故障恢复,回滚未prepared和commit的事务,但对于已经prepared,但未commit的事务,暂时挂起,保存到一个链表中,等待后续读取binlog日志,然后根据binlog日志再对这部分prepared的事务进行处理。 接下来,MySQL会读取最后一个binlog文件。binlog文件通常是以固定的文件名加一组连续的编号来命名的,并且将其记录到一个binlog索引文件中,因此索引文件中的最后一个binlog文件即是MySQL将要读取的最后一个binlog文件。 读取这个binlog文件时,通过文件头上是否存在标记LOG_EVENT_BINLOG_IN_USE_F,通过这个标记可以知道上次MySQL是正常关闭还是异常关闭,如果是异常关闭,则会进入故障恢复过程。 进入故障恢复过程后,会依次读取最后一个binlog文件中的所有log event,并将所有已提交事务的binlog日志中记录的xid提取出来添加到hash表中,以备后续对前述InnoDB故障恢复后遗留的Prepared事务继续处理。另外此处还要定位最后一个完整事务的位置,防止在上次系统异常关闭时有部分binlog日志未刷到磁盘上,即存在写了一半的binlog事务日志,这部分写了一半binlog日志的事务在MySQL中会按事务未提交来处理,后续会将其在存储引擎层回滚。当此文件中的内容全部读出之后,一是得到一个已提交事务的列表,另一个是最后一个完整事务的位置。 然后检查由InnoDB层得到的Prepared事务列表,若Prepared事务在从Binlog中得到的提交事务列表中,则在InnoDB层提交此事务,否则回滚此事务。

出自MySQL · 原理介绍 · 再议MySQL的故障恢复

疑问1:如果事务的Binlog Event只记录了一部分怎么办?
只有最后一个事务的Event会发生这样的情况。在恢复时,binlog会自动的将这个不完整的事务Events从Binlog文件中给清除掉。

疑问2:随着长时间的运行,Binlog中会积累了很多Xid_log_event,读取所有的Xid_log_event会不会效率很低?

当然很低,所以Binlog中有一个机制来保证恢复时只用读取最后一个Binlog文件中的Xid_log_event。这种机制很像一个简单的Xid_log_event的checkpoint机制。

CrashSafe的写盘次数

前面说道要想保证CrashSafe就要设置下面两个参数为1:
sync_binlog=1
innodb_flush_log_at_trx_commit=1
下面我们来看看这两个参数的作用.

  • sync_binlog
    sync_binlog是控制Binlog写盘的, 1表示每次都写. 由于Binlog使用了组提交(Group Commit)的机制, 它代表一组事务提交时必须要将Binlog文件写入硬件存储1次.
  • innodb_flush_log_at_trx_commit的写盘次数
    这个变量是用来控制InnoDB commit时写盘的方法的. 现在commit被分成了两个阶段, 到底在哪个阶段写盘, 还是两个阶段都要写盘呢?
  • Prepare阶段时需要写盘
    2PC要求在Prepare时就要将数据持久化, 只有这样, 恢复时才能提交已经记录了Xid_log_event的事务.
  • Commit阶段时不需要写盘
    如果Commit阶段不写盘, 会造成什么结果呢?已经Cmmit了的事务, 在恢复时的状态可能是Prepared. 由于恢复时, Prepared的事务可以通过Xid_log_event来提交事务, 所以在恢复后事务的状态就是正确的. 因此在Commit阶段不需要写盘.

总的来说保证MySQL服务的CrashSafe需要写两次盘. 在2PC的过程中, InnoDB只在prepare阶段时, 写一次盘. Binlog在commit阶段, 会设置一个参数告诉InnoDB不要写盘. 这个参数是thd->durability_property= HA_IGNORE_DURABILITY;代码在sql/binlog.cc的MYSQL_BIN_LOG::ordered_commit()中.

以上出自MySQL的CrashSafe和Binlog的关系

增强半同步

image-20200920204457490
对于增强半同步, 主要有两种情况:

  • 主库写binlog落盘后, Binlog dump线程发送binlog给从库, 从库IO thread接收写入relay log, 但是没有写入完, 主库挂了
  • 从库IO thread成功写入relay log后, 还没发送ACK或ACK发送了但是主库收到前, 主库挂了

第一种情况

对应用来说, 并没有收到commit ok的信息, 应当认为事务提交失败.

对于主库, 因为已经写了Binlog(写了Xid_log_event), 只是还没有写commit log, crash recover后, 这部分事务又会提交掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
假设一主一从半同步

root@localhost 19:23:00 [dbms_monitor]> show global variables like '%semi%';
+-------------------------------------------+------------+
| Variable_name | Value |
+-------------------------------------------+------------+
| rpl_semi_sync_master_enabled | OFF |
| rpl_semi_sync_master_timeout | 10000 |
| rpl_semi_sync_master_trace_level | 32 |
| rpl_semi_sync_master_wait_for_slave_count | 1 |
| rpl_semi_sync_master_wait_no_slave | ON |
| rpl_semi_sync_master_wait_point | AFTER_SYNC |
| rpl_semi_sync_slave_enabled | OFF |
| rpl_semi_sync_slave_trace_level | 32 |
+-------------------------------------------+------------+
8 rows in set (0.00 sec)

root@localhost 19:23:02 [dbms_monitor]> truncate table semi_sync;
Query OK, 0 rows affected (0.01 sec)

root@localhost 19:23:15 [dbms_monitor]> show global status like '%semi%';
+--------------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------------+-------+
| Rpl_semi_sync_master_clients | 1 |
| Rpl_semi_sync_master_net_avg_wait_time | 0 |
| Rpl_semi_sync_master_net_wait_time | 0 |
| Rpl_semi_sync_master_net_waits | 18 |
| Rpl_semi_sync_master_no_times | 1 |
| Rpl_semi_sync_master_no_tx | 2 |
| Rpl_semi_sync_master_status | ON |
| Rpl_semi_sync_master_timefunc_failures | 0 |
| Rpl_semi_sync_master_tx_avg_wait_time | 555 |
| Rpl_semi_sync_master_tx_wait_time | 5000 |
| Rpl_semi_sync_master_tx_waits | 9 |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0 |
| Rpl_semi_sync_master_wait_sessions | 0 |
| Rpl_semi_sync_master_yes_tx | 9 |
| Rpl_semi_sync_slave_status | OFF |
+--------------------------------------------+-------+
15 rows in set (0.00 sec)

root@localhost 19:23:44 [dbms_monitor]> set global rpl_semi_sync_master_timeout=999999;
Query OK, 0 rows affected (0.00 sec)

root@localhost 19:23:52 [dbms_monitor]> insert into semi_sync values(1, now());
Query OK, 1 row affected (0.00 sec)

slave:
root@localhost 19:24:10 [(none)]> select * from dbms_monitor.semi_sync;
+----+---------------------+
| id | ctime |
+----+---------------------+
| 1 | 2020-09-20 19:24:01 |
+----+---------------------+
1 row in set (0.00 sec)

可以看到当前是半同步状态, 主库rpl_semi_sync_master_timeout=999999, 写入了一条数据.

我们关闭从库, 之后开三个窗口执行三个insert

1
2
3
4
5
root@localhost 19:24:50 [dbms_monitor]> insert into semi_sync values(2, now());

root@localhost 17:41:52 [dbms_monitor]> insert into semi_sync values(3, now());

root@localhost 17:41:52 [dbms_monitor]> insert into semi_sync values(4, now());

由于rpl_semi_sync_master_wait_for_slave_count=1, 所以三个insert都在等待.

此时kill主库, 然后重启主库

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@centos-1 mysql5731]# ps -ef| grep mysqld
root 29734 29681 0 17:33 pts/5 00:00:00 tail -f /data/mysql_3358/logs/mysqld.log
root 29743 29626 0 17:33 pts/4 00:00:00 /bin/sh ./bin/mysqld_safe --defaults-file=/data/mysql_3358/my_3358.cnf --user=mysql
mysql 31479 29743 0 17:33 pts/4 00:00:32 /usr/local/mysql5731/bin/mysqld --defaults-file=/data/mysql_3358/my_3358.cnf --basedir=/usr/local/mysql5731 --datadir=/data/mysql_3358/data --plugin-dir=/usr/local/mysql5731/lib/mysql/plugin --user=mysql --log-error=/data/mysql_3358/logs/mysqld.log --open-files-limit=65535 --pid-file=/data/mysql_3358/run/mysql.pid --socket=/data/mysql_3358/run/mysql.sock --port=3358
root 32730 29626 0 19:25 pts/4 00:00:00 grep --color=auto mysqld
[root@centos-1 mysql5731]# kill -9 29743 31479


[root@centos-1 mysql5731]# ./bin/mysqld_safe --defaults-file=/data/mysql_3358/my_3358.cnf --user=mysql &
[2] 32735
[root@centos-1 mysql5731]# mysqld_safe Adding '/usr/local/mysql5731/lib/mysql/libjemalloc.so.1' to LD_PRELOAD for mysqld
2020-09-20T11:26:12.801721Z mysqld_safe Logging to '/data/mysql_3358/logs/mysqld.log'.
2020-09-20T11:26:12.824485Z mysqld_safe Starting mysqld daemon with databases from /data/mysql_3358/data

查看数据

1
2
3
4
5
6
7
8
9
10
root@localhost 19:26:44 [dbms_monitor]> select * from semi_sync;
+----+---------------------+
| id | ctime |
+----+---------------------+
| 1 | 2020-09-20 19:24:01 |
| 2 | 2020-09-20 19:25:08 |
| 3 | 2020-09-20 19:25:10 |
| 4 | 2020-09-20 19:25:11 |
+----+---------------------+
4 rows in set (0.00 sec)

可以看到, 三个insert在crash recover后成功提交了

由此可以看出, 主库宕机恢复后是不能作为从库加入原集群的, 需要重做, 否则数据不一致.

第二种情况

对应用来说, 并没有收到commit ok的信息, 应当认为事务提交失败.

但是由于这部分事务已经写入了relay log, 从库sql thread应用完relay log中所有binlog event后提升为主库, 此时应用连接到新主库, 准备重试, 那么可能会出现问题:

  • 对于INSERT:

    如果INSERT语句中没有显式指定主键或任何唯一键, 那么应用重试插入后, 会出现重复数据.

  • 对于UPDATE

    如果采用update set amount=amount-1000 where id的方式重试, 会出现重复扣款, 所以即便重试, where条件中也应当带上要更新列的原值

  • 对于DELETE

    没什么影响, 只是影响行数为0

结论

不会丢数据.

对于MHA+增强半同步, 主从切换后, 业务不能盲目重试, 而应当做事务执行失败, 按照正常逻辑重新执行完整的事务.

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 Fan() All Rights Reserved.

访客数 : | 访问量 :