跳转到主要内容
用于可观测性的 ClickHouse 部署通常都会涉及海量数据集,因此需要妥善管理。ClickHouse 提供了多项功能来帮助进行数据管理。

分区

ClickHouse 中的分区允许根据某个列或 SQL 表达式在磁盘上对数据进行逻辑划分。通过这种逻辑划分,可以对每个分区独立执行操作,例如删除。这让你能够按时间高效地在不同存储层级之间移动分区及其子集,或者让数据过期/从集群中高效删除数据 分区是在表初始定义时通过 PARTITION BY 子句指定的。该子句可以包含基于任意列的 SQL 表达式,其结果决定每一行会被写入哪个分区。 磁盘上的每个分区都会在逻辑上关联相应的数据分区片段 (通过相同的文件夹名前缀) ,并且可以单独查询。以下面的示例为例,默认的 otel_logs schema 使用表达式 toDate(Timestamp) 按天分区。随着行被插入 ClickHouse,系统会对每一行计算该表达式,并将其路由到对应的分区;如果某一天的第一行到来,则会创建该分区。
可以对分区执行多种操作,包括备份列操作、按行修改/删除数据的变更)以及清除索引 (例如二级索引) 例如,假设我们的 otel_logs 表按天分区。如果写入结构化日志数据集,其中将包含数天的数据:
可以使用一个简单的系统表查询来查看当前分区:
我们可能还有另一张表 otel_logs_archive,用于存储较早的数据。可以按分区高效地将数据迁移到这张表中 (这只是元数据层面的变更) 。
这不同于其他方法,后者需要使用 INSERT INTO SELECT,并将数据重写到新的目标表中。
移动分区在表之间移动分区需要满足若干条件,其中一个基本要求是这些表必须具有相同的结构、分区键、主键以及索引/投影。有关如何在 ALTER DDL 中指定分区的详细说明,见此处
此外,还可以按分区高效删除数据。与其他替代方案 (变更或轻量级删除) 相比,这种方式的资源效率要高得多,因此应优先采用。
使用设置 ttl_only_drop_parts=1 时,生存时间 (TTL) 会利用这一特性。更多详情,请参见使用 生存时间 (TTL) 进行数据管理

应用场景

以上说明了如何按分区高效地移动和处理数据。实际上,在可观测性场景中,你最常使用分区操作的很可能是以下两种情况: 下面我们将分别详细介绍这两种场景。

查询性能

尽管分区可以帮助提升查询性能,但这在很大程度上取决于访问模式。如果查询只涉及少量分区 (理想情况下是一个) ,性能可能会有所提升。通常,这只在分区键不在主键中且查询按该键进行过滤时才有意义。不过,如果查询需要覆盖很多分区,其性能反而可能比不使用分区更差 (因为可能会有更多的 parts) 。如果分区键已经是主键中靠前的项,那么针对单个分区所带来的收益就会很不明显,甚至几乎没有。如果每个分区中的值都是唯一的,分区还可以用于优化 GROUP BY 查询。但总体而言,你应该先确保主键已得到优化,只有在少数特殊场景下,才考虑将分区作为一种查询优化手段——例如按天分区,而大多数查询都集中在最近一天,这类访问模式只会访问一个可预测的特定数据子集。有关这种行为的示例,请参见这里

使用 生存时间 (TTL) (生存时间) 进行数据管理

生存时间 (TTL) 是基于 ClickHouse 的可观测性解决方案中的一项关键功能,能够高效地进行数据保留和管理,尤其适用于持续产生海量数据的场景。在 ClickHouse 中配置 生存时间 (TTL) 后,旧数据可以自动过期并删除,从而确保存储得到充分利用,并在无需人工干预的情况下保持系统性能。这一能力对于精简数据库、降低存储成本,以及通过聚焦最新且最相关的数据来保证查询快速高效,至关重要。此外,它还能通过系统化管理数据生命周期,帮助满足数据保留策略方面的合规要求,从而提升可观测性解决方案整体的可持续性和可扩展性。 在 ClickHouse 中,可以在表级或列级指定 生存时间 (TTL)。

表级 生存时间 (TTL)

日志和链路追踪的默认 schema 都包含 生存时间 (TTL),用于在指定时间后让数据过期。可在 ClickHouse exporter 中通过 ttl 键进行设置,例如:
此语法当前支持 Golang Duration syntax我们建议用户使用 h,并确保其与分区周期保持一致。例如,如果按天分区,请确保其为天数的整数倍,例如 24h、48h、72h。 这样会自动为表添加 生存时间 (TTL) 子句,例如当 ttl: 96h 时。
默认情况下,带有已过期生存时间 (TTL) 的数据会在 ClickHouse 合并数据分区片段 时被移除。当 ClickHouse 检测到数据已过期时,会执行一次计划外合并。
计划性 生存时间 (TTL)生存时间 (TTL) 不会立即生效,而是如上所述按计划执行。MergeTree 表设置 merge_with_ttl_timeout 用于设置再次执行带删除生存时间 (TTL) 的合并前的最小延迟时间 (秒) 。默认值为 14400 秒 (4 小时) 。但这只是最小延迟,实际触发生存时间 (TTL) 合并可能还需要更长时间。如果该值过低,会执行大量计划外合并,并可能消耗大量资源。可以使用命令 ALTER TABLE my_table MATERIALIZE TTL 强制触发生存时间 (TTL) 过期处理。
**重要:我们建议使用设置 ttl_only_drop_parts=1 ** (默认 schema 会应用该设置) 。启用此设置后,当某个 part 中的所有行都已过期时,ClickHouse 会直接删除整个 part。相比清理 part 中部分已过期的 生存时间 (TTL) 行 (在 ttl_only_drop_parts=0 时,这需要通过资源密集型变更来完成) ,直接删除整个 part 可以使用更短的 merge_with_ttl_timeout,并降低对系统性能的影响。如果数据按执行 生存时间 (TTL) 过期所依据的相同单位进行分区,例如按天分区,那么 part 自然只会包含该时间间隔内的数据。这将确保 ttl_only_drop_parts=1 能够被高效应用。

列级生存时间 (TTL)

上述示例是在表级让数据过期。你也可以让数据在列级过期。随着数据老化,这可用于删除那些在调查中价值不足以抵消其保留成本的列。例如,我们建议保留 Body 列,以防新增了尚未在写入时提取的动态元数据,比如新的 Kubernetes 标签。经过一段时间后,例如 1 个月,可能就会明显看出这些额外元数据并不实用——因此继续保留 Body 列的价值也就有限了。 下面,我们将说明如何在 30 天后删除 Body 列。
指定列级生存时间 (TTL) 需要用户自行定义 schema。OTel collector 中无法进行此项配置。

重新压缩数据

虽然对于可观测性数据集,我们通常建议使用 ZSTD(1),但你也可以尝试不同的压缩算法或更高的压缩级别,例如 ZSTD(3)。除了可以在创建 schema 时指定外,还可以将压缩配置为在设定的一段时间后切换。如果某种 codec 或压缩算法能提升压缩率,但会导致查询性能下降,那么这种做法可能是合适的。对于查询频率较低的旧数据,这种权衡或许可以接受;但对于较新的数据则未必如此,因为这些数据在调查中会更频繁地使用。 下面展示了一个示例:4 天后使用 ZSTD(3) 压缩数据,而不是将其删除。
评估性能我们建议用户始终评估不同压缩级别和算法对 insert 和查询性能的影响。例如,delta 编解码器有助于压缩时间戳。不过,如果这些时间戳属于主键的一部分,过滤性能可能会受到影响。
有关配置生存时间 (TTL) 的更多详细信息和示例,请参见此处。有关如何为表和列添加及修改 生存时间 (TTL) 的示例,请参见此处。关于 生存时间 (TTL) 如何支持热温分层等存储层级架构,请参见存储层级

存储层级

在 ClickHouse 中,你可以在不同磁盘上创建存储层级,例如将热数据/近期数据放在 SSD 上,而将较旧的数据存储在以 S3 为后端的介质上。这种架构可以让较旧的数据使用成本更低的存储;由于这些数据在调查中使用频率较低,因此通常可以接受更宽松的查询 SLA。
不适用于 ClickHouse CloudClickHouse Cloud 使用单份数据副本,以 S3 为后端,并配有基于 SSD 的节点缓存。因此,ClickHouse Cloud 不需要存储层级。
创建存储层级时,用户需要先创建磁盘,再基于这些磁盘定义存储策略,并在创建表时指定相应的卷。数据可以根据写满率、分片大小和卷优先级在磁盘之间自动移动。更多详细信息请参见这里 虽然可以使用 ALTER TABLE MOVE PARTITION 命令在磁盘之间手动移动数据,但也可以使用 TTL 来控制数据在卷之间的移动。完整示例请参见这里

管理 schema 变更

在系统的整个生命周期内,日志和 trace 的 schema 几乎不可避免会发生变化,例如当用户开始监控带有不同元数据或 pod (容器组) 标记的新系统时。通过使用 OTel schema 生成数据,并以结构化格式保留原始事件数据,ClickHouse schema 能够较好地适应这些变化。不过,随着新的元数据不断出现,以及查询访问模式发生变化,你也需要相应更新 schema 以反映这些变化。 为了避免在 schema 变更期间发生停机,用户有几种可选方案,我们将在下文介绍。

使用默认值

可以使用 DEFAULT向 schema 中添加列。如果在 INSERT 时未指定,则会使用指定的默认值。 在修改任何 materialized view 转换逻辑或 OTel collector 配置之前,可以先更改 schema,这样后续就会发送这些新列。 schema 更改完成后,你可以重新配置 OTel collectors。假设用户采用了“使用 SQL 提取结构”中介绍的推荐流程,即 OTel collectors 将数据发送到使用 Null table engine 的表,由 materialized view 负责提取目标 schema,并将结果发送到目标表中存储,那么可以使用 ALTER TABLE ... MODIFY QUERY 语法来修改该视图。假设我们有下面这个目标表及其对应的 materialized view (类似于“使用 SQL 提取结构”中使用的方式) ,用于从 OTel 结构化日志中提取目标 schema:
假设我们想从 LogAttributes 中提取一个新的列 Size。我们可以使用 ALTER TABLE 将其添加到 schema 中,并指定默认值:
在上面的示例中,我们将默认值指定为 LogAttributes 中的 size 键 (如果该键不存在,则为 0) 。这意味着,对于未插入该值的行,查询这一列时必须访问该 Map,因此速度会更慢。我们也可以很容易地将其指定为一个常量,例如 0,从而降低后续查询这些没有该值的行时的开销。查询此表可以看到,该值已按预期从 Map 中填充:
为确保今后的所有数据都会插入该值,我们可以使用如下所示的 ALTER TABLE 语法来修改 materialized view:
后续行的 Size 列会在写入时自动填充。

创建新表

除了上述流程外,你也可以直接按新的 schema 创建一张新的目标表。然后,可使用上文的 ALTER TABLE MODIFY QUERY. 修改任何 materialized view,使其改用这张新表。采用这种方式,你可以为表添加版本号,例如 otel_logs_v3 这种方式会导致用户需要面对多个可查询的表。若要跨表查询,可以使用merge 函数,它支持对表名使用通配符 pattern。下面我们通过查询 otel_logs 表的 v2 和 v3 版本来演示这一点:
如果用户希望避免使用 merge 函数,并向最终用户提供一张整合多个表的表,则可以使用 Merge 表引擎。下面我们来演示这一点:
每次新增表时,都可以使用 EXCHANGE 表语法来更新。例如,要添加 v4 表,可以先创建一个新表,再通过原子方式将其与上一版本进行交换。
最后修改于 2026年6月12日