为什么我们构建 Flux,一种新的数据脚本和查询语言

导航至

上个月,我在伦敦 InfluxDays 大会上就 Flux (#fluxlang) 做了演讲,这是我们为 InfluxDB 2.0 构建的新的查询和脚本语言。 当我谈论 Flux 时,我被问到的最常见问题之一是为什么?为什么要费力创建一种新语言?为什么不直接使用 SQL?或者,如果您需要一种实际的脚本语言,为什么不使用一些已经存在且可嵌入的语言,如 Lua 或 JavaScript?所有这些问题,对我来说,都隐约让人想起我们最初创建 InfluxDB 的时候。为什么你要构建一个时间序列数据库,而不是仅仅在一些通用数据库之上构建?这些都是合理的问题,在这篇文章中,我将尝试阐明我们创建新语言的动机。

当我们第一次创建 InfluxDB 时,我们从一种名为 InfluxQL 的语言开始,它看起来像 SQL。这为新用户提供了一个简单的入门方式,并且看起来有点熟悉,所以在最初的时候让人感到舒适。然而,它并不完全是 SQL,并且在一些关键方面有所不同。随着时间的推移,我们发现这对于那些期望它以相同方式运行并具有 SQL 所有功能的 SQL 专家用户来说,可能会非常令人沮丧。对于那些想要更多更高级查询功能的用户来说,他们最终会遇到语言的局限性。借助 InfluxDB 2.0,我们希望解决所有这些功能请求,并为用户提供易于学习且功能更强大的东西。这促使我们创建了 Flux。

在深入讨论 SQL 与 Flux 之前,我想先阐述一下我们对该语言的设计目标。当我谈论我们可能追求的其他替代方案时,我们将重新审视这些目标。Flux 语言应该是:

  • 可用的
  • 可读的
  • 可组合的
  • 可测试的
  • 可贡献的
  • 可共享的

当我们说我们希望 Flux “可用”时,我们的意思是我们希望优化程序员的幸福感和生产力。我们希望它易于学习和使用,高效甚至有趣。程序员幸福感的论点胜过语言的“纯粹性”。这是我们的首要和最高优先级,其次是“可读性”。这也意味着从一开始就包括 REPL、用于测试脚本的强大 CLI 以及用于点击构建脚本的友好的基于 Web 的 UI,作为一等公民。

程序员阅读的代码远多于他们编写的代码。可读性对于您自己编写的代码也很重要,而不仅仅是其他人的代码。如果您曾经在一个项目中,对您认为愚蠢或难以理解的某行代码执行了“git blame”,结果却发现是您自己在一年前编写的,您就会明白我的意思。

它应该是“可组合的”,这意味着用户应该能够在其之上构建语言,以满足其特定的用例和需求。您应该能够定义函数并创建完整的函数库。您使用该语言越多,就越应该能够围绕您的问题领域来塑造它。

它应该是“可测试的”。查询是代码,如果它们将成为长期应用程序的一部分,则应该对其进行测试并检入源代码控制。此外,查询的各个部分应该可以单独测试。用户应该可以构建一个复杂的查询,但可以分别测试每个部分。

最后,我们希望 Flux 既易于贡献,又易于与其他开发人员共享 Flux 代码的函数和库。我们希望继续向该语言添加新函数,并继续为用户提供更多功能,同时减少他们需要编写的代码。我们还希望鼓励社区贡献,向该语言添加新函数,并且我们有意地构建了事物,以便新贡献者可以在不了解引擎本身内部结构的情况下参与进来。我们在 Telegraf 插件系统的设计方面取得了巨大的成功,我们希望在 Flux 引擎的新函数中复制这一点。Flux 作为查询语言的设计应该能够让每个人都能进行这些添加,而无需与语言的语义作斗争或更改其语义。

对于开发人员来说,一遍又一遍地创建相同的查询是非常低效的。我们希望常见查询和用例能够被表示和共享,以便我们停止在 InfluxDB 中重新发明每个单独的查询。因为我们有一个通用的数据收集器 (Telegraf) 和一个通用的模式,所以应该可以拥有由社区构建的可重用查询和函数。这些可以是用于监控和警报、常见 ETL 任务或与第三方系统集成的定义,以共享数据或发送通知和警报。我们的用户不应该为常见的第三方服务和系统重新发明监控查询。

现在高层设计原则已经阐明,让我们来谈谈为什么我们不只是让 InfluxQL 符合 SQL 标准。采用这种方法有明显的优势。SQL 为许多开发人员所熟知,并且有一个庞大的工具和库生态系统与之兼容。这是一个更安全的选择,因为它与很多东西集成,并且拥有经验丰富的开发人员基础。但是,我不认为 SQL 是处理时间序列数据的最佳语言。SQL 是围绕关系代数和处理集合而设计的。语言的语义与我想到时间序列数据时的基于流的模型不一致。时间序列是一个连续的流,在其上应用函数、计算和转换。一个函数执行某些操作,然后将其输出发送到下一个函数,依此类推。这种函数式风格也使得可以在不更改整个语言语义的情况下引入新函数。

我不希望生活在一个人类能想到的用于处理数据的最佳语言是在 70 年代发明的世界里

SQL 是一个伟大而强大的工具,但不应该是唯一的工具。老实说,我不希望生活在一个人类能想到的用于处理数据的最佳语言是在 70 年代发明的世界里。我拒绝让那成为我的现实。但是,要跃入 21 世纪,就意味着要创造新的东西,这也意味着它不会拥有 40 年的开发、教育和标准化的惯性。让我们深入探讨一些原因,并举例说明为什么我们认为创建 Flux 值得努力随着时间的推移围绕它建立一个社区。

首先,我们希望提供 SQL 标准中当前不存在的功能。当然,SQL 有一些扩展包括时间序列,但它们不是标准的一部分。我们实际上想为处理时间序列(或任何类型)数据创建新的标准语言。这就是为什么我们决定在宽松的 MIT 许可证下许可 Flux 语言和引擎。我们希望它具有渗透性、普遍性,并被许多项目使用。

我们希望有一种语言能够使开发人员和数据科学家将更多工作负载推送到数据平台层。开发人员不应该编写 Python 脚本来进一步处理、塑造和优化他们的数据,然后从中获得洞察力。例如,我们可以计算系列之间的相似性,进行字符串操作和修改,创建警报规则,甚至从其他来源提取数据。将这类功能硬塞到 SQL 语言中会很快变得难看。当然,我们可以创建一个 图灵完备 的 SQL 变体,如 Oracle 的 PL/SQL 或 Microsoft 的 T-SQL,但那样它就不再是完全的 SQL 了。这种新功能看起来像是事后才附加上的东西(实际上也是如此)。那种纯粹的 SQL 语言开始看起来像随着时间推移而建立起来的棚户区。

理想情况下,我们应该有一种从头开始设计的语言,不仅用于声明式查询,还用于数据处理,特别是处理时间序列数据。以下是一个简短的示例,说明在 Flux 中计算多个时间序列的指数移动平均线是什么样的:

from(db:"telegraf")
  |> range(start:-1h)
  |> filter(fn: (r) => r._measurement == "foo")
  |> exponentialMovingAverage(size:-10s)

此示例将查询分解为多行,但它可以存在于单行中。从这段代码中可以跳出几件事。首先,该语言是函数式的。管道转发运算符 (|>) 表明我们将左侧函数的输出发送到右侧函数。我们可以看到这些函数接受命名参数(以帮助提高可读性)。在 filter 函数中,我们看到匿名函数也可以作为参数传递。这种风格应该看起来与 Javascript 非常相似,这是故意的。我们希望创建一种看起来和感觉都有些熟悉的语言。

让我们逐步了解该函数正在做什么。首先,我们已经确定了从中提取数据的数据库,并将数据集筛选到仅限最后一小时以及仅限测量值为 “foo” 的时间序列。最后,我们将这些系列中的每一个都发送到 exponentialMovingAverage 函数,该函数将基于 10 秒的间隔计算它(很像 Graphite 函数)。

在这一点上,编写 SQL 等效的查询示例超出了我的 SQL 能力。随着对象关系映射器和 NoSQL API 在针对数据库的开发时间中占据越来越多的时间,我已经忘记了更多关于 SQL 复杂性的知识。经过搜索,我找到了这个在 SQL 中计算滚动平均值的示例(请注意,我实际上找不到原始来源,因为网上有太多此问题的变体,我无法找到返回此问题的路径):

select id, 
       temp,
       avg(temp) over (partition by group_nr order by time_read) as rolling_avg
from ( 
  select id, 
         temp,
         time_read, 
         interval_group,
         id - row_number() over (partition by interval_group order by time_read) as group_nr
  from (
    select id, 
    time_read, 
    'epoch'::timestamp + '900 seconds'::interval * (extract(epoch from time_read)::int4 / 900) as interval_group,
    temp
    from readings
  ) t1
) t2
order by time_read;

这是计算单个时间序列的滚动平均值。我们的 Flux 示例对许多不同的系列执行了此操作。与 Flux 中的管道转发(可以按顺序查看事物作为流)不同,SQL 查询必须使用嵌套的 select 语句来将数据集放在一起。这有点像 Lisp 最糟糕的部分(嵌套函数调用),但可读性甚至更差。Flux 示例不仅更简洁,而且更具可读性和可理解性。

回到可组合性的概念,用户应该能够定义函数并导入其他开发人员创建的代码和函数。SQL 不具备此功能,因此我们将求助于附加它或实现 T-SQL、存储过程或类似的东西。在 Flux 中定义函数非常简单。例如,假设我们想要获取一堆时间序列,并使系列中的每个值都是其自身的平方:

square = (table=<-) => {
  table |> map(fn: (r) => r._value = r._value * r._value)
}

在这里,我们定义了一个名为 “square” 的函数,它接受一个表(可以是通过管道转发到函数的表,也可以是 “table” 参数)。然后,它将该表发送到 map 函数,该函数遍历每个值并计算其平方。

该语言的函数式风格使得测试查询的各个部分变得容易得多。从用户的角度来看,函数接受输入,执行一些计算或转换,并生成输出。这使得为语言中定义的任何函数编写广泛的单元测试变得非常简单。

好的,所以我们已决定为 InfluxDB 2.0 创建一种脚本语言,而不是实现 SQL(请注意,我们将继续支持 InfluxQL 和 TICKscript)。为什么不使用 Lua 或 Javascript 而不是创建新的东西?首先,Lua 的使用范围不够广泛,无法让我们的用户群更容易学习。Javascript 虽然广为人知和使用,但该语言中存在许多作为历史包袱的东西——就像某种尚未脱落的卷尾猴尾巴。对于我们的用例,我们不需要这些语言的全部功能。即使 Flux 将是图灵完备的,我们也会积极限制语法,并始终以可读性和清晰度为目标。例如,Flux 仅支持函数的命名参数。这使得调用函数的代码更具可读性,而在使用位置参数时,您最终不得不查找函数定义才能理解传入的内容。

在接下来的几周和几个月里,我将撰写更多关于该语言中特定功能和设计考虑因素的文章,以突出显示 Flux 为用户提供的功能远超 InfluxQL,以及这种风格如何为阅读查询的开发人员提供更高的清晰度。我还将尝试突出显示 Flux 将实现的某些功能,这些功能要么在 SQL 标准中是不可能的,要么需要大量的 SQL 技巧才能实现。同时,您可以在我们社区网站的 Flux 版块提问,或在 InfluxData Platform 代码库中标记 area/query 标签提交 issue,或查看 Flux 语言规范,或 Flux 引擎代码(在 MIT 许可证下)。

有关该语言、原因以及指导我们设计的原则的更多信息,请观看我上个月在伦敦的演讲视频: