🤖 追踪你的 AI 应用。使用我们的免费模板.

2025年4月16日

从五年运营大型 ClickHouse® 集群中获得的经验教训:第二部分

这是该系列的第二部分。这里有更多我从过去 5 年多运营 petabyte 级规模的 ClickHouse 集群中学到的东西。

ClickHouse

Javier Santana 联合创始人

内容概要:

这是本系列的第二部分。您可以在此处阅读第一部分。

处理负载

本节主要讨论读取 (reads)。我在上一篇文章中讨论了数据摄取 (ingestion),虽然读写可以使用相同的资源,但我将单独关注读取,就好像您只有读取一样。

好吧,我说谎了。因为我首先要告诉你:你不能将读写分离。 如果您看到任何只提供读取性能的基准测试 (benchmark),它在基准测试中可能看起来不错,但这在现实生活中并非如此。

读取在很大程度上取决于一个表有多少个 part,而 part 的数量取决于数据摄取。如果您经常插入数据,那么无论您的表有什么样的 schema(更多关于性能的信息请参见下文),您在读取时都会受到影响。您可以通过更频繁地运行 merge 来减少 part 的数量,但您需要更多的 CPU。

说到读取,我可以涵盖很多主题。 我将从这里开始:

  1. 处理不同类型的流量
  2. 硬件资源
  3. 查询设计

关于处理不同类型的流量和硬件

在一个集群中,您通常有:

解决这个问题是一个永无止境的设计主题:我如何才能不让长时间运行的查询影响实时查询的延迟,尤其是 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。当然,这并非适用于每一种机器或工作负载,但这是一个好的经验法则。 另一个重要的设置是那些控制内存的设置:

两者都控制内存的使用方式,您需要控制您给每个查询多少内存。 如果您想运行大量的 group by,join,窗口函数或类似的东西,您希望 max_memory 很高。 如果您希望这些查询运行得很快,max_bytes_before_external_group_by 应该很高,以避免 ClickHouse 将部分结果转储到磁盘并降低性能。

ClickHouse 没有优化器; 你就是优化器。 您的用户不在乎优化(该 feature 或报告需要尽快完成)。 查询优化是一门艺术,但您必须遵循一些简单的规则:

  1. 首先在排序键中的列上进行过滤。 排序键设计也超级重要,所以请注意它。 相信我,由于糟糕的排序键设计,您的查询会损失 10-100 倍的性能。 花费 2-3 小时了解数据布局。
  2. 其次运行其他过滤器,如果您的列很大,请尝试使用 PREWHERE。 将高选择性过滤器移动到小型列(不是 Strings,不是 Arrays)上的 prewhere。
  3. 然后运行所有过滤 掉 数据的操作,主要是 IN 操作
  4. 将 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)之外,您还需要跟踪以下内容:

有时事情会卡住,并且杀死查询并不总是有效,所以请密切关注一切。

您还应该跟踪错误。 以下是重要的错误:

您应该学会使用 ClickHouse 系统表。 您可以添加大量的 metrics 和所有内容,但是当事情变得糟糕时,这将拯救您。

您可能想要启用其他一些,但这些是关键的。

还要跟踪 segfault。 它们可能会发生,您需要确定是哪个查询导致了它。 这不会经常发生,如果您的工作负载是稳定的,我认为您永远不会看到一个。 但是,如果它们发生了,并且您没有确定导致它的查询,您的集群将会一直崩溃。 并且您不想一直随叫随到。

我不会过多地讨论工具,有些人使用带有 Altinity operator 的 K8s,另一些人使用脚本来处理它,但每个人都需要:

等等。我们在 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 很难; 如果您有足够的经验,您就会知道这一点。

无论如何,我希望这能为您节省一些麻烦。 (如果您没有阅读第一篇文章,这里再次提供。)