在这篇客座博文中,来自 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)
函数将数据聚合到 1 分钟间隔中。物化视图中的每一行都包含几个原始数据值,所有这些值都根据 observed_at
列对应于特定的一分钟。
通过创建具有不同间隔(例如 6 分钟、40 分钟和 3 小时)的物化视图,我们可以进一步提高查询性能,并在各种时间范围内实现更有效的数据分析。
我们从物化视图中排除所有包含唯一数据的列,例如 trace_id。当我们需要按 trace_id 过滤时,我们会查询原始数据。值得注意的是,原始数据查询在 ClickHouse 中相当快,因为 ClickHouse 可以使用每列的相应索引有效地实时筛选大量行。
在 ClickHouse 中分组数据
在我们的分析中,我们主要使用 GROUP BY 查询来获取数据。虽然标准查询和物化视图查询使用相同的 SQL 语法,但在处理计数的方式上略有不同。物化视图查询不使用 count()
来计数行,而是使用派生列 calls
的 sum(calls)
。列 calls
是通过在物化视图定义中使用 count()
函数并将其别名为 calls
创建的。这种更改使我们能够从物化视图中聚合行的计数,从而实现更有效的查询和更好的性能。
理解 ClickHouse 中的合并和状态后缀
在 ClickHouse 中,-State
和 -Merge
后缀与聚合函数一起使用,用于处理中间聚合状态并有效地合并它们。这些后缀在处理分布式数据库或需要组合来自多个来源的聚合结果时特别有用。
状态后缀:带有状态后缀的聚合函数返回中间聚合状态,而不是最终结果。这些中间状态以紧凑的二进制格式存储,表示正在进行的聚合。
例如,如果我们想要计算名为 server_process_time
的列的第 95 个百分位数 (0.95) 和第 99 个百分位数 (0.99),我们可以使用 quantilesState(value)
函数。此函数返回分位数计算的中间状态,我们可以将其存储在单独的表中或与其他中间状态组合。我们在物化视图中利用这些状态函数,将其输出存储在汇总表中。
合并后缀:带有合并后缀的聚合函数获取其相应的 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 与物化视图结合使用,已被证明是一个令人印象深刻的解决方案。我们强烈推荐它用于寻求高性能分析解决方案的组织。