Chronograf档案:JavaScript的`sort`的奇异案例

导航到

“世界上充满了显而易见的事情,但没有人有机会观察。” —— 亚瑟·柯南·道尔爵士,《巴斯克维尔的猎犬》*

这是你在InfluxData办公室的平常一天。空气中充满了熟悉的声音:乒乓球的声音,总是忙碌的咖啡壶滴答声。这一天就像任何其他一天一样——至少我是这样想的。

一切始于一个新Chronograf工单,用户报告他们的图表表现异常。

错误

我给你“证据A”,这是我们受影响的用户提交的屏幕截图

chrono-out-of-order

虽然这个图表看起来很混乱,但问题一目了然:Chronograf没有按顺序绘制这个用户的数据。对于像Chronograf这样的产品来说,这个问题几乎可以算是最关键的。因为我们从事的是时序数据业务,我们最好确保我们尊重物理定律,时间继续像从时间开始以来那样运行。

此时,Chronograf已经经历了几次重大发布,一年多的时间。我们之前怎么会没见过这种行为呢?

用户还向我们提供了生成上述图表的样本数据集,此处命名为“附件B”。数据集本身很大,因此我不会全部展示,但以下是我们用于调查目的的小样本:时间戳 值 ----------------------- ------ 100000000 4 200000000 9 300000000 6 400000000 7 500000000 6 600000000 4 700000000 9 800000000 7 900000000 10 1000000000 6 1100000000 4 ...

注意:InfluxDB支持多种时间格式,尽管在这种情况下我们使用的是Unix时间。不想剧透太多,但这对我们来说将非常相关!

乍一看,数据似乎合理。数值看起来正常,时间戳是按时间顺序排列的。没有引起警觉,对吧?

修复方案

“等等,你说sort做了什么?” —— 匿名

现在我有了附件A,一个图表,它解释了该错误如何影响最终用户。我还有了附件B,一个样本数据集,我用它准确重现了问题。

下一步是问自己几个问题,这些问题在我开始调试过程时很有帮助

1) 这个错误影响了代码的哪些部分?最有可能的罪魁祸首是谁?

我立刻想到了一段代码,这段代码将使用JSON格式的原始InfluxDB响应转换为我们的图表库可以理解的数据结构。据我所知,样本数据集是正确排序的。问题一定在后面。

2) 是否有任何明显的异常?

就按时间顺序绘图而言,Chronograf在所有其他已知情况下似乎都运行正常。鉴于我们之前看到的图表,这也是一种你可能会注意到的问题。

这里开始变得有趣:测试数据值得注意,因为第一个点始于Unix认为宇宙黎明的那一天。Unix时间戳表示从1970年1月1日00:00:00 UTC以来经过的时间(通常是秒或毫秒)。

例如,我完成这句话时的“Unix时间”是1458851975430。单从这一点来看,这可能不是一个特别令人兴奋或有趣的细节。这是测试数据,我们为什么要关心我们是从1970年还是2016年绘图呢?实际上,我们确实要关心!

我对数据进行了仔细检查,这是在它进入我们的图表库之前,在转换过程中的一个关键点。我确认它已经错位了。使用上一节中的同一小部分数据,这是我看到的测试数据的视图

时间戳 值 ----------------------- ------ 100000000 4 1000000000 6 1100000000 4 200000000 9 300000000 6 400000000 7 500000000 6 600000000 4 700000000 9 800000000 7 900000000 10 ...

等等,为什么1000000000(9个零)出现在200000000(8个零)之前呢?这个新的洞察力足以让我找到罪犯,一个看起来像这样的行

// allTimestamps 是一个包含唯一时间戳的列表,// 例如 [100000000, 200000000, 300000000 ...] allTimestamps.sort()

我记得为什么最初添加这一行:如果数据以任何原因进入转换器时顺序错误,或者需要合并多个系列,我们希望在尝试在图表上绘图之前确保我们的数据是按时间顺序排列的。

突然之间,一切都有了意义。让我们试试看...

如果你有一个带键盘的设备,打开浏览器的开发者控制台。在Chrome中,你可以选择“查看”->“开发者”->“JavaScript控制台”,然后输入以下内容并按回车:[2, 10, 5].sort()

这个结果让你感到惊讶吗? 我想应该是的,因为这就是你本应看到的输出

[2, 10, 5].sort() // => [10, 2, 5]

归结起来就是:JavaScript 内置的 sort 函数默认情况下按“字典顺序”对项目进行排序,即按字母顺序。 根据 JavaScript,'1' 在字母表中排在 '2' 之前。从这个角度来看,我们刚才看到的输出是有道理的。 这是我们在对数字列表进行排序时预期的默认行为吗? 绝对不是。

修复本身很简单。 我们不是使用不带任何参数的 sort,而是提供了 Mozilla 所称的 compareFunction

如果你在浏览器控制台中运行了前面的示例,可以尝试这个

[2, 10, 5].sort((a, b) => a - b)

好多了,对吧?

好的,现在我们知道了 sort 的工作原理。 我们知道 什么 要修复,但我们还没有完全回答我们最初的问题:为什么我们得等这么久才出现这样一个 bug? 答案在于每个时间戳的有效数字位数。 在提供的测试数据中,我们遇到了多次溢出,时间戳不仅在数值上增大,而且增大到需要另一个有效数字来表示它们。

我们可以通过使用测试数据来说明这一点,而且只需要几个点

时间戳值 --------- ------ 800000000 4 900000000 6 1000000000 4

尝试不使用 compareFunction 对这些时间戳进行排序,看看结果

[800000000, 900000000, 1000000000].sort() // => [1000000000, 800000000, 900000000]

1000000000 出现在列表的开头,这在技术上来说是正确的。 但是由于它有一个额外的数字,这导致了非按时间排序的数据。 Chronograf 中的大部分数据都是近期的,通常是来自过去几年。 从 Unix 时间(1970 年开始)到 Unix 时间戳需要另一个数字来表示的时间点已经过去很长时间了。 如果我们使用毫秒,那就是 2286 年 11 月 21 日。

在正常的 Chronograf 使用过程中,如果 InfluxDB 返回 Unix 时间戳,几乎在所有情况下它们的长度都是相同的。 JavaScript 在尝试对这些数字进行排序时将它们视为字符串的事实并不重要——无论我们是否告诉 JavaScript 明确按数字排序,我们都会得到相同的结果。

最后在控制台中做一个实验,结果应该是相同的: // 使用三个真实的 InfluxDB 时间戳:[1459444040000, 1459444050000, 1459444060000].sort() [1459444040000, 1459444050000, 1459444060000].sort((a, b) => a - b)

Chronograf 几乎在所有情况下都在处理这样的时间戳,这就是为什么我们需要一个独特的数据集来追踪这个特定的 bug。

哇!又一个案例。

教训

“历史不是记忆的负担,而是灵魂的启迪。” —— 阿克顿勋爵

作为一个开发者,这次经历真是太棒了。 这是一个有趣、独特、轻微恼人的 bug,具有丰富的教学潜力。 以下是我的主要收获

  • 对工具的深入理解将使你成为更好的 *插入事物*。 这个想法跨越所有领域和学科,尽管它在软件中尤为重要。 这个 `sort` bug 能够咬我们,是因为我们在知识上的一个空白,它激励我们去学习更多关于 JavaScript(我们最有价值的工具)的工作方式。 它将帮助我们做出未来的决策,甚至可能预防一些 bug。
  • 关键错误潜伏在表面之下,通常在最奇怪的情况下才会表现出来。

为了最终看到 sort 的行为与我们的预期不同,我们需要一个使用了特定时间格式并跨越了两个非常具体的年份(1970-1972)的数据集。我发现软件总是以其无数种方式崩溃而令人惊叹。在这方面,我对它的原始和不受约束的创造力抱有一种勉强尊重。

如果您想深入了解 sort 的内部工作原理,这篇文章来自javascriptkit.com,非常有价值:http://www.javascriptkit.com/javatutors/arraysort.shtml

接下来是什么?从 Chronograf 开始吧!

现在它能够熟练地排序数值时间戳!

如果您想尝试 Chronograf,它是 TICK 堆栈中的‘C’,您可以在此处下载最新版本:https://w2.influxdata.com/downloads/。我们喜欢阅读并感谢任何反馈,所以请告诉我们您的想法。无论是错误报告、功能请求、整体体验改进,还是关于产品的任何其他想法或想法,请不要犹豫,通过电子邮件联系团队:[email protected]