Chronograf 文件:JavaScript `sort` 的奇特案例

导航至

“世界充满了显而易见的事物,但没有人碰巧注意到。” — 阿瑟·柯南·道尔爵士,《巴斯克维尔的猎犬》*

这是 InfluxData 办公室里平凡的一天。空气中充满了熟悉的声音:乒乓球的撞击声,以及超负荷运转的咖啡壶滴水声。和往常一样的一天——至少我是这么认为的。

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

错误

我向您展示“证物 A”,这是我们受影响用户提交的屏幕截图

chrono-out-of-order

虽然此图表在视觉上相当混乱,但问题立即显而易见:Chronograf 正在按时间顺序绘制此用户的数据。对于像 Chronograf 这样的产品,这几乎是错误可能达到的最严重程度。因为我们从事时间序列数据业务,所以我们最好确保我们尊重物理定律,并且时间继续像从时间开始之初那样运行,嗯,时间

此时,Chronograf 在一年多的时间里已经发布了几个版本。我们以前怎么没见过这样的行为?

用户还向我们提供了导致上述图表的示例数据集,在此称为“证物 B”。数据集本身很大,因此我不会完整地重现它,但这是一个小样本,我们将用于调查目的: timestamp value --------- ------ 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) 此错误触及代码的哪些部分?谁是最有可能的肇事者?

我立即想到一段代码,该代码将原始 InfluxDB 响应(使用 JSON 格式)转换为我们的绘图库可以理解的数据结构。据我所知,示例数据集已正确排序。一定是更下游的东西。

2) 是否有明显的异常?

就按时间顺序绘制数据而言,Chronograf 在所有其他已知情况下似乎都能正常运行。考虑到我们之前看到的图表,这也是那种您感觉您会注意到的错误。

精彩之处在于:测试数据值得注意,因为第一个点始于 Unix 认为的宇宙曙光的第一天。Unix 时间戳表示自 1970 年 1 月 1 日 UTC 时间 00:00:00 以来经过的时间量(通常以秒或毫秒为单位)。

例如,我完成这句话时的“Unix 时间”是 1458851975430。就其本身而言,这可能不是一个特别令人兴奋或有趣的细节。这是测试数据,谁在乎我们绘制的是 1970 年还是 2016 年的点?事实证明,我们在乎

我仔细查看了经过转换过程后的数据,就在它进入我们的绘图库之前。我确认它已乱序。使用上一节中的相同小数据子集,这是测试数据在我看来显示的样子

timestamp value --------- ------ 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()

我记得最初添加此行的原因:如果出于任何原因,数据以无序方式进入转换器,或者需要合并多个 series,我们希望确保我们的数据在尝试在图表上绘制之前是按时间顺序排列的。

突然一切都变得有意义了。让我们尝试一些东西……

如果您使用的是带有键盘的设备,请打开浏览器的开发者控制台。在 Chrome 上,您可以选择“查看”->“开发者”->“JavaScript 控制台”,然后键入以下内容并按 Enter 键:[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 是如何工作的了。我们知道修复什么,但我们仍然没有完全回答我们最初的问题:为什么我们必须等待这么久才出现这样的错误?答案在于每个时间戳的有效数字位数。使用提供的测试数据,我们有多次翻转,其中时间戳不仅在数值大小上增长,而且增长到足以需要另一个有效数字来表示它们。

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

timestamp value --------- ------ 800000000 4 900000000 6 1000000000 4

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

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

1000000000 出现在列表的开头,这在技术上根据 JavaScript 是正确的。但是由于它有一个额外的数字,因此会导致非时间顺序的数据。输入 Chronograf 的大多数数据都是最近的数据,通常来自过去几年。自 1970 年(Unix 时间的开始)以来,已经过了足够的时间,以至于 Unix 时间戳在需要另一个数字来表示之前还需要很长时间。如果我们使用毫秒,实际上是 2286 年 11 月 21 日。

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

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

Chronograf 在几乎所有情况下都在处理这样的时间戳,这解释了为什么我们需要一个独特的数据集才能追踪到这个特定的错误。

瞧!又一个案例记录在案。

教训

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

作为一名开发人员,这次经历非常棒。这是一个有趣、独特、略微令人恼火的错误,并且不乏教学潜力。以下是我的主要收获

  • 深入了解您的工具会让您成为更好的*此处插入事物*。这个想法跨越所有领域和学科,尽管它在软件中尤其重要。sort 错误之所以能够困扰我们,是因为我们的知识存在差距,它可以激励我们更多地了解 JavaScript(我们最有价值的工具)的工作原理。它将有助于告知我们未来做出的决定,甚至可能防止一些错误。
  • 严重的错误潜伏在表面之下,并且经常在最奇怪的情况下显现出来。

为了让我们最终看到 sort 以我们意想不到的方式运行,我们需要一个使用特定时间格式并跨越两个非常具体的年份(1970-1972 年)的数据集。我发现软件永远不会停止以其可以崩溃的无数种方式令人惊叹。在这方面,我对它原始和不受约束的创造力怀有勉强的敬意。

如果您有兴趣更深入地了解 sort 在幕后是如何工作的,javascriptkit.com 上的这篇文章 非常宝贵。

下一步是什么?开始使用 Chronograf!

现在可以完美地对数字时间戳进行排序!

如果您想试用 Chronograf(TICK stack 中的“C”),您可以从此处下载最新版本。我们喜欢阅读并感谢任何和所有反馈,因此请告诉我们您的想法。无论是错误报告、功能请求、总体体验改进,还是您对产品的任何其他想法或意见,请随时通过电子邮件发送给团队:[email protected]