早在 2022 年 5 月,我们发布了 Grafana 的 ClickHouse 插件,该插件由 Grafana 与 ClickHouse 合作开发。作为我们维护和改进此插件的承诺的一部分,我们很高兴地宣布 2.0 版本的发布。
此主要版本包含对我们解析 ClickHouse 查询方式的根本性更改,这应该有助于解决用户在使用临时筛选器时遇到的一系列常见问题。我们还添加了对使用 HTTP 作为传输协议的支持,并增强了对 ClickHouse 数据类型的支持,包括 JSON。
一种更好的处理筛选器的方式
在我们最初开发插件时,我们希望使用 Grafana 的最新插件框架。这确保了我们支持诸如警报之类的功能,警报具有服务器端依赖关系。通过这个新框架,我们支持了临时筛选器的初始实现。此强大的 Grafana 功能仅受最受欢迎的数据源支持,允许用户通过选择列、运算符和值来筛选所有可视化。虽然我们的初始产品在很大程度上有效,但它依赖于使用客户端AST实现来解析 SQL 查询。即使对于经验丰富的开发人员来说,这也是一个复杂的问题 - 尤其是在 ClickHouse SQL 有几个扩展来帮助用户构建分析查询的情况下。此实现随后成为若干问题的根源,主要与使用Grafana 变量、模板和子查询有关。
与其在 AST 解析器上投入更多时间,我们联系了一个已经以最佳方式解决了此问题的团队:我们 ClickHouse 核心开发团队的朋友们。经过一些简短的讨论,additional_table_filters
在22.7 版本中诞生。这允许将任何筛选器作为 SETTINGS 的一部分与查询一起发送。在解析查询时,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✎
此简单查询告诉我们,随着时间的推移,英国十大最昂贵城镇每个城镇的平均价格。请注意,我们排除了 Gatwick,因为它代表一个异常值。这自然会在 Grafana 中呈现为区域图或折线图。这里的逻辑筛选器可能是让用户按城镇筛选。确保将其注入查询的正确部分是一个挑战。基于 JavaScript 的解决方案虽然可行,但需要涵盖所有可能的边缘情况。例如,在这里,我们将最佳地将城镇筛选器注入 IN 子句并与 town != 'Gatwick'
进行 AND 操作。
注意:由于权限不足无法使用
additional_table_filters
,因此如果使用 play.clickhouse.com 作为 ClickHouse 数据源,临时筛选器将无法工作。有关更多详细信息,请参见下面的“注意更改”。
添加临时筛选器并选择 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
。
添加更多筛选器非常简单。在下面的示例中,我们按地区筛选,重点关注 Hackney。
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\' '}
为了获得正确的结果,ClickHouse 需要确保将筛选器注入到顶级 WHERE 子句中。
虽然是在响应 Grafana 的需求时添加的,但此功能可供更广泛的工具使用,以利用和改进其产品。如果您发现此功能有用,请告知我们!
对 HTTP 的需求
在后台,Grafana 插件使用clickhouse-go客户端将查询发送到 ClickHouse。从历史上看,此客户端通过使用本机格式的 ClickHouse 二进制协议进行通信。这代表了最有效的通信方式,并且出于性能原因而被选中。这对于 INSERT 密集型用例很有意义,但对于 Grafana 中常用的聚合查询来说意义不大。我们的用户通常需要通过 HTTP 传输流量,以允许启用负载均衡器或利用诸如ch-proxy之类的代理解决方案。
由于社区贡献,HTTP 上的本机格式支持已添加到 clickhouse-go 驱动程序中。此功能现在在 Grafana 中公开,可以在数据源级别进行选择。
对于尝试此功能的人员,请记住 HTTP 使用与 Native 不同的端口 - 默认情况下 HTTP/HTTPS 为 8123/8443。最后,我们分开了连接和查询超时,以前它们使用相同的值。
欢迎半结构化数据
ClickHouse 22.6 添加了对 JSON 的支持作为数据类型。当处理复杂嵌套结构(这些结构可能会发生变化)时,JSON 对象类型非常有用。该类型在插入期间会自动从结构中推断列,并将这些列合并到现有的表模式中。将根据需要创建列,允许用户处理半结构化数据,而无需维护模式。此功能有许多用途,尤其是简化 ClickHouse 作为日志存储引擎的使用。
在添加到 ClickHouse 后不久,对 JSON 的支持就添加到 clickhouse-go 客户端中。此支持现已在 2.0 版的 Grafana 插件中实现。
JSON 叶子节点现在可以像任何其他等效基本类型的列一样用于图表。表示 JSON 对象或 JSON 对象列表的列分别像 ClickHouse 中的元组和嵌套类型一样处理。在 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 字段下,因此每一行如下所示。
{"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 日志现在可以在探索视图或日志面板中呈现。我们从响应状态中推断出一个“级别”字段,该字段控制颜色渲染。我们建议对所有查询施加 LIMIT 以避免超时,因为 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✎
日志面板和探索视图都对字段名称敏感。为了使用探索的日志可视化模式(包括直方图)显示结果,行必须包含并按“log_time”字段排序。
JSON 支持需要重写我们如何在插件中处理 ClickHouse 类型的方式。现在覆盖范围应该很全面,所有类型都支持,包括我们需要作为 JSON 工作的一部分支持的复杂类型,例如 Tuple 和 Nested。
变量的更改
从历史上看,我们依赖 AST 自动优化 WHERE 子句中的 IN 条件,以应对用户为变量选择“全部”的情况。插件将有效地从任何 WHERE 子句中删除变量的使用,从而避免发送条件的需要。虽然这是一个不错的功能,但这依赖于 AST 并假设完全理解 ClickHouse SQL 方言。在 2.0 版本中,我们将此优化延迟到用户,用户应使用宏 __conditionalAll 包装使用变量的 IN 子句。
例如,假设我们正在可视化伦敦的房价,并希望允许用户通过选定的区域进行过滤,例如通过 district IN (${district:singlequote})
我们的变量过滤器如下所示 - 请注意选择“全部”选项的功能
如果选择“全部”,则查询变为
虽然对于只有几个值的变量(例如伦敦的区域)来说很好,但对于较长的列表来说,它会成为性能开销。为了优化,用户可以使用 `__conditionalAll` 围绕子句,例如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✎
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 实现。我们始终重视您的反馈,并鼓励用户提出问题,以便我们能够继续改进插件。