
本文字数:24423;估计阅读时间:62 分钟
作者: ClickHouse官方
本文在公众号【ClickHouseInc】首发
本文是 ClickHouse 官网文档可观测性系列文章,共有 5 篇:
4. 与 OpenTelemetry 集成
5. 使用 Grafana 分析数据
本篇为第四篇《与 OpenTelemetry 集成》,正文如下:
任何可观测性 (Observability) 解决方案都离不开日志和跟踪的收集与导出工具。ClickHouse 推荐使用 OpenTelemetry (OTel) 项目来满足这一需求。
“OpenTelemetry 是一个专为创建和管理遥测数据(包括跟踪、指标和日志)而设计的可观测性框架与工具包。”
不同于 ClickHouse 或 Prometheus,OpenTelemetry 并不是一个可观测性后端,而是专注于遥测数据的生成、收集、管理和导出。最初,OTel 的目标是通过语言特定的 SDK 帮助用户为应用程序或系统轻松设置埋点,而如今,它已扩展到通过 OpenTelemetry Collector 收集日志。Collector 是一种代理或中间件,用于接收、处理并导出遥测数据。
与 ClickHouse 相关的组件
OpenTelemetry 包括多种功能组件。除了定义数据与 API 规范、标准协议及字段命名规则外,它提供了两个在基于 ClickHouse 的可观测性解决方案中尤为重要的功能:
1. OpenTelemetry Collector
Collector 是一个代理组件,用于接收、处理和导出遥测数据。在基于 ClickHouse 的解决方案中,Collector 主要用于日志收集和事件处理,随后将数据批量化插入 ClickHouse。
2. 语言 SDK
OpenTelemetry 提供了一系列语言 SDK,用于实现遥测数据的规范、API 和导出。这些 SDK 确保应用程序中的跟踪能够正确记录,并生成相关的 span,同时通过元数据在服务间传递上下文,构建分布式跟踪。这些 SDK 配套的生态系统还能自动为常用库和框架实现埋点功能,无需用户手动修改代码,便可开箱即用。
基于 ClickHouse 的可观测性解决方案充分利用了 OpenTelemetry Collector 和语言 SDK,使遥测数据的生成、收集和分析更加高效。
发行版
OpenTelemetry Collector 提供多个版本,其中与 ClickHouse 解决方案相关的 filelog 接收器和 ClickHouse 导出器仅包含在 OpenTelemetry Collector Contrib 版本中。
这个版本包含了大量的组件,适合用户在开发阶段尝试不同的配置。然而,在生产环境中,我们建议用户只部署所需的组件。这样做的理由包括:
-
减少 Collector 的体积,从而缩短部署时间
-
降低潜在的安全风险,减少攻击面
用户可以使用 OpenTelemetry Collector Builder 来创建自定义 Collector。
使用 OTel 导入数据
Collector 部署角色
为了收集日志并将其存入 ClickHouse,我们推荐使用 OpenTelemetry Collector。该 Collector 可以以两种主要角色部署:
-
代理(Agent) 代理实例通常部署在边缘节点上,例如服务器或 Kubernetes 节点,或直接从应用程序接收已埋点的事件。在这种部署模式下,代理实例与应用程序一起运行,通常作为 sidecar 或 DaemonSet 配置。代理可以直接将收集到的数据发送到 ClickHouse,或者先发送到网关实例,再进行集中处理。如果代理直接将数据发送到 ClickHouse,则称为“代理部署模式”。
-
网关(Gateway) 网关实例提供独立的服务,通常按集群、数据中心或区域进行部署。例如,在 Kubernetes 中部署一个网关,所有应用程序和代理都通过一个统一的 OTLP 端点发送数据。网关部署模式通常包含多个网关实例,并配有负载均衡器来分配请求。所有代理和应用程序通过这一单一端点进行通信。
以下示例展示了一个简单的代理 Collector,直接将数据发送到 ClickHouse。有关网关部署和适用场景的详细信息,请参见《使用网关进行扩展》。
收集日志
使用 OpenTelemetry Collector 的主要优势在于,它能够帮助您的服务快速卸载数据,同时将重试、批处理、加密、敏感数据过滤等复杂任务交给 Collector 处理。
在 Collector 中,数据处理分为接收、处理和导出三个阶段。接收器负责收集数据,可以是拉取(pull)或推送(push)方式。处理器用于对数据进行转换和增强。导出器则负责将数据发送到目标服务。虽然理论上,这个目标服务也可以是另一个 Collector,但在这里我们假设所有数据都直接发送到 ClickHouse 进行处理。

我们建议用户深入了解 Collector 的所有接收器、处理器和导出器。
Collector 提供了两种主要的日志收集方式:
通过 OTLP 协议-在这种模式下,日志数据通过 OTLP 协议从 OpenTelemetry SDK 推送到 Collector。OpenTelemetry 的演示实例采用了这一方法,各种语言的 OTLP 导出器假设一个本地 Collector 端点。此时,Collector 必须配置为使用 OTLP 接收器 — 请参考上面的演示配置。采用这种方式的一个好处是,日志数据将自动带有 Trace Id,便于用户在后续查询时将日志与特定的 trace 对应起来,反之亦然。

这种方式要求用户为应用程序代码加上相应的 OpenTelemetry SDK 进行埋点。
通过 Filelog 接收器抓取日志-Filelog 接收器通过监控磁盘上的日志文件来收集日志,并将其发送到 ClickHouse。这个接收器能处理复杂的任务,比如识别多行日志、处理日志轮换、通过检查点确保重启后的可靠性,并提取日志结构。它还能支持跟踪 Docker 和 Kubernetes 容器的日志,并通过 Helm chart 部署,自动从这些日志中提取结构信息并增强 pod 相关数据。

大多数情况下,部署环境会同时使用这两种接收器。我们建议用户阅读 Collector 的官方文档,熟悉相关概念,配置结构以及安装步骤。
提示:otelbin.io
otelbin.io 是一个非常实用的工具,可以帮助用户验证和可视化配置,确保 Collector 配置正确无误。
结构化与非结构化日志
日志可以是结构化的,也可以是非结构化的。
结构化日志 使用如 JSON 这样的数据格式,定义了元数据字段,例如 http 状态码和源 IP 地址。
{
"remote_addr":"54.36.149.41",
"remote_user":"-","run_time":"0","time_local":"2019-01-22 00:26:14.000","request_type":"GET",
"request_path":"\/filter\/27|13 ,27| 5 ,p53","request_protocol":"HTTP\/1.1",
"status":"200",
"size":"30577",
"referer":"-",
"user_agent":"Mozilla\/5.0 (compatible; AhrefsBot\/6.1; +http:\/\/ahrefs.com\/robot\/)"
}
非结构化日志 通常也包含一些可以通过正则表达式提取的固有结构,但它们主要以纯文本字符串的形式记录日志。
54.36.149.41 - - [22/Jan/2019:03:56:14 +0330] "GET
/filter/27|13%20%D9%85%DA%AF%D8%A7%D9%BE%DB%8C%DA%A9%D8%B3%D9%84,27|%DA%A9%D9%85%D8%AA%D8%B1%20%D8%A7%D8%B2%205%20%D9%85%DA%AF%D8%A7%D9%BE%DB%8C%DA%A9%D8%B3%D9%84,p53 HTTP/1.1" 200 30577 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" "-"
我们建议用户尽量使用结构化日志,特别是以 JSON 格式(即 ndjson)记录日志。这样做可以简化后续日志的处理过程,无论是在通过 Collector 处理器发送到 ClickHouse 之前,还是在插入时通过物化视图进行处理。结构化日志能够有效减少后续处理所需的资源,从而降低 ClickHouse 解决方案对 CPU 的需求。
示例
为了便于理解,我们提供了一个结构化(JSON)日志和一个非结构化日志的数据集,每个数据集大约包含 1000 万行数据,下载链接如下:
-
非结构化日志
-
结构化日志
下载方式:公众号后台【ClickHouseInc】回复“数据集”获取
在以下示例中,我们使用结构化日志数据集。请确保下载并解压该文件,以便复现接下来的示例。
下面是一个简单的 OTel Collector 配置示例,配置中读取磁盘上的日志文件,使用 filelog 接收器,并将处理后的日志输出到标准输出(stdout)。由于我们的日志是结构化的,因此我们使用 json_parser 操作符。请根据实际情况修改访问结构化日志文件的路径。
考虑使用 ClickHouse 进行解析
下面的示例展示了如何从日志中提取时间戳。此过程需要使用 json_parser 操作符,将整个日志行转换为 JSON 格式,并将结果存储在 LogAttributes 字段中。尽管这种方法会占用一定的计算资源,但可以通过在 ClickHouse 中使用 SQL 提取结构的方式更加高效地完成这一任务。对于非结构化日志,您可以使用 regex_parser 来达到相同的效果,具体示例如下所示。
config-structured-logs.yaml
receivers:
filelog:
include:
- /opt/data/logs/access-structured.log
start_at: beginning
operators:
- type: json_parser
timestamp:
parse_from: attributes.time_local
layout: '%Y-%m-%d %H:%M:%S'
processors:
batch:
timeout: 5s
send_batch_size: 1
exporters:
logging:
loglevel: debug
service:
pipelines:
logs:
receivers: [filelog]
processors: [batch]
exporters: [logging]
用户可以按照官方说明在本地安装 OpenTelemetry Collector。需要注意的是,安装时要确保选择包含 filelog 接收器的 contrib 发行版。例如,用户应该下载 otelcol-contrib_0.102.1_darwin_arm64.tar.gz,而不是 otelcol_0.102.1_darwin_arm64.tar.gz。发布版本可以在此处找到【https://github.com/open-telemetry/opentelemetry-collector-releases/releases】。
安装完成后,OTel Collector 可以通过以下命令启动:
./otelcol-contrib --config config-logs.yaml
假设使用结构化日志,输出的日志消息将如下所示:
LogRecord #98
ObservedTimestamp: 2024-06-19 13:21:16.414259 +0000 UTC
Timestamp: 2019-01-22 01:12:53 +0000 UTC
SeverityText:
SeverityNumber: Unspecified(0)
Body: Str({"remote_addr":"66.249.66.195","remote_user":"-","run_time":"0","time_local":"2019-01-22 01:12:53.000","request_type":"GET","request_path":"\/product\/7564","request_protocol":"HTTP\/1.1","status":"301","size":"178","referer":"-","user_agent":"Mozilla\/5.0 (Linux; Android 6.0.1; Nexus 5X Build\/MMB29P) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/41.0.2272.96 Mobile Safari\/537.36 (compatible; Googlebot\/2.1; +http:\/\/www.google.com\/bot.html)"})
Attributes:
-> remote_user: Str(-)
-> request_protocol: Str(HTTP/1.1)
-> time_local: Str(2019-01-22 01:12:53.000)
-> user_agent: Str(Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html))
-> log.file.name: Str(access.log)
-> status: Str(301)
-> size: Str(178)
-> referer: Str(-)
-> remote_addr: Str(66.249.66.195)
-> request_type: Str(GET)
-> request_path: Str(/product/7564)
-> run_time: Str(0)
Trace ID:
Span ID:
Flags: 0
上面显示的是 OTel collector 生成的单条日志消息。我们将在后续部分展示如何将这些日志消息导入到 ClickHouse。
完整的日志消息 schema 和可能出现的附加字段(如果使用其他接收器)可以在此查看【https://opentelemetry.io/docs/specs/otel/logs/data-model/】。我们强烈建议用户阅读并熟悉这个 schema。
关键点是,日志行本身作为字符串存储在 Body 字段中,而通过 json_parser 操作符,JSON 数据被自动提取到 Attributes 字段中。该操作符还用于将时间戳提取到 Timestamp 列中。有关如何处理 OTel 日志的更多建议,请参见处理部分(Processing)。
操作符
操作符是日志处理的基本单元。每个操作符执行一个特定的任务,例如从文件中读取行或从字段中解析 JSON。多个操作符可以串联成一个处理管道,以实现所需的功能。
在上述日志消息中,未包含 TraceID 或 SpanID 字段。如果启用了分布式追踪,用户可以采用类似的方法,从 JSON 中提取这些字段。
对于需要收集本地或 Kubernetes 日志文件的用户,我们建议先了解 filelog 接收器的配置选项,特别是如何处理偏移量和多行日志解析的配置。
收集 Kubernetes 日志
对于 Kubernetes 日志的收集,我们建议参考 OpenTelemetry 的官方文档指南。我们推荐使用 Kubernetes Attributes Processor 来丰富日志和指标,特别是添加 pod 的元数据。这些元数据可能是动态生成的,例如标签,并将存储在 ResourceAttributes 列中。ClickHouse 当前将该列的数据类型设置为 Map(String, String)。有关如何处理和优化这种类型的更多信息,请参考《使用映射和从映射中提取数据》。
收集追踪
对于希望在代码中实施追踪并收集追踪数据的用户,我们建议按照 OpenTelemetry 的官方文档进行操作。
为了将追踪事件传送到 ClickHouse,用户需要部署 OTel collector,接收通过 OTLP 协议传输的追踪事件。OpenTelemetry 的示例演示了如何为每种支持的编程语言实施追踪,并将事件发送到 collector。以下是一个配置示例,展示了如何将事件输出到 stdout:
示例
由于追踪数据必须通过 OTLP 协议接收,我们使用 `telemetrygen` 工具来生成追踪数据。请按照此处的说明完成安装【https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/cmd/telemetrygen】。
以下配置通过 OTLP 接收器接收追踪事件,并将它们发送到 stdout。
配置文件: config-traces.xml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 1s
exporters:
logging:
loglevel: debug
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [logging]
通过以下命令运行配置:
./otelcol-contrib --config config-traces.yaml
使用 telemetrygen 向 collector 发送追踪事件:
$GOBIN/telemetrygen traces --otlp-insecure --traces 300
这将导致如下的追踪消息输出到 stdout:
Span #86
Trace ID : 1bb5cdd2c9df5f0da320ca22045c60d9
Parent ID : ce129e5c2dd51378
ID : fbb14077b5e149a0
Name : okey-dokey-0
Kind : Server
Start time : 2024-06-19 18:03:41.603868 +0000 UTC
End time : 2024-06-19 18:03:41.603991 +0000 UTC
Status code : Unset
Status message :
Attributes:
-> net.peer.ip: Str(1.2.3.4)
-> peer.service: Str(telemetrygen-client)
上面显示的是 OTel collector 生成的一条追踪消息。我们将在后续部分演示如何将这些消息导入到 ClickHouse。
追踪消息的完整 schema 会在此文档中进行维护【https://opentelemetry.io/docs/concepts/signals/traces/】。我们强烈建议用户熟悉这个 schema,以便更好地理解消息的结构。
处理 - 过滤、转换和丰富
正如我们在之前设置日志事件时间戳的示例中所展示的那样,用户往往需要对事件消息进行过滤、转换和丰富。这可以通过 OpenTelemetry 提供的多个功能来实现:
-
处理器-处理器接收接收器收集的数据,并在将数据发送到导出器之前进行修改或转换。处理器会按照在 collector 配置文件中定义的顺序依次应用。处理器是可选的,但通常推荐使用一个最小的处理器集。当将 OTel collector 与 ClickHouse 一起使用时,建议使用以下几种处理器:
-
内存限制器(memory_limiter):防止 collector 出现内存溢出的情况。有关如何配置内存限制,请参考《估算资源》。
-
基于上下文的富集处理器:例如,Kubernetes Attributes Processor,它能自动使用 k8s 元数据(如源 pod ID)来丰富 span、metric 和 log 的资源属性。
-
头部或尾部采样:如果需要追踪数据,可以使用头部或尾部采样。
-
基本过滤:在无法通过运算符实现时,可以丢弃不需要的事件(见下文)。
-
批处理:在与 ClickHouse 配合使用时,批处理是必须的,以确保数据能以批量方式发送到 ClickHouse。请参见《导出到 ClickHouse》【https://clickhouse.com/docs/en/observability/integrating-opentelemetry#exporting-to-clickhouse】。
-
-
运算符-运算符是日志处理的基本单元。它支持基本的字段解析,例如设置事件的严重性(Severity)和时间戳(Timestamp)。运算符支持 JSON 解析、正则表达式解析、事件过滤和简单的字段转换。我们建议将事件过滤放在此阶段进行。
我们建议避免在运算符或转换处理器中进行过度的事件处理。因为这些操作会消耗大量内存和 CPU,尤其是 JSON 解析。大部分数据处理可以通过物化视图和列在 ClickHouse 插入时进行,只有少部分需要上下文感知的富集操作(如添加 k8s 元数据)无法通过这种方式处理,仍需在 OTel collector 中处理。
如果处理发生在 OTel collector 中,建议将转换处理放在网关实例中,而尽量减少代理实例的处理工作。这样可以确保边缘代理(通常运行在服务器上)所需的资源尽可能少。一般来说,用户只会在代理实例中执行基本的过滤、时间戳设置和上下文相关的富集操作。例如,如果网关实例位于不同的 Kubernetes 集群中,k8s 元数据的富集将需要在代理实例中进行。
Example
以下示例配置展示了如何收集非结构化日志文件。在配置中,通过使用运算符(operators)对日志行进行解析(例如使用 `regex_parser` 提取结构化信息)并过滤事件。同时,还引入了处理器(processor)来批量处理事件并优化内存使用,以确保系统的稳定性和高效运行。
config-unstructured-logs-with-processor.yaml
receivers:
filelog:
include:
- /opt/data/logs/access-unstructured.log
start_at: beginning
operators:
- type: regex_parser
regex: '^(?P<ip>[\d.]+)\s+-\s+-\s+\[(?P<timestamp>[^\]]+)\]\s+"(?P<method>[A-Z]+)\s+(?P<url>[^\s]+)\s+HTTP/[^\s]+"\s+(?P<status>\d+)\s+(?P<size>\d+)\s+"(?P<referrer>[^"]*)"\s+"(?P<user_agent>[^"]*)"'
timestamp:
parse_from: attributes.timestamp
layout: '%d/%b/%Y:%H:%M:%S %z'
#22/Jan/2019:03:56:14 +0330
processors:
batch:
timeout: 1s
send_batch_size: 100
memory_limiter:
check_interval: 1s
limit_mib: 2048
spike_limit_mib: 256
exporters:
logging:
loglevel: debug
service:
pipelines:
logs:
receivers: [filelog]
processors: [batch, memory_limiter]
exporters: [logging]
./otelcol-contrib --config config-unstructured-logs-with-processor.yaml
导出到 ClickHouse
导出器负责将数据发送到一个或多个后端或目标。导出器可以采用拉取或推送方式。为了将事件推送到 ClickHouse,用户需要使用基于推送的 ClickHouse 导出器。
使用 OpenTelemetry Collector Contrib
ClickHouse 导出器属于 OpenTelemetry Collector Contrib 发行版,而不是核心发行版的一部分。用户可以选择使用 Contrib 发行版,或者根据需要自行构建 Collector。
以下是一个完整的配置文件示例:
clickhouse-config.yaml
receivers:
filelog:
include:
- /opt/data/logs/access-structured.log
start_at: beginning
operators:
- type: json_parser
timestamp:
parse_from: attributes.time_local
layout: '%Y-%m-%d %H:%M:%S'
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
send_batch_size: 5000
exporters:
clickhouse:
endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1
# ttl: 72h
traces_table_name: otel_traces
logs_table_name: otel_logs
create_schema: true
timeout: 5s
database: default
sending_queue:
queue_size: 1000
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
service:
pipelines:
logs:
receivers: [filelog]
processors: [batch]
exporters: [clickhouse]
traces:
receivers: [otlp]
processors: [batch]
exporters: [clickhouse]
配置文件中的关键设置包括:
-
pipelines:上面的配置展示了如何使用管道,管道包含接收器、处理器和导出器,用于处理日志和追踪数据。
-
endpoint:与 ClickHouse 的通信通过 endpoint 参数进行配置。连接字符串 tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1 表示通过 TCP 与 ClickHouse 进行通信。如果用户希望使用 HTTP 进行流量切换,可以按照此处的说明修改连接字符串【https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/clickhouseexporter/README.md#configuration-options】。详细的连接配置和如何在连接字符串中指定用户名和密码,请参考此文档【https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/clickhouseexporter/README.md#configuration-options】。
重要提示:上述连接字符串启用了压缩(lz4)和异步插入。我们建议始终启用这两项功能。有关异步插入的更多信息,请参见《批处理》。需要注意的是,压缩不会在旧版本的导出器中默认启用,因此建议显式指定启用压缩。
-
ttl:这个设置决定了数据的保留时间。更多信息请参考《管理数据》。TTL 应指定为小时数,例如 `72h`。在以下示例中,我们禁用了 TTL,因为数据来自 2019 年,ClickHouse 会在插入时立即删除这些数据。
-
traces_table_name 和 logs_table_name:这两个参数决定了日志和追踪数据表的名称。
-
create_schema:这个设置决定了是否在启动时创建默认架构的表,默认为 `true`,适用于入门。为了生产环境,用户应将其设置为 `false`,并定义自己的数据库架构。
-
database:目标数据库名称。
-
retry_on_failure:设置是否在批处理失败时重试。
-
batch:批处理处理器确保事件按批次发送。我们建议将批量大小设置为约 5000,并设置 5 秒的超时。无论哪个条件先达到,将触发批量处理并将数据刷新到 ClickHouse。降低这些值会减少数据的延迟,但可能会增加 ClickHouse 的连接数和接收到的批次数量。如果没有使用异步插入,这种做法可能会导致 ClickHouse 中的部分过多,因此不推荐使用。若使用异步插入,数据将在更短时间内可供查询,尽管具体时间取决于异步插入的设置。有关批量处理的详细信息,请参见《批处理》。
-
sending_queue:此设置控制发送队列的大小。每个队列项包含一个批次。如果队列满了,例如因为 ClickHouse 无法访问但事件仍然到达,超出队列的批次将被丢弃。
假设用户已提取结构化日志文件,并且本地 ClickHouse 实例正在运行(使用默认身份验证),用户可以通过以下命令运行此配置:
./otelcol-contrib --config clickhouse-config.yaml
若要将追踪数据发送到此 Collector,用户可以使用 telemetrygen 工具运行以下命令:
$GOBIN/telemetrygen traces --otlp-insecure --traces 300
运行后,通过简单的查询验证日志事件是否已成功导入:
SELECT *
FROM otel_logs
LIMIT 1
FORMAT Vertical
Row 1:
──────
Timestamp: 2019-01-22 06:46:14.000000000
TraceId:
SpanId:
TraceFlags: 0
SeverityText:
SeverityNumber: 0
ServiceName:
Body: {"remote_addr":"109.230.70.66","remote_user":"-","run_time":"0","time_local":"2019-01-22 06:46:14.000","request_type":"GET","request_path":"\/image\/61884\/productModel\/150x150","request_protocol":"HTTP\/1.1","status":"200","size":"1684","referer":"https:\/\/www.zanbil.ir\/filter\/p3%2Cb2","user_agent":"Mozilla\/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko\/20100101 Firefox\/64.0"}
ResourceSchemaUrl:
ResourceAttributes: {}
ScopeSchemaUrl:
ScopeName:
ScopeVersion:
ScopeAttributes: {}
LogAttributes: {'referer':'https://www.zanbil.ir/filter/p3%2Cb2','log.file.name':'access-structured.log','run_time':'0','remote_user':'-','request_protocol':'HTTP/1.1','size':'1684','user_agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0','remote_addr':'109.230.70.66','request_path':'/image/61884/productModel/150x150','status':'200','time_local':'2019-01-22 06:46:14.000','request_type':'GET'}
1 row in set. Elapsed: 0.012 sec. Processed 5.04 thousand rows, 4.62 MB (414.14 thousand rows/s., 379.48 MB/s.)
Peak memory usage: 5.41 MiB.
Likewise, for trace events, users can check the `otel_traces` table:
SELECT *
FROM otel_traces
LIMIT 1
FORMAT Vertical
Row 1:
──────
Timestamp: 2024-06-20 11:36:41.181398000
TraceId: 00bba81fbd38a242ebb0c81a8ab85d8f
SpanId: beef91a2c8685ace
ParentSpanId:
TraceState:
SpanName: lets-go
SpanKind: SPAN_KIND_CLIENT
ServiceName: telemetrygen
ResourceAttributes: {'service.name':'telemetrygen'}
ScopeName: telemetrygen
ScopeVersion:
SpanAttributes: {'peer.service':'telemetrygen-server','net.peer.ip':'1.2.3.4'}
Duration: 123000
StatusCode: STATUS_CODE_UNSET
StatusMessage:
Events.Timestamp: []
Events.Name: []
Events.Attributes: []
Links.TraceId: []
Links.SpanId: []
Links.TraceState: []
Links.Attributes: []
开箱即用的架构
默认情况下,ClickHouse 导出器为日志和追踪数据分别创建目标日志表。用户可以通过设置 create_schema 来禁用此功能,并且可以自定义日志表和追踪表的名称,默认为 otel_logs 和 otel_traces。
注意
以下架构假设已启用 TTL,TTL 设置为 72 小时。
下面展示的是日志的默认架构(otelcol-contrib v0.102.1):
CREATE TABLE default.otel_logs
(
`Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
`TraceId` String CODEC(ZSTD(1)),
`SpanId` String CODEC(ZSTD(1)),
`TraceFlags` UInt32 CODEC(ZSTD(1)),
`SeverityText` LowCardinality(String) CODEC(ZSTD(1)),
`SeverityNumber` Int32 CODEC(ZSTD(1)),
`ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
`Body` String CODEC(ZSTD(1)),
`ResourceSchemaUrl` String CODEC(ZSTD(1)),
`ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
`ScopeSchemaUrl` String CODEC(ZSTD(1)),
`ScopeName` String CODEC(ZSTD(1)),
`ScopeVersion` String CODEC(ZSTD(1)),
`ScopeAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
`LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_scope_attr_key mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_scope_attr_value mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(3)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
这些列符合 OpenTelemetry 官方文档中定义的日志规范【https://opentelemetry.io/docs/specs/otel/logs/data-model/】。
关于该架构的几点重要说明:
-
分区:默认情况下,表使用 PARTITION BY toDate(Timestamp) 按日期进行分区。这样可以高效地删除过期的数据。
-
TTL 设置:TTL 通过 TTL toDateTime(Timestamp) + toIntervalDay(3) 进行设置,与收集器配置中的 TTL 设置保持一致。通过 ttl_only_drop_parts=1 配置,只有在所有行过期时,整个数据块才会被删除。这样比删除数据块中的单个行更高效,避免了代价较大的删除操作。我们建议始终启用此选项。有关更多细节,请参见《使用 TTL 管理数据》【https://clickhouse.com/docs/en/observability/managing-data#data-management-with-ttl-time-to-live】。
-
MergeTree 引擎:该表使用经典的 MergeTree 引擎,适用于日志和追踪数据,通常不需要修改。
-
表排序:表按 (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId) 排序,意味着查询会根据这些字段的顺序优化。例如,按 `ServiceName` 过滤的查询速度将比按 `TraceId` 过滤更快。建议根据用户的查询模式调整表的排序方式,具体内容参见《选择主键》【https://clickhouse.com/docs/en/observability/schema-design#choosing-a-primary-ordering-key】。
-
压缩和编码:架构应用了 ZSTD(1) 压缩,提供最佳的日志压缩效果。用户可以提高 ZSTD 压缩级别来提高压缩率,尽管这通常对日志数据的压缩效果没有显著提升,而且会增加插入时的 CPU 开销。关于压缩的详细信息请参考相关文档。`Timestamp` 列还应用了增量编码以减小其磁盘占用。
-
映射类型:ResourceAttributes、LogAttributes 和 ScopeAttributes 是映射类型,用户应了解它们之间的区别。有关如何访问和优化这些映射的键,参见《使用映射》【https://clickhouse.com/docs/en/observability/schema-design#using-maps】。
-
优化类型:其他列类型,如 ServiceName 采用 LowCardinality 类型,以减少内存使用并提高查询效率。尽管示例日志中的 Body 是 JSON 格式,它被存储为字符串以提高查询性能。
-
布隆过滤器:布隆过滤器被应用于映射键、映射值和 Body 列,这有助于提高这些列的查询性能,但通常不需要启用。更多详情请参见《二级索引/数据跳过索引》【https://clickhouse.com/docs/en/observability/schema-design#secondarydata-skipping-indices】。
CREATE TABLE default.otel_traces
(
`Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
`TraceId` String CODEC(ZSTD(1)),
`SpanId` String CODEC(ZSTD(1)),
`ParentSpanId` String CODEC(ZSTD(1)),
`TraceState` String CODEC(ZSTD(1)),
`SpanName` LowCardinality(String) CODEC(ZSTD(1)),
`SpanKind` LowCardinality(String) CODEC(ZSTD(1)),
`ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
`ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
`ScopeName` String CODEC(ZSTD(1)),
`ScopeVersion` String CODEC(ZSTD(1)),
`SpanAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
`Duration` Int64 CODEC(ZSTD(1)),
`StatusCode` LowCardinality(String) CODEC(ZSTD(1)),
`StatusMessage` String CODEC(ZSTD(1)),
`Events.Timestamp` Array(DateTime64(9)) CODEC(ZSTD(1)),
`Events.Name` Array(LowCardinality(String)) CODEC(ZSTD(1)),
`Events.Attributes` Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1)),
`Links.TraceId` Array(String) CODEC(ZSTD(1)),
`Links.SpanId` Array(String) CODEC(ZSTD(1)),
`Links.TraceState` Array(String) CODEC(ZSTD(1)),
`Links.Attributes` Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1)),
INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_span_attr_key mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_span_attr_value mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_duration Duration TYPE minmax GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(3)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
架构和优化建议
与上述日志架构类似,ClickHouse 导出器为追踪数据也创建了目标表。架构中增加了与跨度 (span) 相关的 Link 列,这些列与 OTel 官方的追踪规范对接。
我们建议用户禁用自动创建表的功能,而是手动创建表。这将使得用户能够根据自己的需求修改主键和副键,并可根据查询需求添加额外的列,从而优化查询性能。更多详细的架构设计建议请参考《架构设计》【https://clickhouse.com/docs/en/observability/schema-design】。
优化数据插入
为了在保证强一致性和高性能插入的前提下,用户需要遵循一些基本的规则来将可观测性数据通过收集器插入到 ClickHouse 中。只要正确配置了 OTel 收集器,遵循这些规则就非常简单,这样可以避免用户首次使用 ClickHouse 时常见的一些问题。
批量插入
默认情况下,每个插入操作会导致 ClickHouse 创建一个存储分区,包含插入数据及相关元数据。因此,相比于发送多个小批次的插入,发送少量但包含更多数据的批次可以减少所需的写入操作次数。我们建议每次插入至少包含 1,000 行数据。更多详情请参考相关文档。
插入到 ClickHouse 的操作是同步的,并且在数据相同的情况下是幂等的。对于 MergeTree 引擎的表,ClickHouse 会默认去重重复的插入操作。这意味着,在以下情况中插入操作是容错的:
-
(1) 如果接收数据的节点出现问题,插入操作会超时(或返回更详细的错误),且不会收到确认。
-
(2) 如果数据已成功写入节点,但由于网络中断未能将确认返回给查询发起方,发起方会收到超时或网络错误。
从收集器的角度来看,(1) 和 (2) 的差别较难识别,但无论是哪种情况,未确认的插入可以立即重试。只要重试的插入数据与原始数据一致且顺序相同,ClickHouse 会自动忽略重试的操作。
我们建议用户使用前述配置中的批处理器,以确保数据插入按要求进行批量处理。这将确保插入操作以一致的数据批次方式发送。如果预计收集器的吞吐量较高,每次插入可以处理至少 5000 个事件,那么这通常是管道中唯一需要的批量插入配置。这样,收集器会在批处理超时之前刷新数据批次,确保管道的端到端延迟保持低,并且数据批次的大小保持一致。
使用异步插入
当收集器的吞吐量较低时,用户通常会被迫发送较小的批次,但仍希望数据能够在最小的端到端延迟内到达 ClickHouse。在这种情况下,当批处理超时后,小批次会被发送。这种方式可能会导致性能问题,因此需要使用异步插入。通常,只有当收集器作为代理角色并被配置为直接将数据发送到 ClickHouse 时,才会遇到此问题。通过在网关层进行聚合,能够有效缓解这一问题——详见《通过网关扩展》。
如果不能保证大批量插入的操作,用户可以选择使用异步插入,让 ClickHouse 负责批量处理数据。异步插入的工作原理是:首先,数据会被插入到一个内存缓冲区中,之后数据会被异步写入到数据库存储中。

启用异步插入
启用异步插入时,ClickHouse 会按照以下流程处理数据:① 当收到插入请求时,数据会立即 ② 写入内存缓冲区;然后 ③ 在下一次缓冲区刷新时,这些数据会被排序,并作为一部分写入到数据库存储中。值得注意的是,直到数据被刷新到存储中,查询是无法访问这些数据的;刷新过程是可以配置的。
要启用异步插入,用户需要在连接字符串中添加 async_insert=1。我们建议用户启用 wait_for_async_insert=1(默认设置)来确保数据的可靠交付——更多信息请参见文档【https://clickhouse.com/blog/asynchronous-data-inserts-in-clickhouse】。
当缓冲区被刷新时,异步插入的数据会被写入数据库。这会发生在超出 async_insert_max_data_size 设置的阈值后,或者在首次执行 INSERT 查询后经过 async_insert_busy_timeout_ms 毫秒。如果 async_insert_stale_timeout_ms 设置为非零值,则数据会在上次查询后的 async_insert_stale_timeout_ms 毫秒后插入。用户可以根据需要调整这些参数,以控制数据处理的延迟。更多关于缓冲区刷新设置的细节可参考文档【https://clickhouse.com/docs/en/operations/settings/settings#asynchronous-insert-settings】。通常情况下,默认设置已经足够。
考虑自适应异步插入
如果系统中使用的代理较少,且吞吐量不高,但要求严格的端到端延迟时,自适应异步插入可能会有所帮助。然而,对于 ClickHouse 等高吞吐量的应用场景,这种方法通常不太适用。
最后需要注意,异步插入时默认不会启用同步插入的去重功能。如果需要,用户可以参考 async_insert_deduplicate 设置来启用去重。
有关如何配置此功能的详细信息,请参见文档,深入了解请参阅相关材料【https://clickhouse.com/docs/en/optimize/asynchronous-inserts#enabling-asynchronous-inserts】。
部署架构
在使用 OTel 收集器与 ClickHouse 的过程中,可以选择几种不同的部署架构。以下是每种架构的介绍,以及它们适用的场景。
仅代理架构
在仅代理架构中,用户将 OTel 收集器作为代理部署到网络边缘。代理会接收来自本地应用程序(例如作为 sidecar 容器运行的应用)的追踪数据,并从服务器及 Kubernetes 节点收集日志。在此模式下,代理会直接将数据发送到 ClickHouse。

适用场景
这种架构适用于小型或中型规模的部署。它的主要优势在于不需要额外的硬件资源,同时也能确保 ClickHouse 的资源占用最小,应用程序与收集器之间的映射关系也非常简单。
然而,当代理数量超过几百个时,建议考虑迁移到基于网关的架构。这种仅代理架构有一些劣势,限制了其扩展性:
-
连接扩展性 - 每个代理都会与 ClickHouse 建立连接。虽然 ClickHouse 能够处理数百甚至上千个并发连接,但当连接数增多时,这会成为瓶颈,导致插入效率下降,因为更多资源将用于维持这些连接。使用网关可以减少连接数量,从而提升插入效率。
-
边缘计算限制 - 在此架构中,所有的转换或事件处理都必须在边缘节点或直接在 ClickHouse 中完成。这既增加了架构复杂性,又可能需要在 ClickHouse 内部使用复杂的物化视图,或者将大量计算推向边缘,进而影响关键服务,甚至导致资源短缺。
-
小批次和延迟问题 - 代理收集器可能会一次只收集到很少的事件,这意味着它们必须按一定的时间间隔刷新数据,以满足交付服务水平协议(SLA)。这种情况通常会导致收集器将小批次数据发送到 ClickHouse,虽然这会造成一些性能瓶颈,但可以通过启用异步插入来改善这一问题——具体方法请见“优化插入”章节。
通过网关扩展
OTel 收集器可以作为网关实例进行部署,以解决之前提到的架构局限性。这些网关通常是按数据中心或区域划分的独立服务。它们通过一个 OTLP 端点接收来自应用程序(或其他代理角色收集器)的事件。通常会部署多个网关实例,并使用负载均衡器将负载合理分配到这些实例上。

架构目标
这种架构的主要目标是将计算密集型的处理任务从代理端转移到网关,从而减轻代理的资源负担。网关可以执行原本由代理完成的转换任务,并且通过聚合来自多个代理的事件,确保向 ClickHouse 发送大批量数据,优化插入性能。随着代理数量的增加以及事件吞吐量的上升,网关架构能够灵活扩展,适应更大的负载需求。以下是网关配置示例,配合使用示例结构化日志文件的代理配置。请注意,代理和网关之间使用 OTLP 进行通信。
配置文件示例:
clickhouse-agent-config.yaml
receivers:
filelog:
include:
- /opt/data/logs/access-structured.log
start_at: beginning
operators:
- type: json_parser
timestamp:
parse_from: attributes.time_local
layout: '%Y-%m-%d %H:%M:%S'
processors:
batch:
timeout: 5s
send_batch_size: 1000
exporters:
otlp:
endpoint: localhost:4317
tls:
insecure: true # Set to false if you are using a secure connection
service:
telemetry:
metrics:
address: 0.0.0.0:9888 # Modified as 2 collectors running on same host
pipelines:
logs:
receivers: [filelog]
processors: [batch]
exporters: [otlp]
clickhouse-gateway-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
send_batch_size: 10000
exporters:
clickhouse:
endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4
ttl: 96h
traces_table_name: otel_traces
logs_table_name: otel_logs
create_schema: true
timeout: 10s
database: default
sending_queue:
queue_size: 10000
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouse]
可以通过以下命令运行这些配置。
./otelcol-contrib --config clickhouse-gateway-config.yaml
./otelcol-contrib --config clickhouse-agent-config.yaml
架构的劣势
该架构的主要缺点是管理多个收集器所带来的额外成本和开销。
若想了解如何管理更大规模的网关架构以及相关经验,可以参考这篇博客文章【https://clickhouse.com/blog/building-a-logging-platform-with-clickhouse-and-saving-millions-over-datadog】。
添加 Kafka
读者可能会注意到,上述架构中并没有使用 Kafka 作为消息队列。
使用 Kafka 作为消息缓冲区是日志架构中广泛应用的一种设计模式,特别是在 ELK 堆栈中。这种做法带来了一些优点,尤其是在提供更强的消息投递保证和缓解背压方面。消息从代理收集器传送到 Kafka,并被写入磁盘。理论上,集群化的 Kafka 实例应该提供高吞吐量的消息缓冲,因为它将数据线性写入磁盘的成本远低于解析和处理消息——例如,在 Elastic 中,分词和索引的过程会消耗大量计算资源。通过将数据移出代理端,还可以减少由于源端日志轮换而丢失消息的风险。此外,Kafka 还提供了消息重发和跨区域复制的功能,这对于某些使用场景可能非常有价值。
然而,ClickHouse 的插入速度非常快,在中等硬件下每秒可以插入百万行数据,通常 ClickHouse 的背压很少出现。因此,使用 Kafka 队列往往意味着增加架构复杂性和成本。如果你可以接受日志不需要像银行交易那样强的投递保证,我们建议避免使用 Kafka,以简化架构。
不过,如果你确实需要高投递保证或数据重放能力(例如重放到多个目标),Kafka 仍然是一个有用的架构扩展。

在这种情况下,OTel 代理可以通过 Kafka 导出器将数据发送到 Kafka,网关实例则通过 Kafka 接收器消费这些消息。具体配置可以参考 Confluent 和 OTel 的文档。
资源估算
OTel 收集器的资源需求会根据事件吞吐量、消息大小和处理量的不同而有所不同。OpenTelemetry 项目提供了基准测试,用户可以参考这些测试来估算资源需求。
根据我们的经验,一个配置了 3 个核心和 12GB 内存的网关实例,可以处理每秒约 60,000 个事件。这假设只有最基本的处理管道,主要负责字段重命名,没有使用正则表达式。
对于负责将事件传送到网关的代理实例,如果只是设置事件的时间戳,建议用户根据预期的日志速率来调整资源。以下是推荐的起始资源配置:
| 日志速率 | 代理资源需求 |
| 1k/秒 | 0.2 CPU,0.2 GiB |
| 5k/秒 | 0.5 CPU,0.5 GiB |
| 10k/秒 | 1 CPU,1 GiB |
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com


被折叠的 条评论
为什么被折叠?



