DQL 是专为 观测云(DataFlux)开发的语言,语法简单,方便使用,可在 DataFlux Studio 进行数据查询,也可通过 客户端命令行 进行数据查询。
在 DataFlux 中,我们用了多个不同的存储引擎(目前主要是 InfluxDB 以及 ElasticSearch),在这种混合存储的场景下,将查询语言统一起来,是非常有意义的:
- DataFlux 是重查询产品,所有的可观测数据,都是通过查询来获取的
- 在具体的可观测场景下,某个简单的图表,可能底层涉及多个不同的存储引擎查找,如果分别查找,会导致网络 IO 剧增,同时也殃及页面响应
- 在巨量观测数据面前,为了防止意外的巨量数据查找,需在查询语句上做保护
- 不同的存储引擎,其查询语法全然不同,无形中给开发人员带来了额外的工作量
- 如果接入其它存储引擎,前端开发又得学一遍查询语法,历史页面也需要大量改造
基于这样一个情况,提供统一的查询语言,迫在眉睫。
对于新设计一门语言,这种事情是工程师所喜闻乐见的。对于新语言的开发,命名首当其冲,对 DataFlux 而言,顺其自然,就是「DQL」。
DQL 要做哪些事情
在上面,我们提到统一查询语言的意义,从中我们也能看到 DQL 要做的一些事情,但只是泛泛而谈。这里,我们要大致列举下 DQL 的能力范围:
所有DataFlux中的观测数据,都能通过DQL查找
DQL查询的返回结构是一致的
不管后端是 InflxuDB 还是 ElasticSearch 还是其它即将引入的存储引擎,它们各自的查询结果返回,结构虽各不相同,但 DQL 需统一好给前端(这里的「前端」包括但不限于 浏览器/命令行等)
DQL需支持参数注入
对浏览器端而言,某些查询条件是不便于直接写入 DQL 的,而 DQL 语句是通过类似 JSON API 发给后端,在这个 JSON 中,需提供各种不同的参数注入,以调整最终的 DQL 查询行为,如额外的分组(group by)参数,查询的时间范围等,因为这些参数,实际上都是可以在 UI 调整的,而表格里面的 DQL 是固定死的,不便于跟 UI 随动,只能通过查询注入
DQL语法要相对简单且高度可扩展
对 DQL 而言,其主要职责是查询(不排除后面提供更新语法),跟 SQL 相比,它只需要提供 SELECT 即可,目前是没有 INSERT/DELETE/UPDATE 功能,从这个角度而言,DQL 就简化了不少。从另一个角度而言,因为底层的存储引擎可能会有多个,对查询功能而言,在语法上,不能对 DQL 做过多限制
DQL针对不同的数据做查询限制
因为不同的数据(日志、时序、对象、APM等),其存储策略不同,查询策略也会有所差异,DQL 需分别对待。
确定了这些,接下来我们确定一下语法选择。
语法选择
我们最为熟悉的查询语言莫过于 SQL,现如今已经成了行业标准,大家基本都能看懂 MySQL/PostgreSQL/SQLServer/Oracle 几家的 SQL 语句,大同小异,稍有不同。查询结构大致如下:
SELECT column_name(s) -- 要查什么 FROM table_name -- 从哪查 WHERE condition -- 过滤条件是什么 GROUP BY column_name(s) -- 结果怎么分组 ORDER BY column_name(s); -- 结果怎么排序
先不论采用何种语法,这些基本要素,DQL 都必须满足。对于目前 DataFlux 使用的查询引擎,以 InflxuDB 为例,其基本查询结构为:
SELECT <field_key>[,<field_key>,<tag_key>] FROM <measurement_name>[,<measurement_name>] WHERE <conditional_expression> GROUP BY [* | <tag_key>[,<tag_key]] ORDER BY time [desc|asc]
而 ElasticSearch 的查询则很庞大(因为它足够灵活),这里以一个简答的查询为例,在 InflxuDB 中查询一条最近的 CPU 数据,其查询大概如下:
SELECT * FROM "cpu" WHERE "host" = '张三的电脑' ORDER BY "time" DESC LIMIT 1
同等的 ElasticSearch 的查询语句则大相庭径,对于这么复杂的查询,如果没有专门的 IDE,是很难写正确的:
{ "query": { "bool": { "must": [ { "bool": { "should": [ { "term": { "class": { "value": "cpu" } } } ] } }, { "term": { "host": { "value": "张三的电脑" } } } } "size": 1, "sort": [ { "last_update_time": { "missing": "_last", "order": "desc", "unmapped_type": "string" } } ] } }
综合两种查询风格,我们可以看到:
- InflxuDB 的查询风格,跟 SQL 基本一致,这也很容易理解,毕竟大家都很熟悉,写起来差不多
- ElasticSearch 的查询很臃肿,但极为灵活,对 ElasticSearch 本身的特性而言,这是正确的设计。虽然 ElasticSearch 也支持 SQL 形式的查询,但其功能(相对)没有 JSON 格式强大
- 对 DQL 而言,这两种风格,似乎都不太合适
- 类 SQL 的语法肯定能满足查询需求,但其关键字太多(SELECT/FROM),写起来繁琐,另外容易让人联想到 insert/update 等语法,而这些在 DQL 设计之初就决定不予支持
- JSON 语法没有必要,DQL 没有这么灵活的查询需求(主要还是太难写了)
为此,我们看了下其它的查询语言,比如 PromQL:
http_requests_total{job="apiserver", handler="/api/comments"}
这里的查询语义为:查询指标 http_requests_total,以 job="apiserver" AND handler="/api/comments" 为过滤条件。注意,这里省去了 SELECT/FROM 这样的语法,直接通过出现的位置来「暗示」其语义,翻译成 SQL 就是:
SELECT * FROM http_requests_total WHERE `job`="apiserver" AND `handler`="/api/comments";
在我们看来,PromQL 的语法,正是 DQL 喜欢的味道,它们要做的事情,其实异曲同工:只专注查询。
确定了语法选型,接下来的事情,就是如何实现这些语法了,语法的实现,有几种常见的思路:
- 直接裸解析,暴力如 TCL 这种 C 编译器,就是这种。当然 InflxuDB 的查询语言处理,也是手写的
- 通过专门的语法生成工具,如 ANTLR 或者 yacc/lex(bison/flex)
我们看了下 PromQL 的实现,决定采用 yacc,相比 ANTLR:
- Golang 中内置了 yacc 实现(PromQL 就是用 golang 实现的),跟我们的技术栈契合很好
- yacc 的性能(内存消耗)相对更好(之前我们通过 ALTLR 做过 InfluxQL 的翻译,性能不太理想)
- 最主要的是,我们的工程师相对更熟悉 yacc
DQL 为什么是现在这个样子
最终,DQL 的语法结构大概如下:
namespace::data-source:(target-clause) {where-clause} [time-expr] by-clause order-by-clause limit-clause
从基本的语法结构中,可以看出,我们倾向于采用一些特殊符号来「暗示」高频语义,而非用确定的单词(如SELECT/FROM 等),因为它们极为常用,简化其输入是我们优先考虑的。但对于相对低频的语义,我们还是选用了英文单词,但还是一个原则:减少输入,如将 GROUP BY,简化成了 BY,但 ORDER BY 我们保持原样,因为它相对不常用。
各个语法结构说明如下:
语法结构
namespace :查询的数据类型,类似于MySQL中的一个数据库
这里我们借鉴了 C++ 中 namespace 语法,如 std::string str1 = "hello",对DQL 而言,就是形如 object::HOST、metric::cpu、logging::nginx,看起来语义很契合。
在 DataFlux 中,截止目前,已经有如下几种数据类型,故需要在语法层面,对查询的数据做命名空间划分,如:
- 时序(metirc/M)
- 对象(object/O)
- 日志(logging/L)
- 事件(event/E)
- 安全(security/S)
- RUM(rum/R)
- APM(tracing/T)
- 自定义对象(custom_object/CO)
- ...
为便于输入,DQL 对各个命名空间,都做了别名。对于最常用的时序数据(M),甚至可以略去别名,默认就是 M 这个命名空间,进一步简化了 DQL 的编写。
data-source :基本查询范围,类似于数据库
以对象为例,这里填写的是对象分类名(class),以时序为例,这里填写的是指标集名称,以日志为例,这里填写的是来源(source),以此类推。
target-clause :查询的字段列表,类似于表字段
如查询 CPU 指标集的两个字段:M::cpu:(usage_guest, usage_idle) LIMIT 1,表示在时序命名空间(M)中查找指标集为 cpu 的两个指标(usage_guest, usage_idle),且只查询一条。
where-clause :以{}来表示过滤条件
如查询主机 CPU 空闲率大于 90% 的机器:
cpu:(host) { usage_idle>90 }
注意,这里的过滤条件可以有多个,按照列表语义来处理,以 , 分割,它们之间是 AND 的关系:
# 如下三个语义是等价的
cpu:(host) { conditon1, condition2 } cpu:(host) { conditon1 AND condition2 } cpu:(host) { conditon1 && condition2 }
既然有 AND 关系,那就有 OR 关系:
# 如下两个语义是等价的
cpu:(host) { conditon1 || condition2 } cpu:(host) { conditon1 OR condition2 }
还可以用括号表示条件之间的各种组合:
cpu:(host) { conditon0 AND (conditon1 || condition2) }
time-expr :时间过滤条件,其表达形式为[start:end:by-interval]
初步看来,这里似乎有一点冗余,比如 time-expr 本质上是一个 where-clause 和 by-clause 的合体,即既指定查询的时间范围,又指定时间范围的分组。之所以将这个语法单独拧出来,主要还是因为,在 DataFlux 的查询中,基于时间的查找以及分组,使用频率极高,几乎所有的查询都有涉及,为了将它们从 where-clause 和 by-clause 中「解放」出来,就单独设计了这个语法单元,我们直接可以在这里实现时间范围过滤以及分组,属于一种「快捷方式」。
另外,这里的 start、end 支持多种时间类型的表示,如:
- [10h:5m:1m] 表示 10 小时以前至 5 分钟以前的时间范围,将查询到的数据,按照 1 分钟的间隔进行分组。在终端手编写 DQL 时,这样指定时间范围非常方便
- [1626401634:1626402634:1m] 表示两个 UNIX 时间戳时间范围,也是按照 1 分钟的间隔分组。这样做更便于大多数编程语言的处理
- [2019-01-01 12:13:14:5m:1w:1d] 表示自 2019-01-01 12:13:14 至一周(1w)前的时间范围,将查询到的数据,按照一天(1d)的间隔分组。这里支持日期格式,主要便于通过 Web 前端的时间控件来指定时间
by-clause :分组语法(同 SQL 中的 GROUP BY)
oder-by-clause :排序语法
limit-clause :限制返回数量间
DQL 如何解决一些具体问题
如何控制查询的数据安全?
DataFlux 是一个准 SAAS 平台,简而言之,就是多租户平台。在这种情况下,不能因为某个意外的查询,影响其他租户的数据体验。基于此,需要对不同的租户,有独立的查询空间:
- 每个独立的工作空间,底层的存储是逻辑隔离的,可以简单理解为,不同租户的数据,是存储在不同的数据库上。而单个 DQL 查询,只能在单个「数据库」上执行查找,不存在「串库」的情况
- 由于观测数据量极大,极有可能是在指定数据查询的时间范围时,手抖了一下,造成底层存储的巨量 IO 查询,进而影响整个集群的租户。为此,在 DQL 的 HTTP 查询接口上支持时间范围的指定(默认 15 分钟),即使没有指定,DQL 本身也会检查查询的时间范围是否超过系统的设定,这在很大程度上保护的底层的存储系统
是如何辅助 DataFlux 前端开发的
前面提到,DQL 的 HTTP 接口是支持注入的,为便于前端实现各种复杂的数据观测场景,DQL 额外支持如下这些查询参数注入:
- 返回的最大点数控制:在一些密集绘图的前端页面上,巨量的数据返回,可能导致前端页面卡死甚至奔溃。有了最大点数控制,就能杜绝这种情况
- 过滤条件注入:这个跟时间范围的注入类似,主要应用在数据权限控制上
- 排序字段注入:在一些特定的数据页面上,需要对返回的数据,按照指定的字段来排序,比如,返回的主机列表上,虽然默认按照主机名来排序(这个默认的 DQL 写好了),单用户可以选择按照 CPU 或内存使用率来排序,此时就可以在 HTTP 请求参数上额外指定排序字段,覆盖默认 DQL 上的 ORDER BY 字段
- 禁止多字段返回:在一些 UI 效果上,多列返回是无法绘图的,但为了避免 DQL 真的返回了多列数据,可以对应的 UI 效果上,通过 HTTP 接口禁用多列查询,这样依赖, DQL 解析阶段就能检测到错误,非常便于日常的开发以及调试
- 额外的其它一些注入,主要也是便于实现数据展示效果,比如深度分页、查询的高亮显示等等
「即时」的数据查询效果
DataFlux 中的数据,大部分都是通过 DataKit 上传的,为此,我们在 DataKit 中内置的 DQL 查询终端。数据采集完后,稍后片刻(考虑到多级缓存、网络传输延迟等因素)即可通过 DQL 查询到刚刚上传的数据,而不用打开 DataFlux 来查看数据。另外,某些情况下,特别是在开发阶段,DataFlux 前端可通过这个命令行终端,来排查一些数据问题
灵活的数据处理
主要体现在如下方面:
- 方便不同的数据接入:如果有新的数据分类需要接入,扩充一个 namespace 即可。如果有新的存储引擎接入,只需要增加一套对应存储引擎的查询翻译即可
- 可以对查询到的数据,进行灵活的额外处理。假定 InfluxDB 不支持某个数学处理函数,DQL 查询到数据后,可通过 Golang/Python 等,自定义实现即可。另外,还能跨服务实现数据的多级计算,比如将 DQL 查询到的数据,送给 Function 处理
目前,DataFlux 中绝大多数的数据查询,都是通过 DQL 来实现的,历经近一年的开发迭代,DQL 日趋稳定,功能也日渐强大。随着 DataFlux 业务的不断发展,DQL 也将面临着更大的挑战。