searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

一个 MySQL 8.0 半同步复制异常场景

2023-04-21 02:32:09
20
0

1 复现问题

1. 部署一主三从,并开启半同步的 mysql 8.0 集群
2. 在主库执行以下设置,并显示以下状态

mysql> show global status like "Rpl_semi_sync_master_clients";
+------------------------------+-------+
| Variable_name               | Value |
+------------------------------+-------+
| Rpl_semi_sync_master_clients | 3     |
+------------------------------+-------+
1 row in set (0.01 sec)

mysql> set global rpl_semi_sync_master_timeout = 100000000;
Query OK, 0 rows affected (0.00 sec)

mysql> set global rpl_semi_sync_master_wait_for_slave_count=4;
Query OK, 0 rows affected (0.00 sec)

mysql> set global binlog_order_commits = 0;
Query OK, 0 rows affected (0.00 sec)

mysql> show global variables like "%semi%";
+-------------------------------------------+------------+
| Variable_name                             | Value     |
+-------------------------------------------+------------+
| rpl_semi_sync_master_enabled             | ON         |
| rpl_semi_sync_master_timeout             | 100000000 |
| rpl_semi_sync_master_trace_level         | 32         |
| rpl_semi_sync_master_wait_for_slave_count | 4         |
| rpl_semi_sync_master_wait_no_slave       | ON         |
| rpl_semi_sync_master_wait_point           | AFTER_SYNC |
| rpl_semi_sync_slave_enabled               | ON         |
| rpl_semi_sync_slave_trace_level           | 32         |
+-------------------------------------------+------------+
8 rows in set (0.00 sec)

3. 开三个 cli,连接 master,命名 cli-1、cli-2、cli-3

4. 每个从库,开一个 cli 连接,命名 sla-1、sla-2、sla-3,并且各自执行 stop slave
mysql> stop slave;
Query OK, 0 rows affected, 1 warning (0.12 sec)

5. 在 master cli-1 上执行插入,由于半同步设置等待 ack,将会卡主
mysql> insert into t values(1, "a");

6. 在 slave sla-3 上执行,start slave

mysql> start slave;
Query OK, 0 rows affected, 1 warning (0.22 sec)

7. 在 slave sla-1 上执行, start slave

mysql> start slave;
Query OK, 0 rows affected, 1 warning (0.24 sec)

8. 在 slave sla-2 上执行, start slave

mysql> start slave;
Query OK, 0 rows affected, 1 warning (0.19 sec)

9. 在 slave sla-1 上执行, stop slave

mysql> stop slave;
Query OK, 0 rows affected, 1 warning (0.10 sec)

10. 在 master cli-2 上执行插入,由于半同步设置等待 ack,将会卡主

mysql> insert into t values(2, "b");

11. 在 slave sla-2 上执行, stop slave

mysql> stop slave;
Query OK, 0 rows affected, 1 warning (0.09 sec)

12. 在 master cli-3 上执行插入,由于半同步设置等待 ack,将会卡主

mysql> insert into t values(3, "c");

13. 在 master cli 上设置 rpl_semi_sync_master_wait_for_slave_count

mysql> set global rpl_semi_sync_master_wait_for_slave_count=3;
Query OK, 0 rows affected (0.01 sec)

14. 第5步骤中的 master cli-1 和第10步骤中的 master cli-2 上的查询都返回了
(5 master cli-1)
mysql> insert into t values(1, "a");
Query OK, 1 row affected (1 min 18.08 sec)

(10 master cli-2)
mysql> insert into t values(2, "b");
Query OK, 1 row affected (45.08 sec)

15. 主从节点中的数据

slave 1 节点(slave sla-1) :
mysql> select * from t;
+------+------+
| id   | name |
+------+------+
|   1 | a   |
+------+------+
1 row in set (0.00 sec)

slave 2 节点(slave sla-2) :
mysql> select * from t;
+------+------+
| id   | name |
+------+------+
|   1 | a   |
|   2 | b   |
+------+------+
2 rows in set (0.00 sec)

slave 3 节点(slave sla-3) :
mysql> select * from t;
+------+------+
| id   | name |
+------+------+
|   1 | a   |
|   2 | b   |
|   3 | c   |
+------+------+
3 rows in set (0.00 sec)

也就是当前的数据(1, "a"),复制到三个节点,(2, "b") 当前只复制到两个从节点。

16. 结论

由于在 master 上,设置的 rpl_semi_sync_master_wait_for_slave_count 为 3;
但是数据(2, "b")目前只复制到两个节点,当前应该阻塞在等待 ack,不应该返回,与期望不符。

2 原因分析

出现上述问题是因为半同步插件的一个小 bug 导致的,如果不是特意的构建场景,问题还是比较难复现的,所以这也是导致这个 bug 一直没有修复的原因吧。

  /**
    Find the minimum ack which is smaller than given position. When more than
    one slots are minimum acks, it returns the one has smallest index.

    @param[in] log_file_name binlog file name
    @param[in] log_file_pos   binlog file position

    @return NULL if no ack is smaller than given position, otherwise
            return its pointer.
*/
AckInfo *minAck(const char *log_file_name, my_off_t log_file_pos) {
  unsigned int i;
  AckInfo *ackinfo = nullptr;

  for (i = 0; i < m_size; i++) {
    if (m_ack_array[i].less_than(log_file_name, log_file_pos))
      ackinfo = m_ack_array + i;
  }

  return ackinfo;
}

从代码注释上看,这里是找 “the minimum ack”,但是实际返回值,却是返回了最后一个小于(log_file_name, log_file_pos)的 ack。

2.1 半同步 ack 数量

rpl_semi_sync_master_wait_for_slave_count 代表半同步中,主库完成事务提交前,需要收到 binlog 的 ack 数量。

一个 slave 接收到事务完整的 binlog 后,向 master 发送一个 ack。

比如在 master 库上,设置:

rpl_semi_sync_master_wait_for_slave_count = 4

在 master 库上执行一条 sql:

mysql> insert into t (id) values(1);

那么 master 需要收到 4 个 slave 发送的关于这条事务的 binlog ack 才能完成整个事务提交。

2.2 ack 的保存

master 半同步插件,是将 slave 的 ack 保存在一个数组里:

array = ack[rpl_semi_sync_master_wait_for_slave_count - 1]

数组大小为 rpl_semi_sync_master_wait_for_slave_count - 1。如果数组数已满,又来了一个 ack,那么收到的 ack 数量满足 rpl_semi_sync_master_wait_for_slave_count 要求了。

假设我们 rpl_semi_sync_master_wait_for_slave_count = 4,那么 slave 依序发送 4 个 ack 过来的流程:

1. 初始状态 array = { null, null, null }
2. 收到 slave 1 的 ack-1 放进 array = { null, null, ack-1 }
3. 收到 slave 2 的 ack-2 放进 array = { null, ack-2, ack-1 }
4. 收到 slave 3 的 ack-3 放进 array = { ack-3, ack-2, ack-1 }
5. 收到 slave 4 的 ack-4,array 满了,目前的 ack 为 { ack-3, ack-2, ack-1 } + ack-4

2.3 找最小的 ack

这里就涉及到 bug 函数 minAck。

在上面收到的 4 个 ack 中,找到最小的那个,就是目前所能确认的满足的事务的 binlog ack。

比如由于从库复制速度的原因,四个 ack 分别为:

{ ack-3(log-1, 100), ack-2(log-1, 300), ack-1(log-1, 200)} + ack-4(log-1, 400)

正确的收到 4 个 ack 位置应该是 ack-3 返回的 (log-1, 100),这个才是最小的。

函数中,返回了 ack - 1(log-1, 200),这个就返回了一个错误的 min ack。

产生的影响就是 master 错误的认为 ack-1( log-1, 200) 前的事务 binlog 都完整的复制到 4 个从节点,但是实际上并没有。

2.4 ack array resize

在上面的内容已经解释了问题的原因,这部分补充内容有助于理解上面的复现 bug 场景。

当调整 rpl_semi_sync_master_wait_for_slave_count 时,代表需要的 ack 数有变化,上面介绍了 array 大小的计算方式,当 rpl_semi_sync_master_wait_for_slave_count 从 4 变为 3 时:

1. old_array 大小为 4-1 = 3
2. new_array 大小为 3-1 = 2
3. 把 old_array 的 ack 复制到 new_array 中
4. 如果 old_array ack 数量有 3 个,那么在复制到 new_array 的时候,就构成了 {ack-1, ack-2} + ack-3 满足要求的情况

也就是说在 array resize 中,触发了 minAck 的 bug 场景。

另外就是,array 插入 ack,是从右向左插入的,也就是先插入序号大的位置,再插入序号小的位置。

3 构建复现 bug 场景

复现步骤 3-8 是为了让不同的 slave 的 ack 占据目标位置,最终为 { ack2, ack-1, ack-3}。

步骤 9, 是为了 slave 1 只复制到数据 (1, "a")。

步骤 10-11,是为了 slave 2 只复制到数据 (2, "b")。

步骤 12,是为了 slave 能复制到更远的位置(3, "c")。

步骤 13,是触发 array resize,插入的过程为 {ack-1, ack-2} + ack-3。

根据 minAck 函数,其返回的最小值为 ack-2,也就是数据(2, "b") 的位置,这时候 master 把它作为可以完全提交的事务点,成功返回了,但是此时(2, "b")只复制到两个 slave。

以上步骤,bug 成功复现。

4 关闭 binlog_order_commits 的原因

无论是否关闭 binlog_order_commits,问题 bug 都是存在的,但是要从测试上直观看到问题,关闭它是关键。

binlog_order_commits 变量控制着 binlog 的 COMMIT_STAGE 阶段,如果开启,将以分组的形式进入 COMMIT_STAGE 等待 ack,并且组之间严格串行。

事务 T1(1, "a"), T2(2, "b"), T3(3, "c"),将会分为两组:{T1},{T2, T3}.

T1组进入等待 ack 结束后,虽然当前确认的 ack 被错误的设置到 T2,但是{T2,T3}将以 T3(3, "c")作为等待点,需要等待 T3 被返回才能推进,导致 T2也阻塞,问题就复现不出来。

关闭后:

binlog_order_commits = 0;

各个事务单独进入等待 ack 阶段,等待各自的事务 binlog 点,当 bug 触发后,T2 立马返回,直观上就看到了异常。

0条评论
0 / 1000
j****3
6文章数
0粉丝数
j****3
6 文章 | 0 粉丝
原创

一个 MySQL 8.0 半同步复制异常场景

2023-04-21 02:32:09
20
0

1 复现问题

1. 部署一主三从,并开启半同步的 mysql 8.0 集群
2. 在主库执行以下设置,并显示以下状态

mysql> show global status like "Rpl_semi_sync_master_clients";
+------------------------------+-------+
| Variable_name               | Value |
+------------------------------+-------+
| Rpl_semi_sync_master_clients | 3     |
+------------------------------+-------+
1 row in set (0.01 sec)

mysql> set global rpl_semi_sync_master_timeout = 100000000;
Query OK, 0 rows affected (0.00 sec)

mysql> set global rpl_semi_sync_master_wait_for_slave_count=4;
Query OK, 0 rows affected (0.00 sec)

mysql> set global binlog_order_commits = 0;
Query OK, 0 rows affected (0.00 sec)

mysql> show global variables like "%semi%";
+-------------------------------------------+------------+
| Variable_name                             | Value     |
+-------------------------------------------+------------+
| rpl_semi_sync_master_enabled             | ON         |
| rpl_semi_sync_master_timeout             | 100000000 |
| rpl_semi_sync_master_trace_level         | 32         |
| rpl_semi_sync_master_wait_for_slave_count | 4         |
| rpl_semi_sync_master_wait_no_slave       | ON         |
| rpl_semi_sync_master_wait_point           | AFTER_SYNC |
| rpl_semi_sync_slave_enabled               | ON         |
| rpl_semi_sync_slave_trace_level           | 32         |
+-------------------------------------------+------------+
8 rows in set (0.00 sec)

3. 开三个 cli,连接 master,命名 cli-1、cli-2、cli-3

4. 每个从库,开一个 cli 连接,命名 sla-1、sla-2、sla-3,并且各自执行 stop slave
mysql> stop slave;
Query OK, 0 rows affected, 1 warning (0.12 sec)

5. 在 master cli-1 上执行插入,由于半同步设置等待 ack,将会卡主
mysql> insert into t values(1, "a");

6. 在 slave sla-3 上执行,start slave

mysql> start slave;
Query OK, 0 rows affected, 1 warning (0.22 sec)

7. 在 slave sla-1 上执行, start slave

mysql> start slave;
Query OK, 0 rows affected, 1 warning (0.24 sec)

8. 在 slave sla-2 上执行, start slave

mysql> start slave;
Query OK, 0 rows affected, 1 warning (0.19 sec)

9. 在 slave sla-1 上执行, stop slave

mysql> stop slave;
Query OK, 0 rows affected, 1 warning (0.10 sec)

10. 在 master cli-2 上执行插入,由于半同步设置等待 ack,将会卡主

mysql> insert into t values(2, "b");

11. 在 slave sla-2 上执行, stop slave

mysql> stop slave;
Query OK, 0 rows affected, 1 warning (0.09 sec)

12. 在 master cli-3 上执行插入,由于半同步设置等待 ack,将会卡主

mysql> insert into t values(3, "c");

13. 在 master cli 上设置 rpl_semi_sync_master_wait_for_slave_count

mysql> set global rpl_semi_sync_master_wait_for_slave_count=3;
Query OK, 0 rows affected (0.01 sec)

14. 第5步骤中的 master cli-1 和第10步骤中的 master cli-2 上的查询都返回了
(5 master cli-1)
mysql> insert into t values(1, "a");
Query OK, 1 row affected (1 min 18.08 sec)

(10 master cli-2)
mysql> insert into t values(2, "b");
Query OK, 1 row affected (45.08 sec)

15. 主从节点中的数据

slave 1 节点(slave sla-1) :
mysql> select * from t;
+------+------+
| id   | name |
+------+------+
|   1 | a   |
+------+------+
1 row in set (0.00 sec)

slave 2 节点(slave sla-2) :
mysql> select * from t;
+------+------+
| id   | name |
+------+------+
|   1 | a   |
|   2 | b   |
+------+------+
2 rows in set (0.00 sec)

slave 3 节点(slave sla-3) :
mysql> select * from t;
+------+------+
| id   | name |
+------+------+
|   1 | a   |
|   2 | b   |
|   3 | c   |
+------+------+
3 rows in set (0.00 sec)

也就是当前的数据(1, "a"),复制到三个节点,(2, "b") 当前只复制到两个从节点。

16. 结论

由于在 master 上,设置的 rpl_semi_sync_master_wait_for_slave_count 为 3;
但是数据(2, "b")目前只复制到两个节点,当前应该阻塞在等待 ack,不应该返回,与期望不符。

2 原因分析

出现上述问题是因为半同步插件的一个小 bug 导致的,如果不是特意的构建场景,问题还是比较难复现的,所以这也是导致这个 bug 一直没有修复的原因吧。

  /**
    Find the minimum ack which is smaller than given position. When more than
    one slots are minimum acks, it returns the one has smallest index.

    @param[in] log_file_name binlog file name
    @param[in] log_file_pos   binlog file position

    @return NULL if no ack is smaller than given position, otherwise
            return its pointer.
*/
AckInfo *minAck(const char *log_file_name, my_off_t log_file_pos) {
  unsigned int i;
  AckInfo *ackinfo = nullptr;

  for (i = 0; i < m_size; i++) {
    if (m_ack_array[i].less_than(log_file_name, log_file_pos))
      ackinfo = m_ack_array + i;
  }

  return ackinfo;
}

从代码注释上看,这里是找 “the minimum ack”,但是实际返回值,却是返回了最后一个小于(log_file_name, log_file_pos)的 ack。

2.1 半同步 ack 数量

rpl_semi_sync_master_wait_for_slave_count 代表半同步中,主库完成事务提交前,需要收到 binlog 的 ack 数量。

一个 slave 接收到事务完整的 binlog 后,向 master 发送一个 ack。

比如在 master 库上,设置:

rpl_semi_sync_master_wait_for_slave_count = 4

在 master 库上执行一条 sql:

mysql> insert into t (id) values(1);

那么 master 需要收到 4 个 slave 发送的关于这条事务的 binlog ack 才能完成整个事务提交。

2.2 ack 的保存

master 半同步插件,是将 slave 的 ack 保存在一个数组里:

array = ack[rpl_semi_sync_master_wait_for_slave_count - 1]

数组大小为 rpl_semi_sync_master_wait_for_slave_count - 1。如果数组数已满,又来了一个 ack,那么收到的 ack 数量满足 rpl_semi_sync_master_wait_for_slave_count 要求了。

假设我们 rpl_semi_sync_master_wait_for_slave_count = 4,那么 slave 依序发送 4 个 ack 过来的流程:

1. 初始状态 array = { null, null, null }
2. 收到 slave 1 的 ack-1 放进 array = { null, null, ack-1 }
3. 收到 slave 2 的 ack-2 放进 array = { null, ack-2, ack-1 }
4. 收到 slave 3 的 ack-3 放进 array = { ack-3, ack-2, ack-1 }
5. 收到 slave 4 的 ack-4,array 满了,目前的 ack 为 { ack-3, ack-2, ack-1 } + ack-4

2.3 找最小的 ack

这里就涉及到 bug 函数 minAck。

在上面收到的 4 个 ack 中,找到最小的那个,就是目前所能确认的满足的事务的 binlog ack。

比如由于从库复制速度的原因,四个 ack 分别为:

{ ack-3(log-1, 100), ack-2(log-1, 300), ack-1(log-1, 200)} + ack-4(log-1, 400)

正确的收到 4 个 ack 位置应该是 ack-3 返回的 (log-1, 100),这个才是最小的。

函数中,返回了 ack - 1(log-1, 200),这个就返回了一个错误的 min ack。

产生的影响就是 master 错误的认为 ack-1( log-1, 200) 前的事务 binlog 都完整的复制到 4 个从节点,但是实际上并没有。

2.4 ack array resize

在上面的内容已经解释了问题的原因,这部分补充内容有助于理解上面的复现 bug 场景。

当调整 rpl_semi_sync_master_wait_for_slave_count 时,代表需要的 ack 数有变化,上面介绍了 array 大小的计算方式,当 rpl_semi_sync_master_wait_for_slave_count 从 4 变为 3 时:

1. old_array 大小为 4-1 = 3
2. new_array 大小为 3-1 = 2
3. 把 old_array 的 ack 复制到 new_array 中
4. 如果 old_array ack 数量有 3 个,那么在复制到 new_array 的时候,就构成了 {ack-1, ack-2} + ack-3 满足要求的情况

也就是说在 array resize 中,触发了 minAck 的 bug 场景。

另外就是,array 插入 ack,是从右向左插入的,也就是先插入序号大的位置,再插入序号小的位置。

3 构建复现 bug 场景

复现步骤 3-8 是为了让不同的 slave 的 ack 占据目标位置,最终为 { ack2, ack-1, ack-3}。

步骤 9, 是为了 slave 1 只复制到数据 (1, "a")。

步骤 10-11,是为了 slave 2 只复制到数据 (2, "b")。

步骤 12,是为了 slave 能复制到更远的位置(3, "c")。

步骤 13,是触发 array resize,插入的过程为 {ack-1, ack-2} + ack-3。

根据 minAck 函数,其返回的最小值为 ack-2,也就是数据(2, "b") 的位置,这时候 master 把它作为可以完全提交的事务点,成功返回了,但是此时(2, "b")只复制到两个 slave。

以上步骤,bug 成功复现。

4 关闭 binlog_order_commits 的原因

无论是否关闭 binlog_order_commits,问题 bug 都是存在的,但是要从测试上直观看到问题,关闭它是关键。

binlog_order_commits 变量控制着 binlog 的 COMMIT_STAGE 阶段,如果开启,将以分组的形式进入 COMMIT_STAGE 等待 ack,并且组之间严格串行。

事务 T1(1, "a"), T2(2, "b"), T3(3, "c"),将会分为两组:{T1},{T2, T3}.

T1组进入等待 ack 结束后,虽然当前确认的 ack 被错误的设置到 T2,但是{T2,T3}将以 T3(3, "c")作为等待点,需要等待 T3 被返回才能推进,导致 T2也阻塞,问题就复现不出来。

关闭后:

binlog_order_commits = 0;

各个事务单独进入等待 ack 阶段,等待各自的事务 binlog 点,当 bug 触发后,T2 立马返回,直观上就看到了异常。

文章来自个人专栏
数据库分享
6 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0