一个有趣的BUG
最近在协助团队完成ES数据的切换(业务数据迁移),过程中遇到一个比较好玩的BUG ,和大家分享并作为经验记录。
01 问题发现过程
通过前期的方案设计和比较,我们决定通过elasticdump 工具来做ES的数据迁移,这个也是比较普遍的迁移方案,于是就动手实施了,过程中也没遇到什么问题。在最后的数据验证环节,发现有一个ID对应不上了,如下图所示,通过对比工具,发现一个长度较大的ID发生了偏移,其他的数据都没有问题。这是为什么呢?一头雾水。
根据二分法的排错思路,我们需要先确认是导出数据的问题,还是导入数据的问题。查看导出过程的中间文件,发现在导出的时候就出现了错误。于是怀疑是elasticdump导出功能的问题。因为这个出错的字段,主要的特征就是长度比较长(18位),于是怀疑是精度的问题。就去查了下elasticdump的源码,一番查找后,果然发现有人遇到过同样的问题,并已经修复了这个BUG,并给出了解决方案和一些猜测的原因。于是这个问题就得到了解决。在elasticdump的导出命令中,加上--support-big-int参数,就可以了。
好像也很简单嘛,不是么。其实排错的过程也走过很多弯路,只是现在回顾起来看着比较轻松而以。
02 问题的根因是什么
只解决问题并不是我的风格,总得看看让我绕这么大圈才解决的问题根因是什么嘛。于是查了相关资料(结合上面GIT上的对话),可以确认,是因为elasticdump中有部分功能是用JS写的,而Js遵循IEEE754规范,采用双精度存储,占用64位,从左到右的安排位第一问表示符号位,11位表示指数,52位来表示尾数,因此Js中能精确表示的最大整数是2^53 (十进制 为 9007199254740992),那么大于这个数(本文中数值长度18位)就可能会丢失精度,因为二进制只有0和1,数值太大,于是就出现了精度丢失的问题。可以在Chrome Console 里面试了一下,果然是这样,(不是超过了能表示的最大值,而是超过了能精确表示的最大值),和elasticdump导出的数据变化基本类似。
再往深了想,为什么用double类型会出现这个问题,其他的数据类型是否会有同样的问题呢?这就涉及了数据精度的问题,在这里篇幅有限,就不再展开,有兴趣的同学可以自己去查看相关资料,本质上还是十进制小数与二进制小数相互转换产生的误差。
03 类似的问题有哪些
因为这个问题比较好玩,就又找了一些资料看了下,发现还有两个精度有关的BUG,还蛮好玩的。
千年虫问题:这个问题相信很多人IT人都听说过,简单来说,就是由于前期计算机的存储资源较为昂,在表达时间时,为了节约空间,有位科学家提出了一个方案,把1960年8月11日,简写成600811。但这样会有一个问题,就是当时被缩写掉的是19XX年中19,如果时间来到2000年,程序就无法准确表达时间。比如:2000年1月1日,简写成六位数是000101。计算机就会怀疑人生,怎么时间倒流了呢?然后就会导致计算机系统发生紊乱。当时大家都觉得自己的程序不会运行到2000年,所以就没太放在心上,而大多数后来人习惯了这种记录方式,就忘记了这回事,结果引发了千禧年的大BUG,造就了多少程序员的不眠之夜。
2038年问题:现在很多时候,我们在处理时间问题时,都喜欢用时间戳来记录,因为简单方便,不需要考虑时区问题(时区问题很让人头疼的,一不小出就容易出错)。但是这里面会有一个小BUG哟。什么是时间戳呢?简单来说就是:以1970年1月1日0时0分0秒为起点,然后通过计算秒数来算出当前时间。比如:2021年5月7日15:00:00,换算一下就是1620370800秒。但是由于32位操作系统所能计算的秒数有限,到2038年1月19日3:14:07,就会达到极限。二进制:01111111 11111111 11111111 11111111,其后一秒,二进制数字会变为10000000 00000000 00000000 00000000,发生溢出错误,造成系统将时间误解为1901年12月13日20时45分52秒,然后系统就会发生各类错误,是不是和上面的千年虫一样?理论上到了2038年,人们应该淘汰掉了32位操作系统, 64位操作系统就不存在这个问题。但是从前面的“千年虫”事件来看,人类从历史中吸取的唯一教训,就是人类不会吸取任何教训。
04 小结
对于发现的缺陷,不能仅停留在把问题解决了就完事。有时间和精力,还是需要更深层次的去了解缺陷背后的逻辑和根因是什么,触类旁通。以避免更多类似的问题发生。
在写这篇文章的时候,又想起了自己以前做报表相关的业务时,对于时间的精度特别敏感,也会遇到一些关于精度上的取舍问题,这需要我们和业务方面讨论并确认清楚,是精确到秒,还是毫秒,以避免出现数据边界的小问题。