跳至主要内容

集成 OpenTelemetry 用于数据收集

任何可观测性解决方案都需要一种收集和导出日志和跟踪的方法。为此,ClickHouse 建议使用 OpenTelemetry (OTel) 项目

"OpenTelemetry 是一个可观测性框架和工具包,旨在创建和管理遥测数据,例如跟踪、指标和日志。"

与 ClickHouse 或 Prometheus 不同,OpenTelemetry 不是可观测性后端,而是专注于遥测数据的生成、收集、管理和导出。虽然 OpenTelemetry 的最初目标是允许用户使用特定于语言的 SDK 轻松地为其应用程序或系统添加检测功能,但它已扩展到通过 OpenTelemetry 收集器(接收、处理和导出遥测数据的代理或代理)收集日志。

ClickHouse 相关组件

Open Telemetry 由许多组件组成。除了为字段/列提供数据和 API 规范、标准化协议和命名约定外,OTeL 还提供了两种对使用 ClickHouse 构建可观测性解决方案至关重要的功能

  • OpenTelemetry 收集器 是一个代理,用于接收、处理和导出遥测数据。基于 ClickHouse 的解决方案使用此组件进行日志收集和事件处理,然后进行批处理和插入。
  • 语言 SDK 实现规范、API 和遥测数据的导出。这些 SDK 有效地确保跟踪在应用程序的代码中正确记录,生成组成跨度并确保上下文通过元数据跨服务传播 - 从而形成分布式跟踪并确保跨度可以关联。这些 SDK 辅以一个自动实现常用库和框架的生态系统,因此用户无需更改其代码即可获得开箱即用的检测功能。

基于 ClickHouse 的可观测性解决方案利用了这两种工具。

分发版

OpenTelemetry 收集器具有 多个分发版。基于 ClickHouse 的解决方案所需的 filelog 接收器以及 ClickHouse 导出器仅存在于 OpenTelemetry 收集器 Contrib 分发版 中。

此分发版包含许多组件,并允许用户尝试各种配置。但是,在生产环境中运行时,建议将收集器限制为仅包含环境所需的组件。一些这样做的原因

  • 减小收集器的大小,减少收集器的部署时间
  • 通过减少可用的攻击面来提高收集器的安全性

可以使用 OpenTelemetry 收集器构建器 构建 自定义收集器

使用 OTel 摄取数据

收集器部署角色

为了收集日志并将它们插入 ClickHouse,我们建议使用 OpenTelemetry 收集器。OpenTelemetry 收集器可以部署在两个主要角色中

  • 代理 - 代理实例在边缘收集数据,例如在服务器上或在 Kubernetes 节点上,或直接从应用程序(使用 OpenTelemetry SDK 检测)接收事件。在后一种情况下,代理实例与应用程序一起运行或与应用程序在同一主机上运行(例如 sidecar 或 DaemonSet)。代理可以将其数据直接发送到 ClickHouse 或发送到网关实例。在前一种情况下,这称为 代理部署模式
  • 网关 - 网关实例提供一个独立的服务(例如,Kubernetes 中的部署),通常每个集群、每个数据中心或每个区域一个。它们通过单个 OTLP 端点接收来自应用程序(或其他收集器作为代理)的事件。通常,会部署一组网关实例,并使用开箱即用的负载均衡器在它们之间分配负载。如果所有代理和应用程序都将其信号发送到此单个端点,则通常称为 网关部署模式

下面我们假设一个简单的代理收集器,将其事件直接发送到 ClickHouse。有关使用网关以及何时适用网关的更多详细信息,请参阅“使用网关进行扩展”。

收集日志

使用收集器的主要优点是它允许您的服务快速卸载数据,让收集器负责其他处理,例如重试、批处理、加密甚至敏感数据过滤。

收集器使用术语 接收器处理器导出器 表示其三个主要处理阶段。接收器用于数据收集,可以是拉取或推送类型的。处理器提供执行消息转换和丰富的能力。导出器负责将数据发送到下游服务。虽然此服务理论上可以是另一个收集器,但在下面的初始讨论中,我们假设所有数据都直接发送到 ClickHouse。

NEEDS ALT

我们建议用户熟悉接收器、处理器和导出器的完整集合。

收集器提供了两个主要的接收器来收集日志

通过 OTLP - 在这种情况下,日志通过 OTLP 协议直接从 OpenTelemetry SDK 推送到收集器。 OpenTelemetry 演示 使用这种方法,每种语言中的 OTLP 导出器都假设一个本地收集器端点。在这种情况下,必须使用 OTLP 接收器配置收集器 - 请参阅上面的 演示以获取配置。这种方法的优点是日志数据将自动包含跟踪 ID,允许用户稍后识别特定日志的跟踪,反之亦然。

NEEDS ALT

此方法要求用户使用其 相应的语言 SDK 为其代码添加检测功能。

  • 通过 Filelog 接收器抓取 - 此接收器跟踪磁盘上的文件并制定日志消息,然后将这些消息发送到 ClickHouse。此接收器处理检测多行消息、处理日志轮换、健壮性检查点以重启以及提取结构等复杂任务。此接收器还可以跟踪 Docker 和 Kubernetes 容器日志,可以作为 helm chart 部署, 从中提取结构 并使用 pod 详细信息丰富它们。
NEEDS ALT

大多数部署将使用上述方法的组合。我们建议用户阅读 收集器文档 并熟悉基本概念,以及 配置结构安装方法

提示:otelbin.io 可用于验证和可视化配置。

结构化与非结构化

日志可以是结构化的或非结构化的。

结构化日志将使用 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 万行,可通过以下链接获取

我们在下面的示例中使用结构化数据集。请确保已下载此文件并解压缩以重现以下示例。

以下内容表示 OTel Collector 的简单配置,它使用 filelog 接收器读取磁盘上的这些文件,并将结果消息输出到标准输出。由于我们的日志是结构化的,因此我们使用json_parser 运算符。修改 access-structured.log 文件的路径。

下面的示例从日志中提取时间戳。这需要使用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]

用户可以按照官方说明在本地安装 Collector。重要的是,请确保修改说明以使用contrib 发行版(包含filelog 接收器),例如,用户将下载otelcol-contrib_0.102.1_darwin_arm64.tar.gz 而不是otelcol_0.102.1_darwin_arm64.tar.gz。发行版可以在这里找到。

安装完成后,可以使用以下命令运行 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。

日志消息的完整模式以及使用其他接收器时可能存在的其他列,在这里维护。我们强烈建议用户熟悉此模式。

这里的关键是日志行本身作为字符串保存在Body 字段中,但由于json_parser,JSON 已自动提取到 Attributes 字段。相同的运算符已被用于将时间戳提取到相应的Timestamp 列。有关使用 Otel 处理日志的建议,请参阅“处理”。

运算符是日志处理的最基本单位。每个运算符都承担一项单一职责,例如从文件读取行或从字段解析 JSON。然后将运算符链接在一起形成管道以实现所需的结果。

上述消息没有 TraceID 或 SpanID 字段。如果存在,例如在用户正在实现分布式跟踪的情况下,可以使用上面显示的相同技术从 JSON 中提取这些字段。

对于需要收集本地或 Kubernetes 日志文件的用户,我们建议用户熟悉filelog 接收器可用的配置选项以及如何处理偏移量多行日志解析

收集 Kubernetes 日志

对于 Kubernetes 日志的收集,我们建议使用Open Telemetry 文档指南Kubernetes 属性处理器建议用于使用 Pod 元数据丰富日志和指标。这可能会生成动态元数据(例如标签),存储在ResourceAttributes 列中。ClickHouse 目前为此列使用类型Map(String, String)。有关处理和优化此类型的更多详细信息,请参阅“使用映射”和“从映射中提取”。

收集跟踪

对于希望检测其代码并收集跟踪的用户,我们建议遵循官方的OTel 文档

为了将事件传递到 ClickHouse,用户将需要部署一个 OTel Collector 来通过适当的接收器通过 OTLP 协议接收跟踪事件。OpenTelemetry 演示提供了一个每个受支持语言的检测示例并将事件发送到 Collector。下面显示了一个将事件输出到标准输出的适当 Collector 配置示例

示例

由于必须通过 OTLP 接收跟踪,因此我们使用telemetrygen 工具生成跟踪数据。请按照此处的说明进行安装。

以下配置在将跟踪事件发送到标准输出之前,在 OTLP 接收器上接收跟踪事件。

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

这将导致类似于以下示例的跟踪消息输出到标准输出

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。

跟踪消息的完整模式在这里维护。我们强烈建议用户熟悉此模式。

处理 - 过滤、转换和丰富

如前面设置日志事件时间戳的示例所示,用户总是希望过滤、转换和丰富事件消息。这可以通过 Open Telemetry 中的多种功能来实现

  • 处理器 - 处理器接收接收器收集的数据并修改或转换它,然后再将其发送到导出器。处理器按 Collector 配置中processors 部分配置的顺序应用。这些是可选的,但最小集合通常是推荐的。在将 OTeL Collector 与 ClickHouse 一起使用时,我们建议将处理器限制为

    • 使用memory_limiter 防止 Collector 出现内存不足的情况。有关建议,请参阅“估算资源”。
    • 任何基于上下文的丰富处理器。例如,Kubernetes 属性处理器允许使用 k8s 元数据自动设置跨度、指标和日志资源属性,例如使用其源 Pod ID 丰富事件。
    • 如果需要,则使用尾部或头部采样进行跟踪。
    • 基本过滤 - 如果无法通过运算符(见下文)完成,则丢弃不需要的事件。
    • 批处理 - 在使用 ClickHouse 时至关重要,以确保数据以批次发送。请参阅“导出到 ClickHouse”。
  • 运算符 - 运算符提供接收器可用的最基本处理单元。支持基本解析,允许设置严重性和时间戳等字段。此处支持 JSON 和正则表达式解析以及事件过滤和基本转换。我们建议在此处执行事件过滤。

我们建议用户避免使用运算符或转换处理器进行过多的事件处理。这可能会导致相当大的内存和 CPU 开销,尤其是 JSON 解析。可以使用物化视图和某些列在 ClickHouse 中插入时完成所有处理,但有一些例外 - 特别是上下文感知丰富(例如添加 k8s 元数据)。有关更多详细信息,请参阅“使用 SQL 提取结构”。

如果使用 OTel Collector 进行处理,我们建议在网关实例上进行转换,并最大限度地减少在代理实例上完成的任何工作。这将确保在服务器上运行的边缘代理所需的资源尽可能少。通常,我们看到用户仅执行过滤(以最大限度地减少不必要的网络使用)、时间戳设置(通过运算符)和丰富,这需要代理中的上下文。例如,如果网关实例驻留在不同的 Kubernetes 集群中,则需要在代理中进行 k8s 丰富。

示例

以下配置显示了非结构化日志文件的收集。请注意,使用运算符从日志行(regex_parser)中提取结构并过滤事件,以及使用处理器对事件进行批处理并限制内存使用。

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 导出器

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://127.0.0.1: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 - 上述配置重点介绍了 pipelines 的使用,它由一组接收器、处理器和导出器组成,分别用于日志和跟踪。
  • endpoint - 通过 endpoint 参数配置与 ClickHouse 的通信。连接字符串 tcp://127.0.0.1:9000?dial_timeout=10s&compress=lz4&async_insert=1 会导致通信通过 TCP 进行。如果用户出于流量切换的原因更喜欢 HTTP,请按照 此处 所述修改此连接字符串。完整的连接详细信息,以及在连接字符串中指定用户名和密码的功能,在 此处 描述。

重要:请注意,上述连接字符串同时启用了压缩 (lz4) 和异步插入。我们建议始终启用这两项。有关异步插入的更多详细信息,请参阅“批处理”。压缩应始终指定,并且在旧版本的导出器中默认情况下不会启用。

  • ttl - 此处的值决定了数据保留的时间长度。更多详细信息请参阅“管理数据”。这应以小时为单位指定,例如 72h。我们在下面的示例中禁用了 TTL,因为我们的数据来自 2019 年,如果插入,ClickHouse 会立即将其删除。
  • traces_table_namelogs_table_name - 确定日志和跟踪表的名称。
  • create_schema - 确定是否在启动时使用默认模式创建表。默认为 true,以便于入门。用户应将其设置为 false 并定义自己的模式。
  • database - 目标数据库。
  • retry_on_failure - 设置以确定是否应重试失败的批次。
  • batch - 批处理程序确保事件以批次的形式发送。我们建议使用大约 5000 的值,超时时间为 5 秒。这两个值中先到达哪个值,就会启动将批次刷新到导出器的操作。降低这些值意味着延迟更低的 pipeline,数据可以更快地查询,但代价是发送到 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_logsotel_traces 修改,如上所述。

在下面的模式中,我们假设 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

此处的列与 此处 文档记录的 OTel 日志官方规范相关联。

关于此模式的一些重要说明

  • 默认情况下,表按日期分区,方法是 PARTITION BY toDate(Timestamp)。这使得删除过期数据变得高效。
  • TTL 通过 TTL toDateTime(Timestamp) + toIntervalDay(3) 设置,并且对应于 Collector 配置中设置的值。 ttl_only_drop_parts=1 意味着当所有包含的行都过期时,只删除整个部分。这比删除部分内的行效率更高,后者会导致代价高昂的删除操作。我们建议始终设置此项。有关更多详细信息,请参阅“使用 TTL 进行数据管理”。
  • 该表使用经典的 MergeTree 引擎。这对于日志和跟踪是推荐的,不需要更改。
  • 该表按 ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId) 排序。这意味着查询将针对 ServiceName、SeverityText、Timestamp 和 TraceId 的筛选器进行优化 - 列表中较早的列的筛选速度将快于较晚的列,例如,按 ServiceName 筛选的速度将明显快于按 TraceId 筛选。用户应根据其预期的访问模式修改此排序 - 请参阅“选择主键”。
  • 上述模式将 ZSTD(1) 应用于列。这为日志提供了最佳的压缩效果。用户可以提高 ZSTD 压缩级别 (1 以上) 以获得更好的压缩效果,尽管这很少有益。提高此值会导致插入时 (压缩期间) CPU 开销更大,尽管解压缩 (以及查询) 应保持可比。有关更多详细信息,请参阅 此处Timestamp 应用了额外的 增量编码,目的是减小其在磁盘上的大小。
  • 请注意 ResourceAttributesLogAttributesScopeAttributes 是如何映射的。用户应熟悉它们之间的区别。有关如何访问这些映射以及优化访问其中的键的方法,请参阅“使用映射”。
  • 此处大多数其他类型(例如 ServiceName 作为 LowCardinality)都已优化。请注意,Body 虽然在我们的示例日志中是 JSON,但存储为字符串。
  • Bloom 过滤器应用于映射键和值,以及 Body 列。这些旨在提高访问这些列的查询时间,但通常不需要。请参阅下面的“辅助/数据跳过索引”。
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

同样,这将与 此处 文档记录的 OTel 跟踪官方规范中的列相关联。此处的模式采用了与上述日志模式相同的许多设置,并增加了特定于跨度的 Link 列。

我们建议用户禁用自动模式创建并手动创建表。这允许修改主键和辅助键,以及有机会引入其他列以优化查询性能。有关更多详细信息,请参阅“模式设计”。

优化插入

为了在获得强一致性保证的同时实现高插入性能,用户在通过 Collector 将可观察性数据插入 ClickHouse 时应遵守简单的规则。通过正确配置 OTel Collector,以下规则应该很容易遵循。这也避免了用户首次使用 ClickHouse 时遇到的 常见问题

批处理

默认情况下,发送到 ClickHouse 的每个插入都会导致 ClickHouse 立即创建存储的一部分,其中包含插入的数据以及需要存储的其他元数据。因此,与发送大量包含较少数据的插入相比,发送少量包含更多数据的插入将减少所需的写入次数。我们建议一次以至少 1000 行的相当大的批次插入数据。更多详细信息 此处

默认情况下,插入 ClickHouse 是同步的,如果相同则幂等的。对于合并树引擎系列的表,ClickHouse 默认情况下会自动 对插入进行重复数据删除。这意味着插入在以下情况下是容错的

  1. 如果接收数据的节点出现问题,插入查询将超时(或收到更具体的错误)并且不会收到确认。
  2. 如果数据已由节点写入,但由于网络中断无法将确认返回给查询的发件人,则发件人将收到超时或网络错误。

从 Collector 的角度来看,(1) 和 (2) 很难区分。但是,在这两种情况下,未确认的插入都可以立即重试。只要重试的插入查询包含相同的数据并按相同的顺序排列,ClickHouse 就会自动忽略重试的插入(如果未确认的原始插入成功)。

我们建议用户使用前面配置中显示的 批处理程序 来满足上述要求。这确保插入作为一致的行批次发送,满足上述要求。如果预计 Collector 具有高吞吐量(每秒事件数),并且每个插入可以发送至少 5000 个事件,则这通常是 pipeline 中唯一需要的批处理。在这种情况下,Collector 将在批处理程序的 timeout 到达之前刷新批次,确保 pipeline 的端到端延迟保持较低,并且批次大小一致。

使用异步插入

通常,当 Collector 的吞吐量低时,用户被迫发送较小的批次,但他们仍然希望数据在最小的端到端延迟内到达 ClickHouse。在这种情况下,当批处理程序的 timeout 到期时,将发送小批次。这可能会导致问题,这时需要异步插入。这种情况通常发生在**代理角色中的 Collector 配置为直接发送到 ClickHouse**时。网关通过充当聚合器可以缓解此问题 - 请参阅“使用网关进行扩展”。

如果无法保证大批次,用户可以使用 异步插入 将批处理委派给 ClickHouse。使用异步插入,数据首先插入缓冲区,然后异步地分别写入数据库存储。

NEEDS ALT

当启用异步插入时,ClickHouse ①接收到插入查询后,会先将查询数据 ②立即写入内存缓冲区。当 ③下一个缓冲区刷新发生时,缓冲区中的数据会被排序并作为一部分写入数据库存储。请注意,在数据刷新到数据库存储之前,查询无法搜索到这些数据;缓冲区刷新是可配置的

要为采集器启用异步插入,请在连接字符串中添加async_insert=1。我们建议用户使用wait_for_async_insert=1(默认值)以获得交付保证 - 请参阅此处了解详细信息。

异步插入的数据在 ClickHouse 缓冲区刷新后才会插入。这发生在超过async_insert_max_data_size 或自第一个 INSERT 查询起超过async_insert_busy_timeout_ms 毫秒之后。如果将async_insert_stale_timeout_ms设置为非零值,则在自最后一个查询起超过async_insert_stale_timeout_ms毫秒后插入数据。用户可以调整这些设置来控制其管道的端到端延迟。有关可用于调整缓冲区刷新的其他设置,请参阅此处。通常情况下,默认值是合适的。

在使用少量代理、吞吐量低但对端到端延迟要求严格的情况下,自适应异步插入 可能有用。通常,这些不适用于 ClickHouse 中看到的具有高吞吐量的可观察性用例。

最后,在使用异步插入时,与同步插入 ClickHouse 相关的前述重复数据删除行为默认情况下未启用。如果需要,请参阅设置async_insert_deduplicate

有关配置此功能的完整详细信息,请参阅此处,并深入了解此处

部署架构

将 OTel 采集器与 Clickhouse 一起使用时,可以使用多种部署架构。我们在下面描述了每种架构及其适用的情况。

仅限代理

在仅限代理的架构中,用户将 OTel 采集器作为代理部署到边缘。这些代理接收来自本地应用程序的跟踪(例如作为 sidecar 容器)并从服务器和 Kubernetes 节点收集日志。在此模式下,代理会将其数据直接发送到 ClickHouse。

NEEDS ALT

此架构适用于中小型部署。其主要优点是不需要额外的硬件,并使 ClickHouse 可观察性解决方案的总资源占用最小化,应用程序和采集器之间具有简单的映射关系。

代理数量超过数百个后,用户应考虑迁移到基于网关的架构。此架构存在一些缺点,这使得它难以扩展。

  • 连接扩展 - 每个代理都将建立与 ClickHouse 的连接。虽然 ClickHouse 能够维护数百(如果不是数千)个并发插入连接,但这最终将成为限制因素并降低插入效率 - 即 ClickHouse 将使用更多资源来维护连接。使用网关可以最大程度地减少连接数量并提高插入效率。
  • 边缘处理 - 在此架构中,任何转换或事件处理都必须在边缘或 ClickHouse 中执行。除了具有限制性之外,这还可能意味着复杂的 ClickHouse 物化视图或将大量计算推送到边缘 - 在那里关键服务可能会受到影响并且资源稀缺。
  • 小批量和延迟 - 代理采集器可能单独收集很少的事件。这通常意味着需要将它们配置为按设定的间隔刷新以满足交付 SLA。这可能导致采集器向 ClickHouse 发送小批量数据。虽然这是一个缺点,但可以通过异步插入来缓解 - 请参阅“优化插入”。

使用网关进行扩展

可以将 OTel 采集器部署为网关实例以解决上述限制。这些提供独立的服务,通常每个数据中心或每个区域一个。它们通过单个 OTLP 端点接收来自应用程序(或代理角色中的其他采集器)的事件。通常会部署一组网关实例,并使用开箱即用的负载均衡器在它们之间分配负载。

NEEDS ALT

此架构的目标是从代理中卸载计算密集型处理,从而最大限度地减少其资源使用。这些网关可以执行原本需要代理执行的转换任务。此外,通过聚合来自多个代理的事件,网关可以确保将大批量数据发送到 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://127.0.0.1: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

此架构的主要缺点是管理一组采集器相关的成本和开销。

有关使用关联的经验教训管理更大基于网关的架构的示例,我们建议您阅读这篇博文

添加 Kafka

读者可能会注意到上述架构未使用 Kafka 作为消息队列。

使用 Kafka 队列作为消息缓冲区是日志记录架构中常见的流行设计模式,并且由 ELK 堆栈推广。它提供了一些好处;主要是,它有助于提供更强大的消息传递保证并帮助处理背压。消息从收集代理发送到 Kafka 并写入磁盘。理论上,集群化的 Kafka 实例应该提供高吞吐量的消息缓冲区,因为它线性地将数据写入磁盘的计算开销小于解析和处理消息 - 例如,在 Elastic 中,标记化和索引会产生很大的开销。通过将数据从代理中移开,您还可以减少由于源日志轮换而导致消息丢失的风险。最后,它提供了一些消息回复和跨区域复制功能,这对于某些用例可能很有吸引力。

但是,ClickHouse 可以非常快速地处理插入数据 - 在中等硬件上每秒数百万行。来自 ClickHouse 的背压很少见。通常,利用 Kafka 队列意味着更多的架构复杂性和成本。如果您能接受日志不需要与银行交易和其他关键任务数据相同的交付保证这一原则,我们建议您避免 Kafka 的复杂性。

但是,如果您需要高交付保证或重放数据(可能到多个源)的功能,Kafka 可以作为有用的架构补充。

NEEDS ALT

在这种情况下,可以将 OTel 代理配置为通过Kafka 导出器将数据发送到 Kafka。反过来,网关实例使用Kafka 接收器来消费消息。我们建议您参阅 Confluent 和 OTel 文档以获取更多详细信息。

估算资源

OTel 采集器的资源需求将取决于事件吞吐量、消息大小和执行的处理量。OpenTelemetry 项目维护着基准测试用户,可用于估算资源需求。

根据我们的经验,具有 3 个核心和 12GB RAM 的网关实例可以处理大约 60k 个事件/秒。这假设一个最小的处理管道负责重命名字段并且没有正则表达式。

对于负责将事件发送到网关的代理实例,并且仅在事件上设置时间戳,我们建议用户根据预期的每秒日志数进行大小调整。以下是用户可以作为起点使用的近似数字。

日志速率 采集器代理的资源 1k/秒 0.2CPU,0.2GiB 5k/秒 0.5 CPU,0.5GiB 10k/秒 1 CPU,1GiB

日志速率采集器代理的资源
1k/秒0.2CPU,0.2GiB
5k/秒0.5 CPU,0.5GiB
10k/秒1 CPU,1GiB