当前位置: 首页 > news >正文

ClickHouse vs Prometheus: 高基数挑战,ClickHouse如何化解?(第2篇)

本文字数:15840;估计阅读时间:40分钟

作者:Tom Schreiber and Lionel Palacin

在上一篇文章中,我们探讨了高基数(high cardinality)为何会给 Prometheus 和其他面向序列(series-oriented)的时序数据库带来挑战。我们分析了它们的底层存储模型——即每个唯一的标签组合都会创建一个独立的时间序列——如何随着维度(dimensionality)和数据更新频率(churn)的增加,导致内存开销、写入放大、操作复杂性以及查询时间上的权衡。

您会经常听到我们提到,当 ClickHouse 用于可观测性(observability)工作负载时,它不会受到高基数的显著影响。这对于我们通常所指的工作负载确实如此,但仍需加以说明。ClickHouse 仍然会为基数付出代价,这主要体现在查询时,而非数据摄取(ingestion)阶段。

在本文中,我们将深入探讨 ClickHouse 以及更广泛的列式数据库,为何在处理高基数可观测性数据方面,与 Prometheus 等系统存在如此显著的差异。

同时,我们也会阐述该模型相较于 Prometheus 的不足之处。ClickHouse 并非 Prometheus 的即插即用替代品,其应用需要我们以不同的视角来思考遥测(telemetry)、数据埋点(instrumentation)和数据聚合(aggregation)。

ClickHouse 中基数为何不同

为了理解高基数在 ClickHouse 中为何有不同的表现,我们首先需要重新思考可观测性数据的建模方式。ClickHouse 提倡一种转变:不再将遥测数据视作数百万个独立维护的时间序列,而是将其表示为表中的行,这些行包含可在查询时进行聚合的属性(attributes)和测量值(measurements)。

这种方法常被称为“宽事件(wide events)”模式,并由 Charity Majors 等实践者推广开来(https://charity.wtf/2022/08/15/live-your-best-life-with-structured-events/)。如果有助于理解,您可以简单地将其视为附加了指标的日志。接下来,我们首先探讨它与传统的 Prometheus 风格指标模型有何不同。

以宽事件形式表示指标

需要强调的是,我们并非旨在 ClickHouse 中直接建模 Prometheus 的指标、指标类型或 PromQL 语义,尽管相关工作正在进行(https://github.com/ClickHouse/ClickHouse/pull/102246),而且这种模型无疑具有其优势,我们将在后续探讨。相反,宽事件(wide events)方法推动了从传统 Prometheus 指标模型向可观测性数据的事件驱动型模型(event-style model)的更广泛转变。

在此模型中,遥测数据被表示为包含维度、属性和数值测量的带时间戳的事件,而非独立维护的时间序列。每个事件都包含上下文属性以及一个或多个数值测量,而不是将每个指标作为拥有独立身份和生命周期的单独序列进行存储。查询在读取时动态地将这些事件聚合成所需的视图、速率、摘要或时间桶。

重要的是,这通常是一种更宽松、更自然地生成遥测数据的方式。应用程序无需预先定义严格的指标 schema,只需发出带有数值的结构化事件。在 ClickHouse 可观测性部署中,一种常见模式是利用宽事件 schema 例如 otel\_logs(https://clickhouse.com/docs/use-cases/observability/clickstack/ingesting-data/schemas) 将指标数据直接与日志一同存储。

例如,服务可能不再为响应时间、请求计数、状态码和主机分别定义多个独立指标,而是简单地发出以下事件:

logger.Info("request completed", zap.String("host", "host-42"), zap.String("application", "checkout-service"), zap.String("request_path", "/api/payments"), zap.Uint16("status", responseStatus), zap.Uint64("response_time", responseTime), zap.Uint64("size", responseSize), )

采用宽事件模型后,请求本身就成为遥测数据的自然单位。所有上下文维度和测量都随附于同一事件。而在 Prometheus 风格的模型中,相同的信息通常会被分解到多个指标定义、计数器、仪表、直方图以及标签组合中。一旦这些信息被归并为聚合数据,单个信号就会丢失且无法恢复。宽事件则颠覆了这一模式:聚合数据是从原始事件派生而来,而非取而代之,因此图表上的任何峰值都能追溯到产生它的具体请求。

列式存储

在采用宽事件模型时,我们最初可能会直觉性地借鉴传统分析工作负载的经验,将每个标签建模为一个专用列。尽管这种做法对于稳定的 schema 能够良好运行,但在可观测性领域却鲜有实用性,并且也不符合宽事件 schema 的本质。标签通常是动态的、高基数的且不可预测的,这使得僵硬的预先定义 schema 难以维护,并且与真实的遥测数据格格不入。

CREATE TABLE metrics ( `time` DateTime CODEC(Delta(4), ZSTD(3)), `host` LowCardinality(String), `application` LowCardinality(String), `request_path` String, `remote_addr` IPv4, `remote_user` LowCardinality(String), `request_type` LowCardinality(String), `request_protocol` LowCardinality(String), `status` UInt16, `domain_referer` LowCardinality(String), `browser` LowCardinality(String), `device` LowCardinality(String), `response_time` UInt16, `size` Decimal(7, 1) ) ENGINE = MergeTree ORDER BY (host, toStartOfMinute(time), status, application, request_path, remote_addr)

上述 schema 尽管针对每个标签独立设列进行了优化,但在新指标频繁添加的高度动态环境中并不切实际。这不是一种推荐的宽事件 schema。

为处理这种动态结构,我们推荐 ClickHouse 的 Map(https://clickhouse.com/docs/sql-reference/data-types/map) 类型。

鉴于我们文档(https://clickhouse.com/docs/use-cases/observability/clickstack/ingesting-data/schema/map-vs-json)中已深入探讨的原因,我们建议在可观测性工作负载中使用 Map 类型而非 JSON 类型。

Map 类型将标签存储为键值对,其中键代表标签名,值代表标签值。它的主要限制在于所有键和值必须共享相同的类型,Map 的定义格式为 Map<String, type>。在实践中,这通常意味着值以字符串形式存储。尽管这种方式很灵活,但如果需要将值解释为数值或其他强类型数据,则可能需要在查询时进行类型转换。

从历史上看,Map 在读取时也存在一个重要缺点。读取单个标签需要读取整个 Map 结构及其所有相关标签,从而产生显著的 I/O 开销。然而,最近对 sharded maps(https://clickhouse.com/blog/clickhouse-release26-03#sharded-map) 的支持,通过允许更具选择性地访问 Map 内容,在很大程度上缓解了这一问题。

对于我们之前的示例,一个针对宽事件的表结构可能如下所示:

CREATE TABLE events ( `time` DateTime CODEC(Delta(4), ZSTD(3)), `labels` Map(LowCardinality(String), String), `response_time` UInt16 MATERIALIZED toUInt16(labels['response_time']), `host` LowCardinality(String) MATERIALIZED labels['host'], `status` LowCardinality(String) MATERIALIZED labels['status'], `application` LowCardinality(String) MATERIALIZED labels['application'], INDEX idx_labels_keys mapKeys(labels) TYPE text(tokenizer = array), INDEX idx_labels_vals mapValues(labels) TYPE text(tokenizer = array) ) ENGINE = MergeTree ORDER BY (host, toStartOfMinute(time), status, application) SETTINGS map_serialization_version = 'with_buckets', max_buckets_in_map = 32, map_buckets_strategy = 'sqrt';

请注意,此处的设置强制 Map 进行分片。更多详情请参见此处(https://clickhouse.com/docs/sql-reference/data-types/map#bucketed-map-serialization)。

原则上,此 Schema 与 ClickStack for OpenTelemetry logs(https://clickhouse.com/docs/use-cases/observability/clickstack/ingesting-data/schemas#logs) 所使用的 Schema 非常相似,尽管已大大简化。在 ClickStack 中,资源和范围属性有效地充当了动态标签 Map。

该 Schema 随后利用了以下讨论的相同技术,例如 Map 键和值上的文本索引。因此,ClickHouse 并非以“series objects”的视角来思考,而是简单地存储数据行,其中包含时间戳、一个分片 Map 的标签,以及用于指标值的常规数值列。

我们还将标签 Map 中的键物化(https://clickhouse.com/docs/use-cases/observability/clickstack/performance_tuning#materialize-frequently_queried-attributes),这些键预期会被频繁过滤或访问,以便它们能作为专用列存储在磁盘上。这种物化在数据写入时自动发生,使 ClickHouse 能够在查询时直接过滤、裁剪和读取这些维度,而无需访问标签 Map,从而减少了常见访问模式的 I/O (Input/Output)。

同样的方法也适用于频繁查询的数值。在上述示例中,response_time 从 Map 中物化为一个专用的类型化列,因为它预期会被定期聚合。这避免了在执行 avg(response_time) 或百分位计算等操作时重复访问 Map 以及在查询时进行类型转换。

值得注意的是,物化列也可以在数据摄入之后再添加。新插入的行将直接在磁盘上物理存储物化列,而旧行在查询时仍可从底层 Map 间接解析出相应的值。例如,如果我们后来决定 labels['region' ]已成为一个常用过滤器,我们可以添加:

ALTER TABLE events ADD COLUMN region LowCardinality(String) MATERIALIZED labels['region'];

新数据将立即受益于直接列访问、数据裁剪和优化的压缩,而历史行仍可通过 region 进行查询,且无需进行全表重写。

在 ClickHouse 内部,Map 被表示为键值对数组,并采用分桶序列化 (bucketed serialization) 机制,根据标签名的哈希值将键分布到多个独立的桶中。这意味着读取特定标签的查询只需访问包含该键的桶,而无需遍历整个 Map 结构。指标列本身是独立存储的,并与标签分开压缩。

每次插入操作都会首先使用默认的 Map 布局创建一个 Level 0 部分,其中键和值以平面数组形式存储,不进行分桶。后续的插入会以相同的格式创建更多的 Level 0 部分。由于这些部分通常较小,扫描完整的键和值数组的开销通常不大。在后台合并过程中,ClickHouse 会通过将键哈希到更小的独立桶中,将 Map 重写为分桶存储。当访问诸如 labels['status' ]这样的键时,ClickHouse 会计算键哈希,并仅读取相应的桶,而不是整个 Map,从而显著减少读取 I/O。实际上,根据 Map 的大小,这可以将单键查找性能提升 2 到 49 倍。

Prometheus 则采取了相反的做法。在 Prometheus 中,每个新的标签组合都会创建一个新的序列对象 (series object),包含其自身的元数据、数据块 (chunks) 和索引开销。而在 ClickHouse 中,添加新的标签值组合并不会创建新的结构对象或内存中的序列表示。它只是简单地添加一行,其中包含一组不同的标签值。行在插入时,会作为一个部分写入磁盘,并按目标表的主键排序——具体到本例中,排序依据是主机、以分钟为粒度的时间戳、状态和应用程序。在进行 ZSTD 压缩之前,可以对每列应用编解码器 (Codec),以最大限度地减少后续读取操作的 I/O 开销。

为了管理每张表的部分 (part) 数量,一个后台合并任务 (background merge job) 会定期将较小的部分合并成较大的部分,同时保留指定的排序顺序,直到它们达到可配置的压缩大小(通常约为 150 GB)。随着时间的推移,这个过程会形成一个合并后的部分的层次结构。

在执行插入操作时,ClickHouse 写入的是行,而不是序列对象。

列式存储的优势

即使标签本身是以 Map 形式存储,而非完全独立的列,表的列式存储特性及其排序机制依然能带来多项重要优势。

  • 通过数据剪枝实现快速过滤 - 在表的排序键上构建稀疏索引,该排序键在此场景下由根据标签映射 (label map) 具象化 (materialized) 出的常用过滤维度组成,紧接着是时间戳。在上述示例中,host、status 和 application 等标签被具象化为专用列,并包含在排序键中。数据以数据分块 (parts) 的形式存储,每个 part 划分为大约 8,000 行的 granule。对于每个 granule,ClickHouse 记录排序键列的第一个值,而非索引每一行,因此该索引被视为稀疏索引。

    由于数据首先按常用查询维度,然后按时间进行物理排序,具有相似值的行在磁盘上自然地聚合在一起。当查询根据 host、application、status 或时间范围等常用标签进行过滤时,ClickHouse 能够高效地对 granule 进行剪枝。它不必扫描整个数据集,而是可以跳过大量不相关数据,仅读取可能包含匹配行的 granule,从而显著减少 I/O 并提高查询性能。在 mapKeys(labels) 和 mapValues(labels) 上建立的二级索引进一步完善了这一机制。这些倒排索引使得 ClickHouse 即使在过滤那些未具象化为专用列的标签时,也能对 granule 进行剪枝,从而有助于为高度动态的维度保留灵活性。

  • 压缩 - 即使标签存储在 Map 结构中,ClickHouse 仍能为可观测性工作负载实现出色的数据压缩。数据根据主机 (host)、状态 (status) 和应用程序 (application) 等常用过滤维度进行物理排序,随后按时间排序。这种排序方式自然地将拥有相似标签集、指标值和重复字符串的行聚合在一起,形成了强大的数据局部性,压缩算法可以高效地利用这一点。将频繁查询的标签物化为独立列,能进一步提升压缩效果。由于具有相同值的行在磁盘上连续存储,这使得字典编码 (dictionary encoding) 和压缩编解码器 (compression codecs) 的效率显著提高。

    已物化的指标列,例如上述示例中的 response_time,仍然保持完全的列式存储,并使用特定类型编解码器和 ZSTD 等通用压缩算法进行独立压缩。数值列还可以额外受益于 Delta 等编解码器(https://clickhouse.com/blog/optimize-clickhouse-codecs-compression-schema)或 Gorilla(https://www.vldb.org/pvldb/vol8/p1816-teller.pdf)。

    Map 结构本身也具备高效的压缩能力。标签键在行之间重复性极高,同时许多标签值在实际工作负载中表现出强大的时间局部性 (temporal locality)——在主键 (primary key) 中使用物化的 Map 标签有助于增强这种局部性。结合分片 Map 序列化 (sharded map serialization),ClickHouse 即使面对高度动态的标签集,也能保持强大的压缩特性。

  • 减少 I/O - 查询只需读取满足其需求的 Granule、列和 Map 桶。频繁用于过滤的标签和指标,例如 host、response_time 和 status,被物化到专用列中,这使得许多常见查询能够完全避免访问标签 Map。由于这些物化列是排序键 (sorting key) 的一部分,ClickHouse 可以在读取指标数据之前高效地剪枝 (prune) Granule。

    对于仅存储在分片 Map 中的标签,ClickHouse 只读取包含请求键的桶,而非整个标签结构。相较于早期的 Map 实现,这显著减少了 I/O 开销,同时保留了动态标签的灵活性。

    该 Schema 还为 mapKeys(labels) 和 mapValues(labels) 定义了倒排索引(https://clickhouse.com/docs/engines/table-engines/mergetree-family/textindexes)。这些索引允许 ClickHouse 在查询根据非物化标签进行过滤时,进一步剪枝 Granule,从而避免读取那些不包含所需标签名称或值的 Granule。这使得即使对于 container_id 或 pod_id 等高度动态的维度,也能实现高效过滤,而无需为每个标签都创建独立的列。

    由于指标列在磁盘上独立且连续存储,ClickHouse 还能高效地应用向量化查询执行 (vectorized query execution),将大批量值连续读入内存,从而实现高分析吞吐量。

在查询时,文本索引采用分层查找机制,通过稀疏索引、字典块和倒排列表,无需扫描整个数据集即可高效找到匹配的行。欲了解更多详情,请参阅我们的专题文章(https://clickhouse.com/blog/clickhouse-full-text-search-object-storage)。

布隆过滤器索引(https://clickhouse.com/docs/optimize/skipping-indexes#bloom-filter-types)也可用于此目的,并且对于许多工作负载而言可能已足够。实际上,最佳选择取决于标签的分布、查询模式以及所应用过滤器的选择性。

  • 分析型聚合 - 在此模型中,指标以数值列的形式表示,例如response_time。聚合操作因此变得简单高效。例如,如果我们要计算avg(response_time),ClickHouse只需读取符合查询条件的行中的response_time列。得益于稀疏索引和辅助过滤器,ClickHouse引擎能够识别出满足过滤条件的特定粒度范围,并仅从磁盘读取这些范围。这些范围可以在集群中的所有CPU核心和服务器上并行处理,从而在存储层实现有效的谓词下推。

    因此,聚合性能不再像基于时序的模型那样直接与基数挂钩。它与符合查询条件的行数更为密切。对于匹配的行,只需读取相关的指标列。在高基数列上施加过滤器通常是有益的,因为它们有助于减少需要扫描的行数。相反,没有过滤器或在低基数列上施加过滤器的查询通常需要读取更多数据,这将需要并行读取才能保证性能。

示例

考虑之前的metrics表。在我们的示例中,该表存储了应用程序的50亿个响应时间及大小数据。这些数据中的某些列自然具有高基数,在任何传统时序数据库(如Prometheus)中,您都会避免将它们用作标签。

SELECT count() FROM events ┌────count()─┐ │ 5339783200 │ -- 5.34 billion └────────────┘ 1 row in set. Elapsed: 0.004 sec.

下文展示了每列的基数,以及如果采用时序模型来表示这些数据时,总的基数情况。

SELECT uniq(host), uniq(application), uniq(labels['request_path']), uniq(labels['remote_addr']), uniq(labels['remote_user']), uniq(labels['request_type']), uniq(labels['request_protocol']), uniq(status), uniq(labels['domain_referer']), uniq(labels['browser']), uniq(labels['device']) FROM events FORMAT Vertical Row 1: ────── uniq(host): 100 uniq(application): 1000 uniq(arrayEl⋯est_path')): 881432 uniq(arrayEl⋯ote_addr')): 258115 uniq(arrayEl⋯ote_user')): 995890 uniq(arrayEl⋯est_type')): 5 uniq(arrayEl⋯protocol')): 7 uniq(status): 15 uniq(arrayEl⋯_referer')): 369 uniq(arrayEl⋯'browser')): 9 uniq(arrayEl⋯ 'device')): 11 SELECT uniq(host, application, labels['request_path'], labels['remote_addr'], labels['remote_user'], labels['request_type'], labels['request_protocol'], status, labels['domain_referer'], labels['browser'], labels['device']) AS total_time_series FROM events ┌─total_time_series─┐ │ 5239274309 │ -- 5.24 billion └───────────────────┘

显然,这里的时序数量非常庞大,几乎与行数相同,达到52亿。

行与时间序列是不同的概念。在不增加独立时间序列数量的前提下,你可以为现有序列添加更多数据点,从而增加行数。在大多数数据集中,总行数将远远超过时间序列的数量。在这个例子中,每行甚至存储了响应时间、大小两个指标,因此行、样本和序列之间的关系很少是一一对应的。

现在我们来看以下查询,该查询在一台32核机器上以毫秒级速度运行。它计算了上述表中响应时间的平均值,按1分钟间隔进行分组,按状态进行分桶,并限定为特定的某一天:

SELECT toStartOfMinute(time) AS minute, labels['status'] as status, avg(response_time) FROM metrics WHERE (time >= '2025-02-23 08:00:00') AND (time <= '2025-02-23 12:00:00') GROUP BY minute, status ORDER BY minute ASC ┌──────────────minute─┬─status─┬─avg(response_time)─┐ │ 2025-02-23 08:00:00 │ 403 │ 5065.290909090909 │ │ 2025-02-23 08:00:00 │ 500 │ 5327.543933054393 │ │ 2025-02-23 08:00:00 │ 302 │ 5119.796687088722 │ .. 3084 rows in set. Elapsed: 0.122 sec. Processed 49.21 million rows, 340.04 MB (404.24 million rows/s., 2.79 GB/s.) Peak memory usage: 726.64 MiB.

这是一个在 Prometheus 中执行会面临挑战的查询示例。你需要访问许多独立的序列才能完成此计算。

请注意,此查询计算的是事件型数据上的简单平均值,并按固定的一分钟时间窗口进行分组。它在语义上与之前的 avg_over_time(...5m) 示例并不完全相同。尽管结果在图表上可能看起来相似,但底层的计算模型是不同的——详见"When Prometheus still makes sense"(#when-prometheus-still-makes-sense)。

将查询范围缩小到更少的“序列”会减少需要读取的数据量。例如:

SELECT toStartOfMinute(time) AS minute, avg(response_time) FROM events WHERE application = '603' AND host = '3' AND status = '200' GROUP BY minute ORDER BY minute ASC ┌──────────────minute─┬─avg(response_time)─┐ │ 2025-01-24 00:00:00 │ 3680.4615384615386 │ │ 2025-01-24 00:01:00 │ 5515.153846153846 │ .. 5 rows in set. Elapsed: 0.078 sec. Processed 53.40 million rows, 479.93 MB (681.97 million rows/s., 6.13 GB/s.) Peak memory usage: 613.54 MiB.

在后一种情况下,得益于倒排索引,我们读取的数据量显著减少,这从查询执行时间中得到了体现。要将查询范围缩小到特定的某个序列,你只需为所有列添加过滤器即可。

当指标的查询频率不足以支持将其物化时,它们也可以保留在 labels 映射中,并在查询时进行动态访问。尽管我们通常建议对 response_time 等常用的聚合指标进行物化,但可观测性工作负载中常包含许多低频测量值,这些值仅偶尔会被查询。在这种情况下,直接从映射中访问指标是完全合理的做法。

例如,假设事件还包含一个表示响应负载大小的 size 属性。我们可能只偶尔需要计算随时间变化的整体流量。此时,与其物化另一个专用列,我们可以直接从映射中访问并转换该值:

SELECT toStartOfMinute(time) AS minute, sum(toDecimal32(labels['size'], 1)) AS total_traffic FROM events WHERE application = '603' GROUP BY minute ORDER BY minute ASC ┌──────────────minute─┬─total_traffic─┐ │ 2025-01-24 00:00:00 │ 89167 │ │ 2025-01-24 00:01:00 │ 352724 │ │ 2025-01-24 00:02:00 │ 284684 │ … 4 rows in set. Elapsed: 1.138 sec. Processed 53.31 million rows, 13.60 GB (46.84 million rows/s., 11.95 GB/s.)

这种灵活性是宽事件模型(wide events model)的优势之一。频繁查询的维度和指标可以被物化为优化列,而那些不常使用的属性则无需严格的预设模式定义,也能动态访问。

当 ClickHouse 处理高基数数据仍有成本时

ClickHouse 并不能完全消除基数(cardinality)带来的成本。

部分成本会转移到读取阶段,尤其是在执行大型 GROUP BY 操作时。

假设我们希望按 request_path 进行分组。此时,我们正在读取时按一个高基数列进行分组,并访问相应的映射结构。

SELECT toStartOfMinute(time) AS minute, labels['request_path'] as request_path, avg(response_time) FROM events WHERE toDate(time) = '2025-01-24' GROUP BY minute, request_path ORDER BY minute ASC -- we'll omit the results :) 0 rows in set. Elapsed: 9.433 sec. Processed 153.23 million rows, 63.85 GB (16.24 million rows/s., 6.77 GB/s.) Peak memory usage: 22.82 GiB.

这种成本在内存使用和查询耗时上均有所体现。

对超高基数列进行高性能分组确实会产生内存开销,因为聚合状态(aggregation states)必须在内存中构建。然而,ClickHouse 支持在超出阈值时将数据溢写到磁盘(https://clickhouse.com/docs/sql-reference/statements/select/group-by#group-by-in-external-memory),这使得这些工作负载变得可管理而非灾难性。通过调整参数以限制内存消耗来换取性能,即使在高基数场景下,它们依然能展现出卓越的性能:

SELECT toStartOfMinute(time) AS minute, labels['request_path'] as request_path, avg(response_time) FROM events WHERE toDate(time) = '2025-01-24' GROUP BY minute, request_path ORDER BY minute ASC -- Start external aggregation around 10 GB SETTINGS max_bytes_before_external_group_by = 10000000000 0 rows in set. Elapsed: 13.930 sec. Processed 153.23 million rows, 63.85 GB (11.00 million rows/s., 4.58 GB/s.) Peak memory usage: 9.57 GiB.

然而,更实际的问题是,你如何在可观测性仪表盘中可视化近百万条不同的数据行?

如果你的工作负载确实需要渲染数百万个独立的数据序列,ClickHouse 可能不是最理想的工具。但如果你需要对这数百万个序列进行聚合计算,例如统计总数或平均值,ClickHouse 则表现得非常出色。

ClickHouse 的适用场景

ClickHouse 特别适用于事件型可观测性数据,例如经由指标(metrics)增强的日志(logs)和追踪(traces),在这类数据中,高基数(high cardinality)是其固有特性而非偶然发生。它还擅长长期趋势分析、SLIs、KPIs 以及需要长时间范围分析的更广泛的业务指标。只要指标是从事件中派生而来,并且需要灵活的基于时间或维度的聚合时,ClickHouse 的列式模型(columnar model)就能充分发挥其优势。

重要的是,ClickHouse 不会受到短生命周期、临时性维度(如 container_id、pod_id 或现代环境中常见的其他动态生成标识符)的结构性影响。对 ClickHouse 而言,这些都仅仅是普通的列值。

这带来的实际影响非常显著。您可以将完整保真度的指标数据存储在 ClickHouse 中,而无需仅仅为了控制基数(cardinality)而进行激进采样、预聚合或剥离标签。您无需为了避免数据爆炸而设计数据模型(schema),而是可以自然地建模数据,并让查询模式决定读取哪些内容。

Prometheus 何时仍然有意义

Prometheus 在许多场景下仍然非常有用,尤其因为它拥有广泛的生态系统支持、成熟的运维工具以及深度集成的告警能力。

Prometheus 围绕着一种指标优先模型构建,在该模型中,样本被抓取并存储为独立的时间序列。它支持多种指标类型:计数器(counters)、仪表盘(gauges)、直方图(histograms)和摘要(summaries)。每种类型都内含了数据应如何解释的假设。计数器用于处理速率和重置。直方图和摘要支持延迟和分布分析。这些语义深度嵌入在 PromQL 及其周边生态系统中。

在 ClickHouse 的事件风格模型中,指标通常作为事件的数值列存储,并在查询时进行聚合。实际上,这些值通常更像是仪表盘(gauges),而非严格的 Prometheus 风格计数器。与 Prometheus 类似,ClickHouse 本质上存储数字(直方图除外),其语义来自查询逻辑而非存储层本身。

相反,Prometheus 假设的是定期采样的序列,并为计数器、速率、直方图和范围向量(range vectors)提供一流的语义支持。而 ClickHouse 则处理带时间戳的事件,这使其更适合对高基数可观测性数据进行灵活的事件风格聚合,而非严格的时间序列语义。

对于高度定向的单序列查询和绘图,Prometheus 通常表现出色。它的倒排索引(inverted index)能快速解决标签交集,并且读取单个压缩序列的效率很高。ClickHouse 在排序键(ordering key)与过滤器对齐时也能达到类似的性能,但在某些情况下可能会扫描更多数据。这种差异很少是决定性的,但这确实是 Prometheus 非常擅长的场景。

Prometheus 在基数 (cardinality) 受控且可预测的情况下表现出色。对于中等数量的长期序列、实时告警和传统监控工作流,Prometheus 效率极高。它能与 LGTM 等技术栈无缝集成,其中 Prometheus 及其分布式对应产品(如 Mimir)是一等公民。虽然分布式系统解决了可扩展性问题,但它们保留了底层的基于序列的模型,这意味着基数管理仍需深思熟虑。

ClickHouse 用于 Prometheus 指标

ClickHouse 的开源可观测性栈 ClickStack 支持 Prometheus 风格的指标(Open Telemetry metrics),但不应将其视为 Prometheus 或现有成熟的 Prometheus 兼容系统的即插即用替代品。其底层的存储和查询模型截然不同,ClickStack 偏爱事件导向的方法,而非严格的时间序列模型。

因此,目前在 ClickStack 中查询 Prometheus 风格的指标,是通过更高层的查询构建器和抽象实现的,它们的能力比原生 PromQL 更有限。这主要是由于完整复现 Prometheus 语义的复杂性,尤其是在处理计数器 (counters)、直方图 (histograms)、范围向量 (range vectors) 和更广泛的 PromQL 执行模型方面。

如果您需要成熟、久经考验的 PromQL 支持,并将其深度集成到仪表盘、告警和操作工作流中,Prometheus 依然是显而易见的选择。话虽如此,ClickHouse 对 PromQL 的支持仍在不断发展,并取得了近期令人鼓舞的进展(https://github.com/ClickHouse/ClickHouse/issues/57545#issuecomment-4246791048)。手动将复杂的 PromQL 查询转换为 SQL 也并非易事,特别是涉及直方图 (histograms) 和范围向量函数 (range-vector functions) 时——但在大型语言模型 (LLMs) 的帮助下仍有可能实现。

对于中等基数指标、以 PromQL 为核心的告警以及有针对性的序列查询,Prometheus 仍然是更合适的选择。

结论

ClickHouse 处理高基数 (cardinality) 并非没有代价,但与传统的时间序列系统相比,其成本体现在截然不同的方面。ClickHouse 通过将模型从面向序列转变为存储在列式存储表中的宽事件 (wide events),从而规避了在 Prometheus 等系统中因高基数而导致的每序列写入放大 (write amplification)、内存开销 (memory overhead) 和运维脆弱性 (operational fragility) 等难题。相应地,其权衡主要体现在查询时聚合 (query-time aggregation) 和数据建模决策上,而这些正是 ClickHouse 的列式执行模型 (columnar execution model)、压缩 (compression)、剪枝 (pruning) 和并行化 (parallelization) 等特性表现尤为突出的领域。

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布,优秀者也有机会推荐到 ClickHouse 官网。

关于我们

ClickHouse 是全球速度最快,资源利用最高效的在线分析列式数据库管理系统。现在,ClickHouse可以作为一个安全可扩展的无服务器应用在云中提供服务。通过云服务,ClickHouse使得任何人都能轻松获取高效的实时分析处理能力。

http://www.jsqmd.com/news/1163768/

相关文章:

  • 【2013-11-10】Android应用开发笔记:使用Messenger与Service通讯
  • 如何构建自进化企业级 Agent 平台
  • 第一章Netty, NIO字节缓冲区案例实现
  • 大模型 API 容错设计:重试、降级、熔断一个都不能少
  • ST-Link 下载报错定位手册(含接线+设置)
  • 2026年7月最新天津雅典官方售后维修服务网点地址与客服电话 - 亨得利官方服务中心
  • Claude Code 运维实战:用 AI Skill 打造自动化运维新范式
  • 托运摩托车,到底要不要打木架? - 快递物流资讯
  • 2026免费图片文字识别工具全解:在线网页、电脑软件、小程序实操指南
  • C#企业级高并发数据采集平台:工控现场复杂环境适配实战
  • 孤能子视角:三十六计之擒贼擒王——拓扑结构重构
  • 如何用OneMore插件实现OneNote效率革命:160+命令的完整指南
  • 寒地专网筑根基,全域服务践初心——黑龙江单工科技赋能东三省内蒙应急与行业通信升级纪实 - 无线电评测大师
  • 酸度梯度控制的原理---(技术突破及工艺应用)
  • 基于改进YOLOV11与Fasternet-BiFPN的工厂碳纤维缺陷识别研究
  • UVa 483 Word Scramble
  • 掌握Java虚拟机内存管理与GC调优
  • 混凝土用膨胀型锚栓
  • 高压快充普及重构用车生态:补能格局洗牌,重新定义新能源车产品力
  • 技术架构:技术选型和基础设施
  • 零成本开启企业碳足迹管理:openLCA开源生命周期评估工具完全指南
  • 使用 TiUP 工具升级 TiDB 集群到 v7.5 的具体步骤是什么?
  • esp32开发与应用(3.5寸触摸屏+lvgl)
  • 雷军从造年轻人车转向中年市场,小米澎程能在增程式SUV赛道再赢一次吗?
  • 2026年房地产动画行业观察
  • 多智能体不是堆 AI——跟着一个“AI 编辑部“看协作怎么跑通
  • 智能路由分发_acp-router
  • AI 赋能传统报表系统:自然语言查询到 SQL 生成的工程化管道
  • 摄像头 ISP 中典型 Image Signal Processing Pipeline
  • 影刀RPA Python读写配置文件:INI与YAML管理参数告别硬编码