操作 Petabyte 级 ClickHouse 集群的经验教训:第二部分
🤖 追踪你的 AI 应用。使用我们的免费模板.
2025年4月16日
从五年运营大型 ClickHouse® 集群中获得的经验教训:第二部分
这是该系列的第二部分。这里有更多我从过去 5 年多运营 petabyte 级规模的 ClickHouse 集群中学到的东西。
Javier Santana
联合创始人
内容概要:
- 处理负载
- 回填 (Backfills)
- 集群运营和监控
- 其他
- 最后的想法
这是本系列的第二部分。您可以在此处阅读第一部分。
处理负载
本节主要讨论读取 (reads)。我在上一篇文章中讨论了数据摄取 (ingestion),虽然读写可以使用相同的资源,但我将单独关注读取,就好像您只有读取一样。
好吧,我说谎了。因为我首先要告诉你:你不能将读写分离。 如果您看到任何只提供读取性能的基准测试 (benchmark),它在基准测试中可能看起来不错,但这在现实生活中并非如此。
读取在很大程度上取决于一个表有多少个 part,而 part 的数量取决于数据摄取。如果您经常插入数据,那么无论您的表有什么样的 schema(更多关于性能的信息请参见下文),您在读取时都会受到影响。您可以通过更频繁地运行 merge 来减少 part 的数量,但您需要更多的 CPU。
说到读取,我可以涵盖很多主题。 我将从这里开始:
- 处理不同类型的流量
- 硬件资源
- 查询设计
关于处理不同类型的流量和硬件
在一个集群中,您通常有:
- 实时流量 (Real-time traffic),也称为需要在少于 X 秒内响应的查询,通常服务于 dashboard 或一些实时工作负载。
- 长时间运行的查询 (Long-running queries),这可能来自临时分析 (ad hoc analysis)(比如有人决定需要知道 7 年数据的平均值)
- 回填 (Backfills)(这需要单独的一节)。
- 任何非实时的东西 (Anything else that's not real time),也就是说,查询运行多长时间并不重要。
解决这个问题是一个永无止境的设计主题:我如何才能不让长时间运行的查询影响实时查询的延迟,尤其是 p95 以上的那些查询?有很多方法可以解决这个问题,例如,为不同的工作负载使用不同的 replica。但这很昂贵,因为您需要为它们都分配硬件,而且虽然实时流量通常是可预测的,但一次性查询只是“发生”,您无法提前计划。您可以更聪明地在应用程序层处理这个问题(在发送查询之前),但您可能有 3-4 个不同的查询来源:应用程序 (app),运行 clickhouse-client 的人,运行 BI 的人,以及运行 BI 的人。是的,我算了两次,因为 BI 查询通常真的很糟糕。
您可以创建 spot replica 来运行这些长时间运行的查询,但这需要额外的处理并增加复杂性。在 Tinybird,我们通过应用程序逻辑和 load balancer 的组合来处理它。我们知道 replica 的状态,并在此基础上选择正确的 replica 来发送查询。 有时我们会拒绝查询以避免服务器崩溃。
关于查询设计
查询有无限的配置选项,但其中一个最重要的选项是 max_threads
。它控制您可以使用多少个线程来读取数据。一般来说,实时查询应该只使用 1 个,如果您需要超过 1-2 个,如果您有很多 QPS,您将需要大量的硬件。您需要理解,大多数时候,您的瓶颈是扫描速度。所以您可以不断地添加 CPU,但您仍然受到扫描大小的限制。一个经验法则是:在一台机器上,平均可以扫描 500Mb/s。当然,这并非适用于每一种机器或工作负载,但这是一个好的经验法则。 另一个重要的设置是那些控制内存的设置:
max_memory
max_bytes_before_external_group_by
两者都控制内存的使用方式,您需要控制您给每个查询多少内存。 如果您想运行大量的 group by,join,窗口函数或类似的东西,您希望 max_memory
很高。 如果您希望这些查询运行得很快,max_bytes_before_external_group_by
应该很高,以避免 ClickHouse 将部分结果转储到磁盘并降低性能。
ClickHouse 没有优化器; 你就是优化器。 您的用户不在乎优化(该 feature 或报告需要尽快完成)。 查询优化是一门艺术,但您必须遵循一些简单的规则:
- 首先在排序键中的列上进行过滤。 排序键设计也超级重要,所以请注意它。 相信我,由于糟糕的排序键设计,您的查询会损失 10-100 倍的性能。 花费 2-3 小时了解数据布局。
- 其次运行其他过滤器,如果您的列很大,请尝试使用
PREWHERE
。 将高选择性过滤器移动到小型列(不是 Strings,不是 Arrays)上的 prewhere。 - 然后运行所有过滤 掉 数据的操作,主要是
IN
操作 - 将
JOIN
、GROUP BY
和其他复杂的操作留到最后。
如果您掌握了这些规则,我可以保证您将成为 ClickHouse 用户中的前 p95。 实际上,可以使用任何 LLM,只需给它这些规则和表 schema,它就会做得很好。 顺便说一句,我们在这里写了 更多关于这些事情的信息。
事情并非那么简单,正如您可能预料的那样,否则查询规划器将只有几行代码(但它们不是)。 根据基数等,您可能需要更改规则。
所以这就是 Query Design 101,是的,它实际上更多的是关于用户而不是 operator,但您仍然需要了解这些事情,因为您将一直在与运行糟糕查询的用户作斗争。 因为即使 ClickHouse 也有一个最大总内存配置,并且会 OOM。 OOM 是不好的; 根据您的 database,可能需要几分钟才能加载(因此您还需要正确设计您的 HA 设置)
您要设置的另一个设置是 max_concurrent_queries
。 当服务器过载时,这可以挽救您,所以不要删除此值。 您可能需要根据您的工作负载更改它,但保留它,它是一个救星。
开始使用 Tinybird 构建 如果您读到这里,您可能想使用 Tinybird 作为您的分析后端。 免费开始,没有时间限制。 注册
回填 (Backfills)
我添加这部分是因为它非常痛苦,如果我设法为哪怕 1 个人节省 1 小时的痛苦,这篇文章就值得了。
让我解释一下这个场景:您正在插入到一个表中,该表有一个 materialized view,您添加了另一个 materialized view,并且您想要回填它。这听起来很简单,但事实并非如此。
您可能想使用 POPULATE
... 不要。 它已损坏,因为数据可能会重复。 来自官方文档:
我们不建议使用 POPULATE,因为在视图创建期间插入到表中的数据将不会插入到视图中。
如果您运行 INSERT INTO SELECT * FROM table
,您可能会丢失或重复数据,并且您需要大量的内存才能做到这一点。
所以,在回填 materialized views 时,这里该怎么做:在实际表之前使用 Null 表,并增加您每个 block 推送的行数,以避免生成数千个 part。
我不能错过解释如何在 Tinybird 中做到这一点的机会。 只需更改 MV SQL,然后 tb deploy
,我们将处理所有这些。
集群运营和监控
除了您监控的常规内容(CPU、内存、IO)之外,您还需要跟踪以下内容:
- 正在运行的查询数量
- ZooKeeper 延迟
- S3 错误(如果您使用它)
- 复制延迟 (Replication lag)
- DDL 队列长度(这是存储所有 replica 需要执行的操作的队列)
- Merge 队列
- Merge 内存
- Mutation
有时事情会卡住,并且杀死查询并不总是有效,所以请密切关注一切。
您还应该跟踪错误。 以下是重要的错误:
- 只读表(当您遇到此错误时,您很可能完蛋了)
- ZooKeeper 错误
- 最大并发查询数。
您应该学会使用 ClickHouse 系统表。 您可以添加大量的 metrics 和所有内容,但是当事情变得糟糕时,这将拯救您。
system.query_log
是您的圣经。 这会告诉您关于查询的所有信息。ProfileEvents
列是您了解查询执行了什么的地方。 学习如何使用它,掌握它,并学习列名。system.processes
告诉您当前正在运行的内容。 在发生紧急情况时很有用。system.part_log
告诉您 part 如何移动、合并了什么等等。- 您需要了解
system.tables
和system.columns
才能了解表形状。
您可能想要启用其他一些,但这些是关键的。
还要跟踪 segfault。 它们可能会发生,您需要确定是哪个查询导致了它。 这不会经常发生,如果您的工作负载是稳定的,我认为您永远不会看到一个。 但是,如果它们发生了,并且您没有确定导致它的查询,您的集群将会一直崩溃。 并且您不想一直随叫随到。
我不会过多地讨论工具,有些人使用带有 Altinity operator 的 K8s,另一些人使用脚本来处理它,但每个人都需要:
- 注意向集群添加和删除 replica 节点
- 小心删除表 replica
- 添加新 replica 时,请注意复制延迟
- 关闭 replica 时,请注意长时间运行的查询
- 观看插入 (insert)
等等。我们在 Tinybird 采用多步流程来关闭一个 replica,该流程可以处理所有这些(首先删除流量,然后等待插入和查询,如果需要,则杀死它们)。
小心处理 ON CLUSTER
操作。 如您所知,更改集群中的表需要一些协调。 为此,ClickHouse 使用 Zookeeper 或 ClickHouse Keeper 来协调 replica。 如果一个 replica 宕机,ON CLUSTER
操作将花费大量时间(取决于配置),并且如果未正确设置超时,可能会在您的应用程序中产生问题(相信我,它们没有)。
对于超低延迟用例(每个查询低于 20 毫秒),您需要预热缓存(查看设置),因为如果您开始将流量发送到 replica,延迟会疯狂上升。 观察 mark_cache
可用的内存。 还有一个未压缩的缓存,但是,说实话,我们从未成功使用过它,所以如果您使用过,请告诉我。
总的来说,在 max_simultaneous_queries
、连接问题和队列不受控制地增长时发出警报。 您将在其他事情上添加更多警报,但这在很大程度上取决于您的集群的设计目的。
最后说明:Altinity 的知识库 (和视频)可能是了解如何自行设置 ClickHouse 的最佳资源之一(总的来说,Altinity 发布的所有内容都 非常 好。如果您处理 ClickHouse,则必须阅读)。
其他
您可能会发现有用的其他随机内容:
表删除
默认情况下,ClickHouse 不会删除任何超过 50 GB 的表。 这完全有道理,您可能会想将限制降低到几 Mb。 有两种类型的开发人员/数据工程师:1) 那些错误地删除了表的人,以及 2) 将要删除表的人。 如果您错误地删除了表,您需要转到备份,这很麻烦(除非您使用我们开发的 引擎,当然)
物化视图 (Materialized views)
MV 是一个杀手级 feature,但是它们很难管理,它们会导致内存问题,并且它们会生成大量的 part。 总的来说,您需要小心 MV,因为人们倾向于在其中添加很多东西。 MV 在插入时执行,所以每个新的 MV 都会使您的数据摄取变慢。
不要杀死你的服务器
某些列类型可能会导致您的服务器崩溃,例如,在具有高基数(+2 亿)的列上使用 uniqExactState
可能会在 merge 时杀死您的集群。 这通常发生在聚合列上,所以要小心它们。
如果 index_granularity
太低,也会杀死您的服务器。 我的建议:不要低于 128。 低值对于点查询很有用。
最后的想法
我花了很多精力关注 ClickHouse 可能会出错的事情,但这并不意味着 ClickHouse 是一个糟糕的 database - 恰恰相反。 大规模处理 database 很难; 如果您有足够的经验,您就会知道这一点。
无论如何,我希望这能为您节省一些麻烦。 (如果您没有阅读第一篇文章,这里再次提供。)