跳到主要内容
跳到主要内容
编辑此页

数据管理

ClickHouse 的可观测性部署通常涉及需要管理的大型数据集。ClickHouse 提供了许多功能来协助数据管理。

分区

ClickHouse 中的分区允许根据列或 SQL 表达式在磁盘上逻辑地分隔数据。通过逻辑地分隔数据,可以独立地操作每个分区,例如删除。这允许用户在存储层之间有效地移动分区和子集,以按时间或 使数据过期/从集群中高效删除

分区在最初通过 PARTITION BY 子句定义表时指定。此子句可以包含任何列的 SQL 表达式,其结果将定义行发送到哪个分区。

NEEDS ALT

数据部分在逻辑上与磁盘上每个分区相关联(通过公共文件夹名称前缀),并且可以隔离查询。对于下面的示例,默认的 otel_logs schema 按天使用表达式 toDate(Timestamp) 进行分区。当行插入到 ClickHouse 中时,将针对每一行评估此表达式,并将行路由到结果分区(如果存在)(如果该行是当天的第一行,则将创建分区)。

CREATE TABLE default.otel_logs
(
...
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)

可以在分区上执行许多操作,包括备份列操作、mutation(修改/按行删除数据)和索引清除(例如,二级索引)

例如,假设我们的 otel_logs 表按天分区。如果使用结构化日志数据集填充,这将包含几天的数据

SELECT Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC

┌────────day─┬───────c─┐
2019-01-222333977
2019-01-232326694
2019-01-261986456
2019-01-241896255
2019-01-251821770
└────────────┴─────────┘

5 rows in set. Elapsed: 0.058 sec. Processed 10.37 million rows, 82.92 MB (177.96 million rows/s., 1.42 GB/s.)
Peak memory usage: 4.41 MiB.

可以使用简单的系统表查询找到当前分区

SELECT DISTINCT partition
FROM system.parts
WHERE `table` = 'otel_logs'

┌─partition──┐
2019-01-22
2019-01-23
2019-01-24
2019-01-25
2019-01-26
└────────────┘

5 rows in set. Elapsed: 0.005 sec.

我们可能有另一个表 otel_logs_archive,我们用它来存储旧数据。数据可以通过分区有效地移动到此表(这只是元数据更改)。

CREATE TABLE otel_logs_archive AS otel_logs
--move data to archive table
ALTER TABLE otel_logs
(MOVE PARTITION tuple('2019-01-26') TO TABLE otel_logs_archive
--confirm data has been moved
SELECT
Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC

┌────────day─┬───────c─┐
2019-01-222333977
2019-01-232326694
2019-01-241896255
2019-01-251821770
└────────────┴─────────┘

4 rows in set. Elapsed: 0.051 sec. Processed 8.38 million rows, 67.03 MB (163.52 million rows/s., 1.31 GB/s.)
Peak memory usage: 4.40 MiB.

SELECT Timestamp::Date AS day,
count() AS c
FROM otel_logs_archive
GROUP BY day
ORDER BY c DESC

┌────────day─┬───────c─┐
2019-01-261986456
└────────────┴─────────┘

1 row in set. Elapsed: 0.024 sec. Processed 1.99 million rows, 15.89 MB (83.86 million rows/s., 670.87 MB/s.)
Peak memory usage: 4.99 MiB.

这与其他技术形成对比,其他技术将需要使用 INSERT INTO SELECT 并将数据重写到新的目标表中。

移动分区

在表之间移动分区需要满足几个条件,尤其是表必须具有相同的结构、分区键、主键和索引/投影。有关如何在 ALTER DDL 中指定分区的详细说明,请参见此处

此外,可以通过分区有效地删除数据。这比其他技术(mutation 或轻量级删除)更节省资源,并且应该是首选。

ALTER TABLE otel_logs
(DROP PARTITION tuple('2019-01-25'))

SELECT
Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC
┌────────day─┬───────c─┐
2019-01-224667954
2019-01-234653388
2019-01-243792510
└────────────┴─────────┘
注意

当使用设置 ttl_only_drop_parts=1 时,TTL 会利用此功能。有关更多详细信息,请参见使用 TTL(生存时间)进行数据管理

应用场景

以上说明了如何按分区有效地移动和操作数据。实际上,用户最有可能在可观测性用例中利用分区操作用于以下两种场景

我们在下面详细探讨这两种情况。

查询性能

虽然分区可以帮助提高查询性能,但这在很大程度上取决于访问模式。如果查询仅针对少数分区(理想情况下为一个),则性能可能会提高。这通常仅在分区键不在主键中并且您按其进行过滤时才有用。但是,需要覆盖许多分区的查询可能比不使用分区时性能更差(因为可能存在更多部分)。如果分区键已经是主键中的早期条目,则针对单个分区的好处将变得更小甚至不存在。如果每个分区中的值是唯一的,则分区还可以用于优化 GROUP BY 查询。但是,通常,用户应确保主键已优化,并且仅在访问模式访问数据的特定可预测子集(例如,按天分区,大多数查询在最近一天内)的特殊情况下才考虑使用分区作为查询优化技术。有关此行为的示例,请参见此处

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

生存时间 (TTL) 是由 ClickHouse 驱动的可观测性解决方案中的一项关键功能,用于高效的数据保留和管理,尤其是在持续生成大量数据的情况下。在 ClickHouse 中实施 TTL 可以自动使旧数据过期和删除,从而确保最佳地使用存储并保持性能,而无需手动干预。此功能对于保持数据库精简、降低存储成本以及通过关注最相关和最新的数据来确保查询保持快速高效至关重要。此外,它通过系统地管理数据生命周期来帮助遵守数据保留策略,从而增强可观测性解决方案的整体可持续性和可扩展性。

可以在 ClickHouse 中的表级别或列级别指定 TTL。

表级别 TTL

日志和追踪的默认 schema 都包含一个 TTL,用于在指定时间段后使数据过期。这在 ClickHouse exporter 中在 ttl 键下指定,例如。

exporters:
clickhouse:
endpoint: tcp://:9000?dial_timeout=10s&compress=lz4&async_insert=1
ttl: 72h

此语法当前支持 Golang Duration 语法我们建议用户使用 h 并确保其与分区周期对齐。例如,如果您按天分区,请确保它是天数的倍数,例如 24h、48h、72h。 这将自动确保将 TTL 子句添加到表中,例如,如果 ttl: 96h

PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(4)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1

默认情况下,当 ClickHouse 合并数据部分时,将删除 TTL 过期的数据。当 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 应用)。启用此设置后,当某个部分中的所有行都过期时,ClickHouse 将删除整个部分。与部分清除 TTL 行(当 ttl_only_drop_parts=0 时通过资源密集型 mutation 实现)相比,删除整个部分允许具有更短的 merge_with_ttl_timeout 时间并降低对系统性能的影响。如果数据按与您执行 TTL 过期相同的单位(例如,天)进行分区,则部分自然只会包含来自定义间隔的数据。这将确保可以有效地应用 ttl_only_drop_parts=1

列级别 TTL

上面的示例在表级别使数据过期。用户还可以在列级别使数据过期。随着数据老化,这可以用于删除在调查中其值不足以证明保留其资源开销的列。例如,我们建议保留 Body 列,以防添加了新的动态元数据,这些元数据在插入时未提取,例如,新的 Kubernetes 标签。在一个时期(例如 1 个月)之后,可能很明显,此附加元数据没有用处 - 因此限制了保留 Body 列的价值。

下面,我们展示了如何在 30 天后删除 Body 列。

CREATE TABLE otel_logs_v2
(
`Body` String TTL Timestamp + INTERVAL 30 DAY,
`Timestamp` DateTime,
...
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)
注意

指定列级别 TTL 需要用户指定自己的 schema。这不能在 OTel 收集器中指定。

重新压缩数据

虽然我们通常建议对可观测性数据集使用 ZSTD(1),但用户可以尝试使用不同的压缩算法或更高的压缩级别,例如 ZSTD(3)。除了能够在 schema 创建时指定这一点之外,还可以配置压缩在设定的时间段后更改。如果编解码器或压缩算法提高了压缩率但导致查询性能下降,则这可能是合适的。这种权衡可能适用于查询频率较低的旧数据,但不适用于在调查中更频繁使用的最新数据。

下面显示了一个示例,其中我们在 4 天后使用 ZSTD(3) 压缩数据,而不是删除数据。

CREATE TABLE default.otel_logs_v2
(
`Body` String,
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`Status` UInt16,
`RequestProtocol` LowCardinality(String),
`RunTime` UInt32,
`Size` UInt32,
`UserAgent` String,
`Referer` String,
`RemoteUser` String,
`RequestType` LowCardinality(String),
`RequestPath` String,
`RemoteAddress` IPv4,
`RefererDomain` String,
`RequestPage` String,
`SeverityText` LowCardinality(String),
`SeverityNumber` UInt8,
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)
TTL Timestamp + INTERVAL 4 DAY RECOMPRESS CODEC(ZSTD(3))
评估性能

我们建议用户始终评估不同压缩级别和算法对插入和查询性能的影响。例如,delta 编解码器可能有助于压缩时间戳。但是,如果这些是主键的一部分,则过滤性能可能会受到影响。

有关配置 TTL 的更多详细信息和示例,请参见此处。有关如何为表和列添加和修改 TTL 的示例,请参见此处。有关 TTL 如何启用存储层级结构(例如热-温架构)的信息,请参见存储层

存储层

在 ClickHouse 中,用户可以在不同的磁盘上创建存储层,例如,SSD 上的热/最新数据和 S3 支持的旧数据。这种架构允许对旧数据使用更便宜的存储,由于其在调查中不经常使用,因此具有更高的查询 SLA。

与 ClickHouse Cloud 无关

ClickHouse Cloud 使用数据的单个副本,该副本在 S3 上备份,并具有 SSD 支持的节点缓存。因此,ClickHouse Cloud 中不需要存储层。

存储层的创建需要用户创建磁盘,然后使用这些磁盘来制定存储策略,并在表创建期间指定卷。可以根据填充率、部分大小和卷优先级在磁盘之间自动移动数据。更多详细信息,请参见此处

虽然可以使用 ALTER TABLE MOVE PARTITION 命令手动在磁盘之间移动数据,但也可以使用 TTL 控制卷之间的数据移动。完整的示例可以在此处找到。

管理 schema 更改

日志和追踪 schema 不可避免地会在系统的生命周期内发生更改,例如,当用户监视具有不同元数据或 pod 标签的新系统时。通过使用 OTel schema 生成数据,并以结构化格式捕获原始事件数据,ClickHouse schema 将对这些更改具有鲁棒性。但是,随着新元数据变得可用并且查询访问模式发生变化,用户将希望更新 schema 以反映这些发展。

为了避免在 schema 更改期间停机,用户有几个选项,我们在下面介绍这些选项。

使用默认值

可以使用DEFAULT将列添加到 schema 中。如果在 INSERT 期间未指定,则将使用指定的默认值。

可以在修改任何物化视图转换逻辑或导致发送这些新列的 OTel 收集器配置之前进行 schema 更改。

更改 schema 后,用户可以重新配置 OTel 收集器。假设用户正在使用 “使用 SQL 提取结构” 中概述的推荐流程,其中 OTel 收集器将其数据发送到 Null 表引擎,物化视图负责提取目标 schema 并将结果发送到目标表进行存储,则可以使用 ALTER TABLE ... MODIFY QUERY 语法修改视图。假设我们有以下目标表及其对应的物化视图(类似于“使用 SQL 提取结构”中使用的视图),以从 OTel 结构化日志中提取目标 schema

CREATE TABLE default.otel_logs_v2
(
`Body` String,
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`Status` UInt16,
`RequestProtocol` LowCardinality(String),
`RunTime` UInt32,
`UserAgent` String,
`Referer` String,
`RemoteUser` String,
`RequestType` LowCardinality(String),
`RequestPath` String,
`RemoteAddress` IPv4,
`RefererDomain` String,
`RequestPage` String,
`SeverityText` LowCardinality(String),
`SeverityNumber` UInt8
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)

CREATE MATERIALIZED VIEW otel_logs_mv TO otel_logs_v2 AS
SELECT
Body,
Timestamp::DateTime AS Timestamp,
ServiceName,
LogAttributes['status']::UInt16 AS Status,
LogAttributes['request_protocol'] AS RequestProtocol,
LogAttributes['run_time'] AS RunTime,
LogAttributes['user_agent'] AS UserAgent,
LogAttributes['referer'] AS Referer,
LogAttributes['remote_user'] AS RemoteUser,
LogAttributes['request_type'] AS RequestType,
LogAttributes['request_path'] AS RequestPath,
LogAttributes['remote_addr'] AS RemoteAddress,
domain(LogAttributes['referer']) AS RefererDomain,
path(LogAttributes['request_path']) AS RequestPage,
multiIf(Status::UInt64 > 500, 'CRITICAL', Status::UInt64 > 400, 'ERROR', Status::UInt64 > 300, 'WARNING', 'INFO') AS SeverityText,
multiIf(Status::UInt64 > 500, 20, Status::UInt64 > 400, 17, Status::UInt64 > 300, 13, 9) AS SeverityNumber
FROM otel_logs

假设我们希望从 LogAttributes 中提取新列 Size。我们可以使用 ALTER TABLE 将其添加到我们的 schema 中,并指定默认值

ALTER TABLE otel_logs_v2
(ADD COLUMN `Size` UInt64 DEFAULT JSONExtractUInt(Body, 'size'))

在上面的示例中,我们将默认值指定为 LogAttributes 中的 size 键(如果不存在,则为 0)。这意味着对于没有插入值的行,访问此列的查询必须访问 Map,因此会更慢。我们也可以轻松地将其指定为常量,例如 0,从而降低后续查询没有值的行的成本。查询此表显示该值按预期从 Map 中填充

SELECT Size
FROM otel_logs_v2
LIMIT 5
┌──Size─┐
30577
5667
5379
1696
41483
└───────┘

5 rows in set. Elapsed: 0.012 sec.

为了确保为所有未来数据插入此值,我们可以使用 ALTER TABLE 语法修改我们的物化视图,如下所示

ALTER TABLE otel_logs_mv
MODIFY QUERY
SELECT
Body,
Timestamp::DateTime AS Timestamp,
ServiceName,
LogAttributes['status']::UInt16 AS Status,
LogAttributes['request_protocol'] AS RequestProtocol,
LogAttributes['run_time'] AS RunTime,
LogAttributes['size'] AS Size,
LogAttributes['user_agent'] AS UserAgent,
LogAttributes['referer'] AS Referer,
LogAttributes['remote_user'] AS RemoteUser,
LogAttributes['request_type'] AS RequestType,
LogAttributes['request_path'] AS RequestPath,
LogAttributes['remote_addr'] AS RemoteAddress,
domain(LogAttributes['referer']) AS RefererDomain,
path(LogAttributes['request_path']) AS RequestPage,
multiIf(Status::UInt64 > 500, 'CRITICAL', Status::UInt64 > 400, 'ERROR', Status::UInt64 > 300, 'WARNING', 'INFO') AS SeverityText,
multiIf(Status::UInt64 > 500, 20, Status::UInt64 > 400, 17, Status::UInt64 > 300, 13, 9) AS SeverityNumber
FROM otel_logs

后续的行将在插入时填充 Size 列。

创建新表

作为上述过程的替代方法,用户可以简单地使用新 schema 创建一个新的目标表。然后可以使用上述 ALTER TABLE MODIFY QUERY. 修改任何物化视图以使用新表。使用此方法,用户可以对他们的表进行版本控制,例如 otel_logs_v3

此方法使用户可以使用多个表进行查询。为了跨表查询,用户可以使用 merge 函数,该函数接受表名称的通配符模式。我们在下面通过查询 otel_logs 表的 v2 和 v3 版本来演示这一点

SELECT Status, count() AS c
FROM merge('otel_logs_v[2|3]')
GROUP BY Status
ORDER BY c DESC
LIMIT 5

┌─Status─┬────────c─┐
20038319300
3041360912
302799340
404420044
301270212
└────────┴──────────┘

5 rows in set. Elapsed: 0.137 sec. Processed 41.46 million rows, 82.92 MB (302.43 million rows/s., 604.85 MB/s.)

如果用户希望避免使用 merge 函数并向最终用户公开一个组合多个表的表,则可以使用 Merge 表引擎。我们在下面演示了这一点

CREATE TABLE otel_logs_merged
ENGINE = Merge('default', 'otel_logs_v[2|3]')

SELECT Status, count() AS c
FROM otel_logs_merged
GROUP BY Status
ORDER BY c DESC
LIMIT 5

┌─Status─┬────────c─┐
20038319300
3041360912
302799340
404420044
301270212
└────────┴──────────┘

5 rows in set. Elapsed: 0.073 sec. Processed 41.46 million rows, 82.92 MB (565.43 million rows/s., 1.13 GB/s.)

每当添加新表时,都可以使用 EXCHANGE 表语法更新此表。例如,要添加 v4 表,我们可以创建一个新表并使用先前版本原子地交换它。

CREATE TABLE otel_logs_merged_temp
ENGINE = Merge('default', 'otel_logs_v[2|3|4]')

EXCHANGE TABLE otel_logs_merged_temp AND otel_logs_merged

SELECT Status, count() AS c
FROM otel_logs_merged
GROUP BY Status
ORDER BY c DESC
LIMIT 5

┌─Status─┬────────c─┐
20039259996
3041378564
302820118
404429220
301276960
└────────┴──────────┘

5 rows in set. Elapsed: 0.068 sec. Processed 42.46 million rows, 84.92 MB (620.45 million rows/s., 1.24 GB/s.)
© . This site is unofficial and not affiliated with ClickHouse, Inc.