相信对于很多 DBA 同学来说,由 Github 网站开源的在线 DDL 工具 gh-ost ,相信不会陌生。
对比另一款在线 DDL 工具 pt-osc,gh-ost 的开销更小。gh-ost 通过二进制日志(binlog)记录 DDL 变更过程中的修改,而 pt-osc 通过触发器记录修改变化。
显然,触发器开销更大,且 5.7 版本之前一张表只能有一个类型的触发器,因此使用 pt-osc 的限制也更多些。
因此,使用工具 gh-ost 来进行在线表结构变更操作几乎可以认为是目前 MySQL 的一种标准变更规范。
然而,gh-ost 翻车了!在我们的生产环境中,使用 gh-ost 进行表结构变更后,导致了1条数据的丢失!
在金融业务中,数据丢失是绝对不容许发生的场景
我们的工程师同学经过周末连番的源码研究与测试,最终定位这的确是 gh-ost 代码 bug。
这意味着之前所有通过 gh-ost 进行 MySQL 数据库变更的操作,都有可能触发数据丢失。
复盘
接着,我给同学们复盘下使用 gh-ost 导致数据丢失的原因。
gh-ost 的基本实现原理如下图所示,具体实现不在本文章赘述,感兴趣的可移步官网:https://github.com/github/gh-ost
从源码实现的角度看,gh-ost 经历以下几个关键函数步骤:
1. addDMLEventsListener:添加对于二进制日志的过滤采集(指定表的二进制日志过滤)
2. ReadMigrationRangeValues:获取对应表唯一索引的max、min值
3. onBeforeRowCopy:将捕获的二进制日志应用到表 *_gho
4. iterateChunks:根据 min、max 值,批量插入数据到表 *_gho
5. rename & drop 新旧表
但是,存在一种可能性,addDMLEventListner后,对应的二进制日志“丢失”了!
这种情况发生的原因可能是因为 MySQL 数据库启用了 after_sync 模式的半同步复制(二阶段提交)。
怎么理解呢?可以通过下面的时序图来看:
从上图可以看到,在某些场景下,可能发生 gh-ost 开始捕获 DML 操作后的二进制日志,但是之前的二进制事务并没有提交!
在上图的案例中,步骤1 addDMLEventsListener 将会捕获记录5以后发生的日志。
然而,在步骤2 ReadMigrationRangeValues 中,获取 min、max的值将会是1、4。
这是因为由于 after_sync 半同步模式,记录5对应的事务还未提交(如网络原因,或从机宕机等场景),记录5对于 gh-ost 中的函数 ReadMigrationRangeValues 是不可见的。
因此,步骤3、4只会插入记录1-4,以及回放记录5之后的所有日志,但会丢失记录5。
既然知道了原因,那么修复就变得非常简单了。只需要在获取 min、max的边界值的时候通过一致性读取即可
SELECT MIN(UK),MAX(UK) FROM xxxLOCK IN SHARE MODE;
通过 LOCK IN SHARE MODE,即便发生上述 after_sync 半同步等待问题,则在函数 ReadMigrationRangeValues 执行过程中,需要等待上述事务提交才能完成边界值的获取。
这时,边界值就会变为1、5,从而不会导致数据的丢失。
总结
从上述复盘看,gh-ost 数据丢失的可能性是比较大的,而且并不只是一条记录的丢失。
理论上可以是最后一组提交事务的数量,且每个事务可能影响的记录也不止一条。
反观工具 pt-osc,其通过触发器捕获增量日志,因此不存在该问题。
另一方面,从这个案例中可以看到一致性共享读取的使用场景。
FOR UPDATE 的一致性排他读取大家都了解,但 LOCK IN SHARE MODE 何时使用呢?这个场景给了你很好的答案。
最后,在金融场景不能仅仅相信数据库的一致性检查。
在上述场景下,主从数据核对检查依然是一致的,没有数据丢失。
所以,金融场景一定还要有业务层的数据核对,通过逻辑核对,确保数据库中的数据是没有任何物理丢失。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.