MySQL半同步插件网络容错性测试

发布时间:2020-05-07 浏览次数:475

背景

在保障MySQL高可用时, 数据零丢失是某些场景比较关心的指标, 一种常用的方案是用半同步插件并将超时时间调整的比较大. 这种用法可以保障一定场景内的数据零丢失, 不过会丧失一定运维性(需要实时监控半同步插件的状况, 不能简单地通过`show slave status`获取), 也会丧失一定的架构健壮性(需要考虑备机故障时将高可用性降级, 维持业务连续性).


除了上面的特性丧失, 还有一个比较稀有的场景需要考虑, 就是网络的健壮性.

测试背景是MySQL 5.7.12.

半同步流程简述

MySQL组提交分为三个阶段:

1. Flush (将待提交事务的日志写入cache)

2. Sync (将待提交事务的日志cache刷盘)

3. Commit (将待提交事务提交, 更新存储引擎/更新GTID等)


每个阶段由一个leader线程负责, 其它非leader线程则睡眠等待. leader线程进入新的阶段后, 可以合并其他组的进入同一阶段的leader, 由新的leader带领, 继续进行.


根据配置`rpl_semi_sync_master_wait_point`, 如果是:

1. `AFTER_SYNC`, 在Sync阶段结束后, master等待slave返回的ack, 当`ack中标记的(binlog file:pos) >= 当前等待位置`时, 则可以继续. 此选项用于保障数据一致性.

2. `AFTER_COMMIT`, 在Commit阶段完成向存储引擎的提交并更新GTID后 (此时, 无论事务可见性如何, 数据已经对外可见), master等待slave返回的ack, 当`ack中标记的(binlog file:pos) >= 当前等待位置`时, 则可以继续.

master等待slave的动作, 是由组提交的leader完成, 即一组一组地等待ack.

关于网络故障后的容错行为的猜想

MySQL 5.7将master接受ack的线程独立出来, 为了不阻塞binlog的发送, 提高吞吐. 但ack包是个短包, 没有额外的checksum, 仅依靠TCP层的checksum.


在复制的数据传输中, MySQL提供了`binlog_checksum`和`slave-sql-verify-checksum`验证binlog在传输过程中的完整性; 但在ack包上并未提供应用层的校验机制.


一旦传输发生了错误, 且骗过了TCP层的校验, 那么半同步插件是否能有正确的容错行为就值得研究. 若slave发往master的ack包, 标记的位置应为A, 但master收到的位置变为B, 考虑以下场景:

1. A < B

2. A > B


测试 1

测试环境: MySQL 5.7.12, `rpl_semi_sync_master_wait_point=AFTER_SYNC`, `rpl_semi_sync_master_timeout=86400`.


使用systemtap进行这次的测试, 以下脚本将master收到的ack位置+1048576, 模拟一个网络故障时的数据变更.

probe process("/usr/local/mysql/lib/plugin/semisync_master.so").function("reportReplyPacket") {
       printf("before: %d\n", user_int($packet + 1));
       set_kernel_int($packet+1, user_int($packet + 1) + 1048576);
       printf("after: %d\n", user_int($packet + 1));
}

执行这个脚本:

huangyan@R820-09:/opt/test-semi-sync$ sudo stap -x $(ps aux | grep usr/local/mysql | grep 3306 | awk '{print $2}') -v -g test-semi-sync.stp
Pass 1: parsed user script and 106 library script(s) using 95228virt/38880res/5900shr/33576data kb, in 150usr/30sys/181real ms.
Pass 2: analyzed script: 1 probe(s), 3 function(s), 1 embed(s), 0 global(s) using 96156virt/41216res/7204shr/34504data kb, in 10usr/0sys/11real ms.
Pass 3: translated to C into "/tmp/stapu5ChBt/stap_bfba13fe9d574254774cd1c382da32d9_3068_src.c" using 96156virt/41440res/7392shr/34504data kb, in 10usr/110sys/222real ms.
Pass 4: compiled C into "stap_bfba13fe9d574254774cd1c382da32d9_3068.ko" in 2390usr/280sys/2866real ms.
Pass 5: starting run.

在master端输入数据:

mysql-master> insert into test.t values(3);
Query OK, 1 row affected (0.00 sec)

systemtap脚本的输出会多出两行:

before: 442
after: 1049018

停下slave复制后在master上写入数据, 正常情况下, master上的数据写入会等待, 直到有slave接受数据, 或者超时. 但:

mysql-slave> stop slave;
Query OK, 0 rows affected (0.00 sec)

mysql-master> insert into test.t values(3);
Query OK, 1 row affected (0.00 sec)

master端的数据写入马上完成, 即使slave端没有收到数据.


测试 2

以下脚本将master收到的ack位置-1, 模拟一个网络故障时的数据变更.

probe process("/usr/local/mysql/lib/plugin/semisync_master.so").function("reportReplyPacket") {
       printf("before: %d\n", user_int($packet + 1));
       set_kernel_int($packet+1, user_int($packet + 1) -1);
       printf("after: %d\n", user_int($packet + 1));
}

执行这个脚本后, 在master端输入数据:

...
mysql-master> insert into test.t values(3);

insert会一直等待, 且systemtap脚本的输出会多出两行:

before: 442
after: 441

如果此时停止systemtap脚本的运行, 让行为恢复正常, 再向master上写入一条数据, 则可以解开先前一直在等待的insert.


结论

若slave发往master的ack包, 标记的位置应为A, 但master收到的位置变为B, 考虑以下场景:


  1. 场景1, 当B>A时, master会认为slave已经收到了一些”未发生的事务”, 当这些事务进行时, 不会要求slave进行ack. 导致一段时间内, master和slave之间的数据不是完全同步的, 此时发生故障会丢失数据.

  2. 场景2, 当B<A时, master会认为slave还未收全当前事务, 进行等待. 但好消息是: 当有新的事务提交时, 由于发送数据的线程没有被接受ack的线程阻塞, 新的事务会要求发送ack, 只要正确的ack到了, 就可以解开事务等待的状态. 这个场景相对安全.

在此讨论的是比较极端的情况: 网络发生的数据错误能骗过TCP的校验位. 这种情况极少发生(使用半同步插件的网络环境一般都很好), 万一发生, 会导致系统的可用性受损, 而且无法被监控到.

MySQL在接受ack的处理上, 已经考虑了网络包乱序到达的情况, 对网络包错误完全交由TCP处理. 另一方面, ack包很短, 净荷一般只有19字节, 如果加上校验位成本会升高. 此处取舍由人。

上一篇: MySQL之MGR相同GTID产生不同transaction故障分析

下一篇: MySQL技术分享,大对象一例

产品试用 产品试用
400-820-6580 免费电话