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

导航至

上个月,我在 InfluxDays London 上做了一次演讲,关于 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的界面,用于点对点击构建脚本,从一开始就应该是第一优先级的。

程序员阅读的代码远比他们自己写的要多。可读性对于你自己写的代码也同样重要,而不仅仅是别人的代码。如果你曾经参与过一个项目,并对一些你认为愚蠢或难以理解的代码行进行了“git blame”,结果发现是你一年前写的,你就知道我的意思了。

它应该是“可组合的”,这意味着用户应该能够根据自己的特定用例和需求在语言之上构建。你应该能够定义函数并创建它们自己的整个库。随着你与语言的互动越来越多,你应该能够更多地围绕你的问题领域对其进行定制。

它应该是“可测试的”。查询是代码,如果它们将成为长期运行应用程序的一部分,那么它们应该被测试并纳入源代码控制。进一步来说,查询的各个部分应该能够单独测试。用户应该能够构建复杂的查询,但又能分别测试查询的各个部分。

最后,我们希望Flux既易于贡献,又易于与其他开发者分享Flux代码库和函数。我们希望继续向语言添加新函数,并继续通过更少的代码为用户提供更多功能。我们还希望鼓励社区贡献以添加新函数,并且我们有意地构建了这样的结构,使新贡献者可以在不了解引擎内部结构的情况下参与其中。我们在Telegraf的插件系统设计中取得了巨大成功,我们希望为Flux引擎中的新函数复制这种成功。Flux作为查询语言的设计应该使得每个人都可以添加这些功能,而不必与其语义进行斗争或更改。

开发者反复创建相同的查询是极其低效的。我们希望常见的查询和使用案例得到表示和共享,这样我们就可以停止在InfluxDB中重新发明每个单独的查询。因为我们有一个共同的数据收集器(Telegraf)和共同的模式,所以应该能够有社区构建的可重用查询和函数。这些可以是为监控和警报、常见的ETL任务或与第三方系统集成以共享数据或发送通知和警报的定义。我们的用户不应该需要为常见的第三方服务和系统重新发明监控查询。

在高级设计原则已经明确之后,让我们来谈谈为什么我们不仅仅是将 InfluxQL 转换为符合 SQL 标准。采用这种方法有明显的优势。SQL 被许多开发者所熟知,并且有一个与之兼容的大型工具和库生态系统。由于它集成了许多事物并且拥有经验丰富的开发者基础,因此这是一个更安全的选择。然而,我认为 SQL 不是处理时序数据的最佳语言。SQL 是围绕关系代数和集合操作设计的。语言的语义与我认为的时序数据的基于流的模型不符。时序数据是一个连续的流,应用函数、计算和转换。一个函数执行一些操作然后将其输出传递给下一个函数,该函数再执行一些操作,依此类推。这种函数式风格也使得可以在不改变语言整体语义的情况下引入新的函数。

我不想生活在一个人类能够想到的最佳数据语言是在 70 年代发明的世界里。

SQL 是一个伟大而强大的工具,但不应该是唯一的选择。说实话,我不想生活在一个人类能够想到的最佳数据语言是在 70 年代发明的世界里。我拒绝让这种情况成为我的现实。但要从 20 世纪进入 21 世纪,就需要创造一些新的东西,这也意味着它不会拥有 40 年的发展、教育和标准化的惯性。让我们深入探讨一些原因和为什么我们认为创建 Flux 值得花费精力在长期内建立一个围绕它建立社区的原因。

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

我们希望有一种语言可以让开发者和数据科学家将更多的工作负载推入数据平台层。开发者不应该在从数据中获得洞察之前,不得不编写 Python 脚本来进一步处理、塑形和精炼他们的数据。例如,我们可以计算序列之间的相似性,进行字符串操作和修改,创建警报规则,甚至从其他来源拉取数据。将这种功能强行塞入 SQL 语言会很快变得丑陋。当然,我们可以创建一个类似 Oracle 的 PL/SQL 或 Microsoft 的 T-SQL 的图灵完备 SQL 变体,但那样它就不再是真正的 SQL 了。这种新的功能看起来像是事后添加的(它确实是)。纯粹的语言 SQL 开始看起来像随着时间的推移建立起来的棚户区。

理想情况下,我们会有一种从头开始设计的语言,不仅可以进行声明性查询,还可以专门用于数据处理和时序数据。以下是一个在 Flux 中计算多个时序指数移动平均的简短示例。

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

这个例子将查询拆分成多行,但实际上它也可以放在一行中。从这段代码中,我们可以看到一些亮点。首先,它使用的是函数式编程语言。管道向前操作符(|>)表示我们将左侧函数的输出发送到右侧函数。我们可以看到这些函数接受命名参数(有助于提高可读性)。在过滤器函数中,我们看到也可以传递匿名函数作为参数。这种风格看起来非常像JavaScript,这是故意的。我们希望创建一种看起来和感觉上多少有些熟悉的语言。

让我们逐步分析函数的功能。首先,我们确定了数据提取的数据库,并将集合过滤到只有最后一个小时的数据,以及只有测量值为“foo”的时间序列。最后,我们将这些序列发送到指数移动平均函数,该函数将基于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 平台存储库中标记为 area/query 的 问题 中记录问题,或者查看 Flux 语言规范,或者 Flux 引擎代码(MIT 许可证下)。

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