在这篇客座博文中,来自 Inigo 的高级软件工程师 Ruslan Shcherbin 分享了他探索各种数据库替代方案的经验,并最终选择 ClickHouse 与物化视图相结合来处理他们的高性能分析需求。
Inigo 是一家 GraphQL API 管理行业的先驱公司,专门从事大规模的全面安全和治理。他们的平台通过提供即时可见性、安全性、访问控制、合规性和持续交付,使 API 和平台团队能够完全控制、管理和保护 GraphQL 部署。除了安全功能外,Inigo 还提供关键的开发者体验增强和工作流程效率,最终使组织能够加速 GraphQL API 开发,降低成本并改善最终用户体验。
概述
在 Inigo,我们正在寻找一种数据库解决方案,可以处理大量用于分析的原始数据。在探索了包括 SQLite、Snowflake 和 PostgreSQL 在内的各种替代方案之后,我们决定在一台相对较慢的机器上使用 ClickHouse 实验物化视图。在这篇文章中,我们分享了加载数十亿行数据并进行测量后的发现。
我们在 Inigo 处理和分析数十亿个 GraphQL API 请求。实时的传入 API 数据使我们能够创建关于高基数数据的聚合视图,生成警报,并基于广泛的数据创建仪表板。
大规模分析的数据库替代方案
在选择 ClickHouse 之前,我们尝试了其他几个数据库
- Snowflake - 对于我们的需求来说太慢且成本太高。虽然它在处理内部数据方面表现良好,但在产品中处理实时客户数据时,它变得非常昂贵,这对产品的单位经济效益产生了负面影响。缺少本地 Snowflake 模拟器进一步使开发复杂化,因为它阻止了针对数据库运行连续单元测试。此外,由于 Snowflake UI 仪表板更侧重于对单个表的查询,因此用户界面与我们首选的数据库使用方式不符。由于每个服务都有自己独立的表,我们不得不在 UI 中获取并硬编码表名。这使得无法在用户界面中使用单个查询从所有表中检索数据。
- PostgreSQL - 一个出色的事务数据库,但不适用于大型分析工作负载。我们能够让它处理大约 10 万行数据,但超过这个数量后,索引变得失控,并且运行 PostgreSQL 集群的成本没有意义。一旦我们达到 10 万 - 100 万行标记,性能就会显着下降。但是,我们承认这种比较可能是不公平的,因为 PostgreSQL 的 OLTP 性质并非非常适合此用例。
为什么选择 ClickHouse?
我们选择 ClickHouse 有几个原因。ClickHouse 是一个列式数据库,专门设计用于处理大量数据,同时提供快速的查询执行。ClickHouse 中的物化视图允许数据集进行预聚合,这可以显着提高分析查询的性能。此外,ClickHouse 能够自动管理和更新物化视图,随着新数据的摄取,简化了维护过程,使其成为那些寻求高效且强大的分析数据库解决方案的人的有吸引力的选择。
ClickHouse 的开源性质以及在我们笔记本电脑上以简单二进制文件或 docker 运行它的能力是主要的吸引力。本地测试的便利性,加上其可扩展性,使我们相信 ClickHouse 是满足我们需求的理想选择。
Inigo 的 ClickHouse 设置
我们当前的 ClickHouse 设置是一个单一数据库,几乎每个 Inigo 服务都有自己的表,其中一些表包含数十亿行数据。它在开发环境中运行在 4 个 vCPU、32GB RAM 上,在生产环境中运行在 16 个 vCPU、128GB RAM 上。
设置 ClickHouse 具有挑战性,因为理解每个索引、分片和物化视图的细微之处需要相当大的努力。然而,最终的结果是一个系统,能够以可扩展的方式加载和查询大量的分析数据,无论是在硬件成本还是工程开发时间方面。
ClickHouse 中的物化视图
鉴于我们在一些原始表中拥有数十亿行数据,我们力求尽可能减少连接。相反,我们将数据整合到几个物化视图中,每个物化视图都将数据分组到不同的时间窗口桶中。因此,大多数时候,我们查询的是数千行聚合数据,而不是数百万行原始数据,从而实现更快的实时过滤。
在 ClickHouse 中创建物化视图
我们的原始表都包含一个时间戳列 observed_at
。
然后,我们将数据行聚合到物化表的指定间隔中。例如,如果间隔为 1 分钟,我们使用 toStartOfInterval(observed_at, INTERVAL 1 minute)
函数将数据聚合到一分钟间隔中。物化视图中的每一行都包含几个原始数据值,所有这些值都对应于基于 observed_at
列的特定分钟。
通过创建具有不同间隔的物化视图,例如 6 分钟、40 分钟和 3 小时,我们可以进一步提高查询性能,并实现跨各种时间范围的更有效的数据分析。
我们从物化视图中排除所有包含唯一数据的列,例如 trace_id。当我们需要按 trace_id 过滤时,我们查询原始数据。值得注意的是,原始数据查询在 ClickHouse 中相当快,因为 ClickHouse 可以使用每个列的适当索引实时有效地筛选大量行。
在 ClickHouse 中分组数据
在我们的分析中,我们主要使用 GROUP BY 查询来获取数据。虽然标准查询和物化视图查询使用相同的 SQL 语法,但它们在处理计数的方式上略有不同。物化视图查询不是使用 count()
来计数行,而是使用 sum(calls)
和派生列 calls
。列 calls
是通过在物化视图定义中使用别名为 calls
的函数 count()
创建的。这种更改使我们能够聚合来自物化视图的行数,从而实现更高效的查询和更好的性能。
理解 ClickHouse 中的 Merge 和 State 后缀
在 ClickHouse 中,-State
和 -Merge
后缀与聚合函数一起使用,以处理中间聚合状态并有效地合并它们。当使用分布式数据库或需要组合来自多个来源的聚合结果时,这些后缀特别有用。
State 后缀:带有 State 后缀的聚合函数返回中间聚合状态,而不是最终结果。这些中间状态以紧凑的二进制格式存储,表示正在进行的聚合。
例如,如果我们想计算名为 server_process_time
的列的第 95 个 (0.95) 和第 99 个 (0.99) 百分位数,我们可以使用 quantilesState(value)
函数。此函数返回分位数计算的中间状态,我们可以将其存储在单独的表中或与其他中间状态组合。我们在物化视图中使用了这些状态函数,并将它们的输出存储在汇总表中。
Merge 后缀:带有 Merge 后缀的聚合函数获取由其对应的 State 函数生成的中间聚合状态,并将它们合并以生成最终聚合结果。当我们有多个需要组合成单个聚合结果的中间状态时,这非常有用。
例如,要获得聚合的 server_process_time
的第 99 个百分位数,我们将使用 quantileMerge(0.99)(quantiles_server_process_time)
,其中 quantiles_server_process_time
是由物化视图中使用的 quantilesState
函数产生的中间状态。我们在下面展示一个完整的例子
--- receiving table for the inserts
CREATE TABLE query_data
(
`observed_at` DateTime64(3),
`sidecar_process_time` Float64,
`server_process_time` Float64
)
ENGINE=MergeTree()
ORDER BY (observed_at);
CREATE MATERIALIZED VIEW query_data_mv
ENGINE = AggregatingMergeTree()
ORDER BY (observed_at)
AS SELECT
toDateTime64(toStartOfInterval(observed_at, INTERVAL 1 MINUTE), 3) as observed_at,
quantilesState(0.95, 0.99)(sidecar_process_time) as quantiles_sidecar_process_time,
quantilesState(0.95, 0.99)(server_process_time) as quantiles_server_process_time
FROM query_data
GROUP BY observed_at;
INSERT INTO query_data (observed_at, sidecar_process_time, server_process_time) VALUES (toDateTime('2023-06-15 19:09:01'), 0, 0), (toDateTime('2023-06-15 19:09:09'), 100, 1000) (toDateTime('2023-06-15 19:19:01'), 0, 0), (toDateTime('2023-06-15 19:19:09'), 100, 1000);
--we query the materialized views underlying table with merge functions
SELECT
observed_at,
quantileMerge(0.95)(quantiles_sidecar_process_time) as p95_sidecar_process_time,
quantileMerge(0.99)(quantiles_sidecar_process_time) as p99_sidecar_process_time,
quantileMerge(0.95)(quantiles_server_process_time) as p95_server_process_time,
quantileMerge(0.99)(quantiles_server_process_time) as p99_server_process_time
FROM query_data_mv
GROUP BY observed_at;
虽然我们仍在努力重建大型物化视图,但我们相信我们正走在通往有希望的解决方案的道路上。
使用 ClickHouse 和物化视图的结果和好处
- 我们在应用程序的几乎所有方面都使用了物化视图查询,除了一个包含原始数据的表和任何涉及按唯一字段过滤的查询。
- 在必要时切换到使用原始数据非常简单。我们在查询中使用了相同的 SQL 文件,并对物化视图进行了少量修改。
- 数据大小存在显着差异。例如,原始数据最多覆盖一周,而物化视图可以容纳长达一个月的数据:
- 由于 MV 表的尺寸较小,数据库负载更低,整个应用程序运行更快
- 我们不仅可以使用 SQL 文件编写迁移,还可以使用 Go 编写迁移,这对我们来说更方便,因为我们经常同时迁移多个表。详细示例请点击此处。
- ClickHouse 的强类型简化了我们的迁移逻辑,而这种逻辑在历史上需要在 NoSQL 解决方案中允许较弱的类型保证。
- 测试驱动开发 (TDD) 的便利性通过我们独立的集成测试的速度得到增强。
在使用物化视图时,无论是使用独立实例还是在集群环境中,我们都没有遇到任何问题。
使用 ClickHouse 和物化视图的性能结果
- 现在数据库比我们的 Golang 代码更快,这使我们能够使用连接池并提高 UI 响应速度。
- 对查询速度的信心使我们能够识别数据库之外的问题,例如在追踪扩展中。
使用 ClickHouse Cloud
此解决方案的可扩展性取决于多种因素,其中最重要的因素之一是扩展架构的设置。ClickHouse 提供了一项云服务,具有 720 GiB RAM、180 个 vCPU 和 10 TB 的数据,表明其具有处理大量工作负载的潜力。我们已经能够轻松扩展到 10 亿行,并且这个数字还在继续增长。
展望未来,我们对 ClickHouse Cloud 产品感到兴奋。尽管我们早在该产品可用之前就开始使用 ClickHouse,并花费了大量时间来配置和管理分片和磁盘,但我们预计会将我们的一些自托管工作负载迁移到云端。
结论
ClickHouse 一直是我们大规模分析需求的绝佳选择。即使它具有某些复杂性,例如缺少事务,但这些复杂性被它的优点大大掩盖了。速度、处理大量数据的能力以及物化视图等功能使 ClickHouse 成为强大的分析工具。总的来说,ClickHouse 与物化视图结合使用已被证明是一个令人印象深刻的解决方案。我们强烈建议将其用于寻求高性能分析解决方案的组织。