🤖 追踪你的 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. JOINGROUP 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 很难; 如果您有足够的经验,您就会知道这一点。

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