{{ it.name }}
{{ it.text }}
背景
在保障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, 当B>A时, master会认为slave已经收到了一些”未发生的事务”, 当这些事务进行时, 不会要求slave进行ack. 导致一段时间内, master和slave之间的数据不是完全同步的, 此时发生故障会丢失数据.
场景2, 当B<A时, master会认为slave还未收全当前事务, 进行等待. 但好消息是: 当有新的事务提交时, 由于发送数据的线程没有被接受ack的线程阻塞, 新的事务会要求发送ack, 只要正确的ack到了, 就可以解开事务等待的状态. 这个场景相对安全.
在此讨论的是比较极端的情况: 网络发生的数据错误能骗过TCP的校验位. 这种情况极少发生(使用半同步插件的网络环境一般都很好), 万一发生, 会导致系统的可用性受损, 而且无法被监控到.
MySQL在接受ack的处理上, 已经考虑了网络包乱序到达的情况, 对网络包错误完全交由TCP处理. 另一方面, ack包很短, 净荷一般只有19字节, 如果加上校验位成本会升高. 此处取舍由人。