天天看点

Grafana发布2.0版:解析ClickHouse查询方式

作者:JAVA我要发大财
Grafana发布2.0版:解析ClickHouse查询方式

早在 2022 年 5 月,我们就发布了Grafana 的 第一方 ClickHouse 插件 ,由 Grafana 与 ClickHouse 合作开发。 作为我们对维护和改进此插件的承诺的一部分,我们很高兴地宣布发布 2.0 版。

这个主要版本对我们解析 ClickHouse 查询的方式进行了根本性更改,这将有助于解决我们的用户遇到的关于 ad-hoc 过滤器的一组常见问题。 我们还添加了对使用 HTTP 作为传输协议的支持,并增强了对 ClickHouse 数据类型(包括 JSON)的支持。

处理过滤器的更好方法

当我们最初开发插件时,我们想使用 Grafana 的最新插件框架。 这可确保我们支持具有服务器端依赖性的警报等功能。 通过这个新框架,我们支持了 Ad-hoc 过滤器的初始实现。 这个强大的 Grafana 功能 仅支持最流行的数据源 ,允许用户通过选择列、运算符和值来过滤所有可视化。 虽然我们最初的产品在很大程度上发挥了作用,但它依赖于使用客户端 AST 实现来解析 SQL 查询。 即使对于有经验的开发人员来说,这也是一个复杂的问题——尤其是因为 ClickHouse SQL 有几个扩展来帮助用户构建分析查询。 这个实现随后是 几个问题 ,主要与使用 Grafana 变量、模板 和子查询有关。

我们没有在 AST 解析器上投入更多时间,而是联系了一个已经以最佳方式解决了这个问题的团队:我们在 ClickHouse 核心开发团队中的朋友。 经过一番 简短的讨论 ,诞生 additional_table_filters 于 22.7 。 这允许任何过滤器作为设置的一部分与查询一起发送。 在解析查询时,ClickHouse 可以将这些过滤器注入到适当的子句中。

随意针对 play.clickhouse.com 测试以下任何示例。 数据库中的任何表都可用 blogs ,因此 FROM <table> 应相应调整子句,即 FROM blogs <table> .

考虑来自英国房地产价格数据集 的简单查询 。

SELECT     toStartOfYear(date) AS time,     town,     round(avg(price)) AS price FROM uk_price_paid WHERE town IN (     SELECT town     FROM uk_price_paid     WHERE town != 'GATWICK'     GROUP BY town     ORDER BY avg(price) DESC     LIMIT 10 ) GROUP BY     time,     town ORDER BY time ASC
           

这个简单的查询告诉我们随着时间的推移,英国最昂贵的 10 个城镇的每个城镇的平均价格。 请注意,我们排除了盖特威克机场,因为它代表了异常情况。 这自然会在 Grafana 中呈现为区域图或折线图。 这里的逻辑过滤器可能是供用户按城镇过滤。 确保将其注入查询的正确部分是一项挑战。 基于 JavaScript 的解决方案虽然可能,但需要涵盖所有可能的边缘情况。 例如,在这里,我们将最好将城镇过滤器注入 IN 子句并与 town != 'Gatwick' .

Grafana发布2.0版:解析ClickHouse查询方式

注意:由于使用权限不足 additional_table_filters ,如果使用 play.clickhouse.com 作为 ClickHouse 数据源,Adhoc 过滤器将不起作用。 有关详细信息,请参阅下面的“注意更改”。

添加一个 ad hoc 过滤器并选择 town=London ,会导致 Grafana 发送以下查询:

SELECT toStartOfYear(date) AS time, town, round(avg(price)) AS price FROM uk_price_paid WHERE town IN (SELECT town                FROM uk_price_paid                WHERE uk_price_paid.town != 'GATWICK'                GROUP BY town                ORDER BY avg(price) DESC                LIMIT 10) GROUP BY time, town ORDER BY time SETTINGS additional_table_filters = {'uk_price_paid' : 'town = \'LONDON\' '}
           

反过来,ClickHouse 会努力确定需要在何处添加词字句 - 请注意如何将过滤器指定为键等于表名的映射 uk_price_paid 。

Grafana发布2.0版:解析ClickHouse查询方式

添加更多过滤器是微不足道的。 在下面的示例中,我们按地区过滤,重点关注哈克尼。

SELECT toStartOfYear(date) AS time, town, round(avg(price)) AS price
FROM uk_price_paid
WHERE town IN (SELECT town
FROM uk_price_paid
WHERE uk_price_paid.town != 'GATWICK'
AND uk_price_paid.town == 'LONDON'
GROUP BY town
ORDER BY avg(price) DESC
LIMIT 10)
GROUP BY time, town
ORDER BY time settings additional_table_filters = {'uk_price_paid' : 'town = \'LONDON\' AND district = \'HACKNEY\' '}           
Grafana发布2.0版:解析ClickHouse查询方式

为了获得正确的结果,ClickHouse 需要确保将过滤器注入到顶级 WHERE 子句中。

虽然是为了响应 Grafana 要求而添加的,但此功能可用于更广泛的工具来利用和改进其产品。 请让我们知道这是否有用!

对 HTTP 的需求

在底层,Grafana 插件使用 clickhouse-go 客户端向 ClickHouse 发送查询。 从历史上看,这个客户端使用原生格式通过 ClickHouse 二进制协议进行通信。 这代表了最有效的通信方式,并出于性能原因而选择。 这对于 INSERT 繁重的用例是有意义的,但对于 Grafana 中常用的聚合查询则不太有意义。 我们的用户经常需要通过 HTTP 传输流量,以允许打开负载均衡器或使用代理解决方案,例如 ch-proxy 。

由于 社区贡献 ,clickhouse-go 驱动程序添加了对 HTTP 原生格式的支持。 此功能现在在 Grafana 中公开,可以在数据源级别进行选择。

Grafana发布2.0版:解析ClickHouse查询方式

对于那些尝试此功能的人,请记住 HTTP 使用与 Native 不同的端口 - 默认情况下 HTTP/HTTPS 使用 8123/8443。 最后,我们将之前使用相同值的连接超时和查询超时分开。

欢迎半结构化数据

ClickHouse 22.6 添加 了对 JSON 作为数据类型的支持。 JSON Object 类型在处理复杂的嵌套结构时具有优势,这些结构可能会发生变化。 该类型在插入过程中自动从结构中推断列,并将这些列合并到现有的表模式中。 列将根据需要创建,允许用户在不维护模式的情况下处理半结构化数据。 这种能力有很多用途,尤其是简化了 ClickHouse 作为日志存储引擎的使用。

在它添加到 ClickHouse 后不久,对 JSON 的支持被添加到 clickhouse-go 客户端。 这个支持现在已经在我们的 Grafana 插件 v2.0 中实现了。

JSON 叶节点现在可以像任何其他等效原始类型的列一样用于图表。 表示 JSON 对象或 JSON 对象列表的列的处理方式分别类似于 ClickHouse 中的 Tuple 和 Nested 类型。 在 Grafana 中,这意味着将它们呈现为 JSON 字符串。 虽然这与 Grafana 图表不兼容,但 JSON 对象可以显示在 探索视图 或 日志面板 中- 对于在 ClickHouse 中存储结构化日志的人来说,这是一个有用的补充。

为了测试此功能,用户可以使用 此处 描述的日志数据集的子集。 这里我们使用一个简单的模式:

SET allow_experimental_object_type=1;

CREATE TABLE http_logs
(
`message` JSON,
`timestamp` DateTime
)
ENGINE = MergeTree()
ORDER BY timestamp;

INSERT INTO http_logs (timestamp, message) SELECT
`@timestamp` AS timestamp,
concat('{"status":', toString(status), ', "size":', toString(size), ', "clientip": "', toString(clientip), '", "request": ', toJSONString(request), '}') AS message
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/http/documents-01.ndjson.gz', 'JSONEachRow')           

我们从公开可用的 s3 存储桶中插入上述数据 - 随意尝试或使用 play.clickhouse.com ! 请注意,我们确保我们有一个 timestamp 与 JSON 分开的时间列。 由于移动了消息字段下的所有列,每行如下所示。

{"message":{"clientip":"40.135.0.0","request":{"method":"GET","path":"\/images\/hm_bg.jpg","version":"HTTP\/1.0"},"size":24736,"status":200},"timestamp":"1998-04-30 19:30:17"}
           

这些 JSON 日志现在可以在浏览视图或日志面板中呈现。 我们从响应状态中推断出一个“级别”字段,它控制颜色渲染。 我们建议对所有查询施加限制以避免超时,因为 Grafana 需要将完整的结果集转换为帧。

探索视图的直方图渲染基于返回的结果,尽管我们计划在以后的版本中计算 类似于 Loki 和 Elasticsearch 的全范围日志分布。 为了确保用户不需要在 Grafana 中调整时间范围,我们使用数据集的最大日期将此数据集转移到当前时间范围。

SELECT     now() - (toDateTime('1998-05-08 13:44:46') - timestamp) AS log_time,     multiIf(message.status > 500, 'critical', message.status > 400, 'error', message.status > 300, 'warning', 'info') AS level,     message.request.method AS method,     message.status AS status,     message.size AS size,     message.request AS log FROM http_logs ORDER BY timestamp DESC LIMIT 10000
           

Grafana发布2.0版:解析ClickHouse查询方式

日志面板和浏览视图都对字段名称敏感。 要使用 Explore 的日志可视化模式(包括直方图)显示结果,行必须包含“log_time”字段并按“log_time”字段排序。

JSON 支持需要重写我们在插件中处理 ClickHouse 类型的方式。 覆盖范围现在应该是全面的,支持所有类型,包括我们需要作为 JSON 工作的一部分支持的复杂类型,例如 Tuple 和 Nested。

变量的变化

从历史上看,我们依靠 AST 自动优化 WHERE 子句中的 IN 条件,以应对用户为变量选择“全部”的情况。 该插件将有效地从任何 WHERE 子句中删除对变量的使用,从而避免发送条件的需要。 虽然这是一个不错的功能,但它依赖于 AST,并假设完全理解 ClickHouse SQL 方言。 在 2.0 版中,我们将此优化推迟给应该使用宏 __conditionalAll 包装 IN 子句的用户,该子句使用变量。

例如,假设我们正在可视化伦敦的房价,并希望允许按用户选择的地区进行过滤,即通过 district IN (${district:singlequote}) :

Grafana发布2.0版:解析ClickHouse查询方式

我们的变量过滤器看起来像这样——注意选择“All”选项的能力:

Grafana发布2.0版:解析ClickHouse查询方式

如果选择全部,则查询变为:

SELECT     toStartOfYear(date) AS time,     district,     round(avg(price)) AS price FROM uk_price_paid WHERE (district IN (     SELECT district     FROM uk_price_paid     WHERE town = 'LONDON'     GROUP BY district     ORDER BY avg(price) DESC     LIMIT 10 )) AND (district IN ('TOWER HAMLETS', 'HACKNEY', 'NEWHAM', 'CITY OF LONDON', 'WALTHAM FOREST', 'REDBRIDGE', 'BARKING AND DAGENHAM', 'HAVERING', 'HARINGEY', 'EPPING FOREST', 'ISLINGTON', 'CAMDEN', 'CITY OF WESTMINSTER', 'BARNET', 'HARROW', 'HILLINGDON', 'ENFIELD', 'EALING', 'HOUNSLOW', 'HAMMERSMITH AND FULHAM', 'LEWISHAM', 'BRENT', 'WANDSWORTH', 'SOUTHWARK', 'LAMBETH', 'GREENWICH', 'KENSINGTON AND CHELSEA', 'MERTON', 'BROMLEY', 'RICHMOND UPON THAMES', 'CROYDON', 'BEXLEY', 'KINGSTON UPON THAMES', 'HARLOW', 'SUTTON', 'CITY OF BRISTOL', 'MALVERN HILLS', 'THURROCK', 'RHONDDA CYNON TAFF')) GROUP BY     time,     district ORDER BY time ASC
           

虽然对于只有几个值的变量(例如伦敦的地区)来说很好,但对于较长的列表来说,它会成为性能开销。 为了优化,用户可以在子句周围加上一个 __conditionalAll 例如

SELECT toStartOfYear(date) AS time,        town,        round(avg(price))   AS price FROM uk_price_paid WHERE town IN (     SELECT town     FROM uk_price_paid     WHERE town != 'GATWICK' AND $__conditionalAll(district IN (${district:singlequote}), $district)     GROUP BY town     ORDER BY avg(price) DESC     LIMIT 10 ) GROUP BY time, town ORDER BY time ASC
           

在选择“全部”时,地区限制被简单地替换为一个 1=1 条件。

SELECT     toStartOfYear(date) AS time,     district,     round(avg(price)) AS price FROM uk_price_paid WHERE (district IN (     SELECT district     FROM uk_price_paid     WHERE uk_price_paid.town = 'LONDON'     GROUP BY district     ORDER BY avg(price) DESC     LIMIT 10 )) AND (1 = 1) GROUP BY     time,     district ORDER BY time ASC
           

注意变化

除了上述的大量新增内容外,我们还修复了 此版本的 几个错误。 AST 的删除特别意味着此版本确实有一些重大更改,除了需要手动优化 IN 过滤器之外,用户应该注意:

  • 新的 Adhoc 过滤器实现依赖于该 additional_table_filters 功能,因此 ClickHouse 22.7。 旧版本的 ClickHouse 不会填充过滤器。 如果您无法迁移到大于或等于此版本的 ClickHouse 版本,请不要升级您的插件。
  • additional_table_filters 与查询本身一起在 SETTINGS 中传递。 除非readonly=2 ,否则只读用户不允许这样做 。 这并不理想,我们不建议为 ClickHouse 的公共实例设置此设置。 我们认识到这里需要改进。 敬请期待 。

由于上述原因,Adhoc 过滤器不适用于 play.clickhouse.com。

对于希望贡献或关注最新问题和改进的用户,ClickHouse 官方插件是 托管在 GitHub 上并在 TypeScript 和 Go 中实现 的开源项目。 我们始终重视您的反馈并鼓励用户提出问题,以便我们继续改进插件。

继续阅读