* 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
最近,IMG 的姜老师发布了一篇关于使用 gh-ost 会丢数据的文章(gh-ost 翻车!使用后导致数据丢失!),大致结论就是:在 MySQL AFTER_SYNC的 场景下,使用 gh-ost 进行表结构变更(包括最新 GA 的1.1.2版本在内),可能会导致数据丢失,还引起大家在微信群内展开了一些讨论。得知这个消息,还是觉得有些意外的,毕竟对于大部分 DBA 来说,gh-ost 属于比较常用的 DDL 工具,会用其替代 pt-osc 或 MySQL 自带的 online ddl 。出于好奇,去 gh-ost 的 Gtihub 主页上看了下,还真有相关的 issue ,并且已经有人提交了 fix 的 PR (目前该 fix 尚未得到官方回应)
根据文中提到的复现方法,我们来测试下,看看数据是不是真的会丢,会丢失到什么程度呢
git clone https://github.com/github/gh-ost.git
安装很简单,官方提供了编译脚本,只需执行 master 目录下的 build.sh 即可,执行后会自动在 /tmp 下生成 gh-ost-release 目录,包括可执行二进制文件、二进制 tar 包、以及一个 gh-ostxxxxxx 的文件夹
确认半同步状态正常
确认已配置AFTER_SYNC模式的半同步
1、修改 gh-ost 源码文件./gh-ost-master/go/logic/migarator.go ,在 addDMLEventsListener 函数前增加一个等待时间,此处修改为 sleep 60s
2、在主库上配置半同步超时参数(注意,此处设置为一个大于 60s 的值,比如120000,即120s),为什么要设置大于 60s 呢?后面会解释
3、创建测试表并插入 1 条数据
zlm@10.186.60.62 [zlm]> show create table t\G
*************************** 1. row ***************************
Table: t
Create Table: CREATE TABLE `t` (
`id` int(11) NOT NULL,
`name` varchar(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
zlm@10.186.60.62 [zlm]> insert into t(id,name) values(1,'a');
Query OK, 1 row affected (0.05 sec)
zlm@10.186.60.62 [zlm]> select * from t;
+----+------+
| id | name |
+----+------+
| 1 | a |
+----+------+
1 row in set (0.00 sec)
4、运行 gh-ost ,对测试表执行 DDL ,这里选择一个最简单的“alter table t engine=innodb;”进行测试
/tmp/gh-ost-release# ./gh-ost --execute --initially-drop-ghost-table --ok-to-drop-table --debug --allow-on-master --host=127.0.0.1 --port=3332 --user=zlm --password
命令执行后,会创建_t_gho、_t_ghc的中间表,随后进入 Listening 状态
用 watch 命令可以实时观察这 2 张表的变化
5、将从库上的 IO 线程停止(模拟半同步从库与主库通信异常)
zlm@10.186.60.68 [zlm]> stop slave io_thread;
6、主库执行 insert 语句(插入一条包含更大主键值的记录,如:id=2)
SQL 执行后会处于一直等待的状态,就像在等待事务锁释放一样
下一秒,事务提交完毕后,对表的 DDL 变更也完成了,将原 t 表 rename 为_t_del表,将_t_gho表 rename 为新t表
由于 gh-ost 命令配置了 --ok-to-drop-table ,DDL 完成后会将_t_del直接删除(生产环境建议保留),最终仅剩下变更后的新 t 表
可以看到,这个事务最后是提交完成的,执行了1分56秒多,大致等于之前配置的半同步超时时间 120s ,但在完成 DDL 变更后,发现t表中并没有 id=2 的记录,即数据已经丢失了,测试结果符合预期
之前在源码中配置了 sleep 60s ,从打印的日志中可以看到,在 gh-ost 命令执行1分钟后,即 16:37:23 开始进行了数据迁移(将原表数据写入影子表_t_gho),在获取原表数据时的取值范围是[1]..[1],id=2的新值(最大值)没有被获取到,为什么呢?
主库上配置了 autocommit=1 ,当事务语句执行后会自动进入提交流程(two-phase commit),解析 binlog 可以观察到事务写入 binlog 的具体时间,状态是 COMMIT 的,且包含 Xid ,而此时的 redo log 中,该事务仍然没有被“封口”,不能返回 ok 给客户端
由于主库配置的半同步超时时间大于gh-ost获取原表首尾记录临界值的开始时间(120s > 60s),gh-ost在读取Range时,主库事务还未真正提交(after_sync增强半同步要求至少有一个从库返回ACK后,才能进行引擎层提交,这是保证MySQL 5.7 Lossless semi-sync replicaiton的基础),gh-ost无法读取到这个新插入id=2的事务,最终导致这条记录丢失,如果这个新插入的事务包含N条记录,那么这N条记录都会一起丢失。
修改./gh-ost-master/go/sql/builder.go 文件
修改./gh-ost-master/go/logic/migrator.go 文件
重新编译 gh-ost 后再次验证,同样是采用之前的配置,事务执行了1分53秒多,这次数据并没有丢失,看来修复方案有效
再来看下 gh-ost 打印的日志,在调整 Range 值得获取方式后,即使是 after_sync 模式下,也能使 Range 读取到正确的值,确保最终的数据一致性
文章推荐:
技术分享 | 如何优雅地在 Windows 上从 MySQL 5.6 升级到 5.7
社区近期动态