Photo @ Akshar Dave
文 | 故知
背景——讲个真实的“鬼”故事
菜鸟 CTO 线研发效能团队开发了一个大促协同平台,来提高大家在处理大促相关工作时的协同效率,内部应用名称为 iwork 。某一天 iwork 应用突然发生了一个线上问题:
线上的一张核心数据表,所有的数据突然全都不见了。
删库跑路,是程序员的终极技能,新闻上也数次见过报道。小伙伴们虽然第一次在现实中遇到这种紧急情况,但是大家镇定地处理了这个问题。
首先在 DBA 的帮助下,导出 binlog 日志, 一起临时恢复了数据,对问题进行了止血。紧接着是要定位造成问题的原因。
第一反应是生产环境的代码执行了数据删除逻辑,但是近一段时间小伙伴们都在埋头开发新功能,生产环境有一段时间没有发布过了,如果生产环境代码有问题的话,早该暴露出来了。查看了一下 master 分支上面的代码,只有一个 delete 方法会删除对应表的数据,但是这个方法并没有在任何地方被调用。
如果不是生产环境代码的问题,还有什么可能性会导致数据删除呢?我们想到预发环境也有可能,因为预发和正式共用同一个数据库。这个时候一位小伙伴提供了一条关键信息,在预发环境的数据库操作日志中,找到了 delete 操作的执行记录,看来是预发环境的代码删除了数据。
本以为事情到这里已经快水落石出了,但是当我们去查看预发环境代码的时候,发现代码中也没有任何地方会对数据表进行删除,更加诡异的是,甚至 master 分支上的那个 delete 方法,在预发代码的分支上,已经被注释掉了。为什么要注释掉这段代码呢?小伙伴在日常开发的时候,遇到了数据偶有丢失的问题,但是由于日常环境经常不稳定,并且问题也没有复现,代码中也找不到调用该 delete 方法的地方,为了保险起见,就把整个 delete 方法注释掉了。
问题排查到这里陷入了困境,所有的线索都指向这个推论:
被注释掉的delete方法,不甘心自己的生命被终结,它决定向程序员复仇。在某个时间,它复活并运行了自己,把对应的数据表中的所有数据,所有数据都给删掉,然后跑路。
时值盛夏,突然觉得公司的空调开得有点凉。
破案——幽灵是如何产生的
没有办法根据代码顺藤摸瓜,我们只能转头去寻找更多的线索作为输入。内部鹰眼( eagleeye )系统提供了强大的链路查询功能,让我们可以查出数据删除操作相关的整个链路调用情况。
基于预发环境的数据库操作日志,我们查询了这段时间附近的调用链路,最终找到了问题的根本原因:最近新开发的一个数据同步功能,其代码引入了一个 Bug ,代码如下:
代码没太多可说的, query 对象没有处理查询字段为空的情况,导致 list 方法返回了全表对象,在后续的循环中调用了 delete 方法,删除了整张表的数据。
至此根本原因已经找到了,但是案子只破了一半,我们还没有回答幽灵代码是如何产生的。以上这段代码,是在一个分支 B 当中,但是当前预发环境中运行的是分支 A ,而分支 A ,如前文所述,是不包含这段问题代码的,甚至 delete 方法都是注释掉的。
其实产生幽灵的元凶,是当前 iwork 应用采用的分支模式:
由于 iwork 日常环境不稳定/不可用,大家常常用预发来做测试。预发上面部署的分支,其功能通常还没有开发完成,不能直接集成,于是为了测试自己的分支,常常需要把其它人的分支踢掉(利用 Aone 提供的退出预发功能),部署上自己的分支。越是临近项目发布阶段紧张关头,这种踢来踢去的情况就越普遍。
幽灵正是这样产生的,问题代码所在的分支 B ,之前在预发环境部署运行过,现在被分支 A 踢掉了。分支 A 上运行着健康的代码,让我们误以为幽灵的存在。<