集成 OpenTelemetry 进行数据采集
任何可观测性解决方案都需要一种方式来采集并导出日志和追踪(traces)。为此,ClickHouse 推荐使用 OpenTelemetry(OTel)项目。
“OpenTelemetry 是一个可观测性框架和工具集,旨在创建和管理诸如追踪(traces)、指标(metrics)和日志(logs)等遥测数据。”
与 ClickHouse 或 Prometheus 不同,OpenTelemetry 本身并不是可观测性后端,而是专注于遥测数据的生成、采集、管理和导出。虽然 OpenTelemetry 的最初目标是让用户能够方便地使用特定语言的 SDKs 为其应用或系统进行插桩(instrumentation),但其能力已经扩展到通过 OpenTelemetry collector 采集日志——这是一个用于接收、处理并导出遥测数据的代理/代理程序(agent/proxy)。
ClickHouse 相关组件
OpenTelemetry 由多个组件组成。除了提供数据和 API 规范、标准化的协议以及字段/列的命名约定之外,OTel 还提供了两项在使用 ClickHouse 构建可观测性解决方案时至关重要的能力:
- OpenTelemetry Collector 是一个用于接收、处理和导出遥测数据的代理。基于 ClickHouse 的解决方案会使用该组件在批处理和插入之前完成日志收集和事件处理。
- Language SDKs 实现了规范、API 以及遥测数据的导出。这些 SDK 能够确保在应用程序代码中正确记录 trace,生成构成 trace 的各个 span,并通过元数据在服务之间传播上下文——从而形成分布式 trace,并确保 span 可以被关联。这些 SDK 背后有一个生态系统,为常见库和框架提供自动化集成,因此用户无需更改代码即可获得开箱即用的自动埋点能力。
基于 ClickHouse 的可观测性解决方案会同时利用这两类组件。
发行版
OpenTelemetry collector 提供了多个发行版。ClickHouse 方案所需的 filelog receiver 和 ClickHouse exporter 仅在 OpenTelemetry Collector Contrib Distro 中提供。
该发行版包含许多组件,允许用户试验各种配置。不过,在生产环境中运行时,建议将 collector 限制为只包含该环境真正需要的组件。这样做的原因包括:
- 减小 collector 的体积,从而缩短其部署时间
- 通过减少可用攻击面来提升 collector 的安全性
可以使用 OpenTelemetry Collector Builder 来构建自定义 collector。
使用 OTel 进行数据摄取
Collector deployment roles
为了收集日志并将其写入 ClickHouse,我们推荐使用 OpenTelemetry Collector。OpenTelemetry Collector 可以以两种主要角色部署:
-
Agent - Agent 实例在边缘侧收集数据,例如在服务器或 Kubernetes 节点上,或者直接从使用 OpenTelemetry SDK 进行埋点的应用程序接收事件。在后一种情况下,Agent 实例与应用程序一起运行,或者运行在与应用程序相同的主机上(例如以 sidecar 或 DaemonSet 守护进程集的形式)。Agent 可以将数据直接发送到 ClickHouse,也可以发送到网关实例。在前一种情况下,这种方式被称为 Agent deployment pattern。
-
Gateway - Gateway 实例提供一个独立的服务(例如,在 Kubernetes 中的一个部署),通常按集群、数据中心或区域划分。它们通过单个 OTLP 端点从应用程序(或作为 Agent 的其他 Collector)接收事件。通常会部署一组 Gateway 实例,并使用开箱即用的负载均衡器在它们之间分发负载。如果所有 Agent 和应用程序都将其信号发送到这个单一端点,这通常被称为 Gateway deployment pattern。
下面我们假设使用一个简单的 Agent Collector,直接将其事件发送到 ClickHouse。有关使用 Gateway 以及适用场景的更多详细信息,请参阅 Scaling with Gateways。
收集日志
使用 Collector 的主要优势在于,它允许你的服务快速将数据交给 Collector 处理,由 Collector 负责额外的逻辑,例如重试、批处理、加密,甚至敏感数据过滤。
Collector 使用 receiver、processor 和 exporter 这三个术语表示其三个主要处理阶段。Receiver 用于数据收集,可以是拉取(pull)或推送(push)模式。Processor 用于对消息进行转换和丰富。Exporter 负责将数据发送到下游服务。理论上这个服务也可以是另一个 Collector,但在下面的初始讨论中,我们假定所有数据都直接发送到 ClickHouse。

我们建议用户熟悉所有可用的 receiver、processor 和 exporter。
Collector 提供了两个用于收集日志的主要 receiver:
通过 OTLP - 在这种情况下,日志通过 OTLP 协议由 OpenTelemetry SDK 直接(推送)发送到 Collector。OpenTelemetry demo 采用了这种方式,各语言中的 OTLP exporter 默认假定本地 Collector 作为端点。在这种情况下,Collector 必须配置 OTLP receiver——请参阅上面的 demo 配置示例。这种方式的优点是日志数据会自动包含 Trace ID,用户随后可以根据特定日志查找对应的 trace,反之亦然。

这种方式要求用户使用其对应语言的 SDK对代码进行埋点(接入 OTel)。
- 通过 Filelog receiver 抓取(Scraping) - 此 receiver 会对磁盘上的文件进行 tail 并生成日志消息,然后将其发送到 ClickHouse。该 receiver 能处理复杂任务,例如检测多行消息、处理日志轮转、通过检查点机制提高重启时的健壮性,以及抽取结构化信息。该 receiver 还可以 tail Docker 和 Kubernetes 容器日志,可通过 Helm 图表进行部署,从这些日志中抽取结构化信息,并使用 pod(容器组)详细信息对其进行丰富。

大多数部署会组合使用上述多种 receiver。我们建议用户阅读 collector 文档,并熟悉基础概念,以及配置结构和安装方法。
结构化日志 vs 非结构化日志
日志可以是结构化的,也可以是非结构化的。
结构化日志通常采用 JSON 等数据格式,定义 HTTP 状态码、源 IP 地址等元数据字段。
非结构化日志虽然通常也具有一些可以通过正则表达式模式提取的内在结构,但在表示时仍只是一个纯字符串。
我们建议用户尽可能采用结构化日志,并使用 JSON 格式记录日志(例如 ndjson)。这将简化后续对日志所需的处理,无论是在发送到 ClickHouse 之前使用 Collector 处理器,还是在写入时通过物化视图进行处理。结构化日志最终将节省后续处理所需的资源,从而降低 ClickHouse 部署中的 CPU 需求。
示例
作为示例,我们提供了一个结构化(JSON)和一个非结构化的日志数据集,每个大约包含 1,000 万行,可通过以下链接获取:
在下面的示例中,我们使用结构化数据集。请确保已下载并解压此文件,以便复现后续示例。
下面是一个简单的 OTel Collector 配置示例,它使用 filelog receiver 从磁盘读取这些文件,并将处理后的消息输出到标准输出(stdout)。由于我们的日志是结构化的,因此使用 json_parser operator。请修改路径以指向 access-structured.log 文件。
下面的示例会从日志中提取时间戳。这需要使用 json_parser operator,它会将整行日志转换为 JSON 字符串,并将结果放入 LogAttributes 中。这在计算上可能比较昂贵,而在 ClickHouse 中可以更高效地完成这一工作——参见使用 SQL 提取结构。一个等价的非结构化示例,使用 regex_parser 来实现同样目标,可以在这里找到。
用户可以按照官方说明在本地安装 collector。需要特别注意的是,请根据说明将使用的发行版替换为 contrib 发行版(其中包含 filelog receiver),例如,用户应下载 otelcol-contrib_0.102.1_darwin_arm64.tar.gz,而不是 otelcol_0.102.1_darwin_arm64.tar.gz。发行版本可以在此处找到。
安装完成后,可以通过以下命令运行 OTel collector:
如果使用结构化日志,输出的消息将具有如下形式:
上面展示的是由 OTel collector 生成的一条日志消息。在后续章节中,我们会将这些相同的消息摄取到 ClickHouse 中。
日志消息的完整 schema,以及在使用其他 receivers 时可能出现的附加列,记录在此处。我们强烈建议用户熟悉该 schema。
这里的关键点是,日志行本身作为字符串存储在 Body 字段中,而 JSON 已通过 json_parser 自动解析并提取到了 Attributes 字段中。使用同一个operator 还会将时间戳提取到对应的 Timestamp 列中。关于使用 OTel 处理日志的建议,请参阅 Processing。
Operators 是日志处理的最基本单元。每个 operator 只负责一项职责,例如从文件中读取行,或从某个字段中解析 JSON。然后将多个 operators 串联成一个 pipeline,以实现预期结果。
上述消息没有 TraceID 或 SpanID 字段。如果存在(例如在实现分布式跟踪的场景下),可以使用上面演示的相同技术从 JSON 中提取这些字段。
对于需要采集本地或 Kubernetes 日志文件的用户,我们建议熟悉 filelog receiver 提供的配置选项,以及偏移量(offset)管理和多行日志解析的处理方式。
收集 Kubernetes 日志
对于收集 Kubernetes 日志,我们建议参考 OpenTelemetry 的 Kubernetes 指南。建议使用 Kubernetes Attributes Processor 来为日志和指标补充 pod(容器组)元数据。这样可以生成动态元数据(例如标签),并将其存储在 ResourceAttributes 列中。ClickHouse 当前为该列使用 Map(String, String) 类型。关于如何处理和优化此类型,请参阅 使用 Map 和 从 Map 中提取。
收集 Trace 数据
对于希望对其代码进行插桩并收集 trace 的用户,我们建议参考官方的 OTel 文档。
为了将事件发送到 ClickHouse,用户需要部署一个 OTel collector,通过相应的 receiver 使用 OTLP 协议接收 trace 事件。OpenTelemetry 示例应用提供了为每种受支持语言进行插桩并将事件发送到 collector 的示例。下面展示了一个合适的 collector 配置示例,它会将事件输出到 stdout:
示例
由于必须通过 OTLP 接收 trace 数据,我们使用 telemetrygen 工具来生成 trace。请按照此处的说明进行安装。
下面的配置通过 OTLP 接收器接收 trace 事件,然后将其发送到标准输出(stdout)。
通过以下命令运行此配置:
使用 telemetrygen 将 trace 事件发送到收集器:
这将在 stdout 中输出类似于下面示例的 trace 消息:
上面展示的是由 OTel collector 生成的一条 trace 消息。我们会在后续章节中将同样的消息摄取到 ClickHouse 中。
trace 消息的完整 schema 可以在这里找到。我们强烈建议用户充分熟悉这一 schema。
处理:过滤、转换和丰富
如前面设置日志事件时间戳的示例所示,用户通常需要对事件消息进行过滤、转换和丰富。这可以通过 OpenTelemetry 中的多种功能来实现:
-
Processors(处理器) - 处理器获取由 receivers 收集到的数据并对其进行修改或转换,然后再发送给 exporters。处理器会按照在 collector 配置中
processors部分定义的顺序依次应用。处理器是可选的,但通常会推荐一个最小集合。在将 OTel collector 与 ClickHouse 一起使用时,我们建议将处理器限制为:- memory_limiter 用于防止 collector 出现内存耗尽的情况。推荐配置可参考 Estimating Resources。
- 任何基于上下文进行富化的处理器。例如,Kubernetes Attributes Processor 允许基于 k8s 元数据自动设置 spans、metrics 和 logs 的资源属性,例如使用其源 pod(容器组)ID 对事件进行富化。
- 如果跟踪(traces)需要的话,可使用 尾部或头部采样。
- 基本过滤——如果无法通过 operator(见下文)完成,则可以在此丢弃不需要的事件。
- 批处理——在与 ClickHouse 协作时必不可少,以确保数据以批量形式发送。参见 "Exporting to ClickHouse"。
-
Operators(算子) - Operators 提供了在 receiver 端可用的最基本处理单元。它们支持基本解析,允许设置诸如严重性(Severity)和时间戳(Timestamp)等字段。这里支持 JSON 和正则表达式解析,同时支持事件过滤和基本转换。我们建议在此执行事件过滤。
我们建议用户避免使用 operators 或 transform processors 对事件进行过多处理。这些操作可能会带来较大的内存和 CPU 开销,尤其是 JSON 解析。完全可以在 ClickHouse 中通过在插入时使用物化视图和列来完成所有处理,但有一些例外——特别是需要上下文感知的富化,例如添加 k8s 元数据。更多详情参见 Extracting structure with SQL。
如果使用 OTel collector 进行处理,我们建议在网关实例上执行转换,并尽量减少在 agent 实例上完成的工作。这样可以确保在服务器边缘运行的 agents 所需资源尽可能少。通常,我们看到用户在 agents 中只执行过滤(以尽量减少不必要的网络使用)、时间戳设置(通过 operators)以及需要上下文的富化。例如,如果网关实例位于不同的 Kubernetes 集群中,则需要在 agent 中完成 k8s 富化。
示例
如下配置展示了如何采集非结构化日志文件。请注意其中使用了操作符(regex_parser)从日志行中提取结构并过滤事件,同时还使用了处理器对事件进行批处理并限制内存使用。
config-unstructured-logs-with-processor.yaml
导出到 ClickHouse
Exporter 会将数据发送到一个或多个后端或目标。Exporter 可以是拉取式或推送式。要将事件发送到 ClickHouse,用户需要使用推送式的 ClickHouse exporter。
ClickHouse exporter 属于 OpenTelemetry Collector Contrib,而不是核心发行版。用户可以使用 contrib 发行版,或者构建自己的 collector。
下面展示了一个完整的配置文件示例。
请注意以下关键设置:
- pipelines - 上述配置重点展示了对 pipelines 的使用,它由一组 receivers、processors 和 exporters 组成,并分别为 logs 和 traces 定义了一个 pipeline。
- endpoint - 与 ClickHouse 的通信通过
endpoint参数进行配置。连接字符串tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1指定通过 TCP 进行通信。如果用户出于流量切换等原因更偏好使用 HTTP,请按照此处的说明修改该连接字符串。完整的连接细节(包括在连接字符串中指定用户名和密码的功能)在这里有详细描述。
Important: 请注意,上述连接字符串启用了压缩(lz4)以及异步插入。我们建议始终同时启用这两项。关于异步插入的更多细节,请参阅 Batching。压缩应始终显式指定,否则在旧版本的 exporter 中不会默认启用。
- ttl - 此处的值决定数据的保留时长。更多细节见 “Managing data”。应当使用小时为单位的时间值,例如 72h。我们在下面的示例中禁用了 TTL,因为我们的数据来自 2019 年,如果开启 TTL,数据在插入后会立即被 ClickHouse 删除。
- traces_table_name 和 logs_table_name - 决定 logs 和 traces 表的名称。
- create_schema - 决定在启动时是否使用默认 schema 创建表。默认值为 true,便于快速上手。用户在生产环境中应将其设置为 false,并自行定义 schema。
- database - 目标数据库。
- retry_on_failure - 设置是否对失败的批次进行重试。
- batch - batch processor 确保事件以批次的形式发送。我们建议批大小约为 5000,超时时间为 5s。两者中任一条件先满足时,都会触发将批次刷新到 exporter。减小这些值会降低端到端延迟,使数据更早可被查询,但代价是向 ClickHouse 建立更多连接并发送更多批次。如果用户未使用 asynchronous inserts,则不推荐这么做,因为这可能在 ClickHouse 中导致 too many parts 的问题。相反,如果用户使用了异步插入,数据何时可用于查询也将依赖于异步插入的相关设置——尽管数据仍会更早从 connector 中被刷新。更多细节请参阅 Batching。
- sending_queue - 控制发送队列的大小。队列中的每个项包含一个批次。如果该队列被占满,例如由于 ClickHouse 不可达但事件仍持续到达,则新的批次将被丢弃。
假设用户已经提取了结构化日志文件,并且本地已有一份正在运行的 ClickHouse 实例(使用默认认证配置),则可以通过以下命令运行该配置:
要将跟踪数据发送到此收集器,请使用 telemetrygen 工具运行以下命令:
运行后,使用一个简单查询确认已存在日志事件:
开箱即用的 schema
默认情况下,ClickHouse exporter 会为 logs 和 traces 分别创建目标表。可以通过设置 create_schema 来禁用此行为。此外,可以通过上述设置,将 logs 和 traces 表的名称从默认的 otel_logs 和 otel_traces 修改为其他名称。
在下面的 schema 中,我们假设 TTL 被设置为 72 小时。
logs 的默认 schema 如下所示(otelcol-contrib v0.102.1):
此处的列与 OTel 官方日志规范中定义的列相对应,详见此处。
关于此 schema,有几点重要说明:
- 默认情况下,表通过
PARTITION BY toDate(Timestamp)按日期进行分区。这样可以高效地删除过期数据。 - TTL 通过
TTL toDateTime(Timestamp) + toIntervalDay(3)设置,并与在 collector 配置中设置的值相对应。ttl_only_drop_parts=1表示仅在某个数据分片内所有行都已过期时才会删除整个分片。相比在分片内部逐行删除(会触发代价高昂的删除操作),这种方式更加高效。我们建议始终启用该设置。更多细节请参见 Data management with TTL。 - 表使用经典的
MergeTreeengine。这对于日志和 trace 是推荐的选择,通常不需要更改。 - 表按
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)排序。这意味着针对ServiceName、SeverityText、Timestamp和TraceId的过滤查询会得到优化——列表中越靠前的列过滤速度越快,例如按ServiceName过滤会显著快于按TraceId过滤。用户应根据预期的访问模式调整此排序方式——参见 Choosing a primary key。 - 上述 schema 对列应用了
ZSTD(1)。这为日志提供了最佳压缩效果。用户可以将 ZSTD 压缩级别提高到默认值 1 以上以获得更高的压缩率,但这种情况通常收益不大。提高该值会在插入时(压缩期间)带来更高的 CPU 开销,尽管解压缩(以及查询)性能应保持大致相同。更多详情参见此文。额外的 delta 编码 被应用于 Timestamp,以减小其在磁盘上的占用。 - 请注意
ResourceAttributes、LogAttributes和ScopeAttributes是 map。用户应熟悉三者之间的差异。关于如何访问这些 map 以及如何优化对其中键的访问,参见 Using maps。 - 此处大多数其他类型(例如将
ServiceName设为 LowCardinality)均已优化。请注意,在示例日志中为 JSON 的Body被存储为 String。 - Bloom filter 应用于 map 的键和值,以及
Body列。这些设置旨在加速访问这些列的查询,但通常并非必需。参见 Secondary/Data skipping indices。
同样,这里会与 OTel 官方 trace 规范中对应的列保持一致,相关文档见此处。该 schema 复用了上文日志 schema 的许多设置,并额外增加了用于 span 的 Link 专用列。
我们建议用户禁用自动创建 schema 的功能,改为手动创建表。这样可以修改主键和辅助键,并可根据需要增加额外列以优化查询性能。更多详情参见 Schema design。
优化写入
为了在获得强一致性保证的同时实现高写入性能,用户在通过 OTel collector 向 ClickHouse 插入可观测性数据时,应遵循一些简单的规则。只要正确配置 OTel collector,遵循以下规则就会变得很简单。这也有助于避免用户在首次使用 ClickHouse 时遇到的一些常见问题。
批处理
默认情况下,每次向 ClickHouse 发起的 insert 都会让 ClickHouse 立即创建一个包含该 insert 数据以及需要存储的其他元数据的数据片段(part)。因此,相比于发送大量每次只包含少量数据的 insert,发送次数较少但每次包含更多数据的 insert 可以减少所需的写入次数。我们建议一次性以较大的批量插入数据,每批至少 1,000 行。更多详情见此处。
默认情况下,向 ClickHouse 的 insert 是同步的,并且对相同内容是幂等的。对于 MergeTree 系列表,ClickHouse 默认会自动对插入进行去重。这意味着在如下场景中,插入是可容错的:
- (1) 如果接收数据的节点出现问题,
insert查询会超时(或返回更具体的错误),且不会收到确认。 - (2) 如果数据已经被该节点写入,但由于网络中断,确认无法返回给查询的发送方,则发送方会收到超时或网络错误。
从 collector 的角度来看,很难区分 (1) 和 (2)。不过,在这两种情况下,都可以立即重试未被确认的 insert。只要重试的 insert 查询包含相同顺序的相同数据,如果原始(未确认的)插入已成功,ClickHouse 会自动忽略这次重试的插入。
我们建议用户使用前面配置中展示的 batch processor 来满足上述要求。这样可以确保 insert 以满足上述要求的一致行批次形式发送。如果预期某个 collector 具有高吞吐量(每秒事件数),并且每次 insert 至少可以发送 5,000 个事件,那么这通常就是该 pipeline 中唯一需要的批处理。在这种情况下,collector 会在 batch processor 的 timeout 被触发之前刷新批次,从而确保 pipeline 的端到端延迟保持较低,并且批次大小保持一致。
使用异步插入
通常,当采集器吞吐量较低时,用户被迫发送更小的批次,但同时又希望在端到端延迟不超过一定上限的前提下尽快将数据发送到 ClickHouse。此时,当批处理器的 timeout 到期时就会发送小批次数据。这可能带来问题,此时就需要使用异步插入。这种情况通常出现在以 agent 角色运行的采集器被配置为直接发送到 ClickHouse 时。网关作为聚合器可以缓解这个问题——参见通过网关扩展。
如果无法保证发送较大的批次,用户可以通过 Asynchronous Inserts 将批处理逻辑委托给 ClickHouse。使用异步插入时,数据会先写入一个缓冲区,然后再写入数据库存储,即以延迟或异步的方式完成写入。

在启用异步插入的情况下,当 ClickHouse ① 接收到一条插入查询时,该查询的数据会被 ② 立即写入内存缓冲区。随后在 ③ 下一次缓冲区刷写发生时,缓冲区中的数据会被排序,并作为一个 part 写入数据库存储。请注意,在数据刷写到数据库存储之前,查询无法检索到这些数据;缓冲区刷写是可配置的。
要为采集器启用异步插入,请在连接字符串中添加 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 毫秒后插入数据。用户可以调优这些参数,以控制其数据管道的端到端延迟。更多可用于调优缓冲区刷写的参数记录在此处。通常情况下,默认值已足够适用。
在仅使用少量 agent、吞吐量较低但端到端延迟要求严格的场景中,adaptive asynchronous inserts 可能会有所帮助。总体而言,对于 ClickHouse 中常见的高吞吐可观测性场景,它们通常并不适用。
最后,在使用异步插入时,之前与同步插入 ClickHouse 相关的去重行为默认不会启用。如有需要,请参阅设置项 async_insert_deduplicate。
关于配置该特性的完整细节可在此处找到,更深入的解析请参见这里。
部署架构
在将 OTel collector 与 ClickHouse 配合使用时,可以采用多种不同的部署架构方案。下面将分别进行说明,并指出各自的适用场景。
仅代理模式
在仅代理(agent-only)架构中,用户将 OTel collector 作为代理部署到边缘。这些代理从本地应用程序接收 trace 数据(例如以 sidecar 容器的形式),并从服务器和 Kubernetes 节点收集日志。在此模式下,代理会将数据直接发送到 ClickHouse。

此架构适用于中小规模部署。其主要优点是无需额外硬件,能够将 ClickHouse 可观测性解决方案的整体资源占用保持在最低水平,同时在应用程序与 collector 之间维持简单的映射关系。
一旦代理数量超过数百个,用户应考虑迁移到基于 Gateway 的架构。此架构存在若干劣势,使其在扩展时面临挑战:
- 连接扩展性 - 每个代理都会与 ClickHouse 建立一个连接。虽然 ClickHouse 能够维护数百(甚至上千)个并发写入连接,但最终这会成为限制因素,使写入效率降低——也就是说,ClickHouse 需要消耗更多资源来维护连接。使用 gateway 可以将连接数量最小化,使写入更加高效。
- 在边缘进行处理 - 在此架构下,任何转换或事件处理都必须在边缘或在 ClickHouse 中完成。除了具有限制性之外,这通常意味着要么在 ClickHouse 中实现复杂的物化视图,要么将大量计算下推到边缘——此处关键服务可能会受到影响且资源紧张。
- 小批量与延迟 - 代理 collector 可能各自只收集到很少的事件。这通常意味着需要将其配置为按固定时间间隔进行 flush,以满足交付 SLA。这可能会导致 collector 向 ClickHouse 发送小批量数据。尽管这是一个缺点,但可以通过异步写入(Asynchronous inserts)加以缓解——参见 Optimizing inserts。
通过网关进行扩展
可以将 OTel collector 以 Gateway 实例的形式部署,以解决上述限制。这些实例提供独立的服务,通常按数据中心或区域划分。它们通过单个 OTLP 端点从应用程序(或处于 agent 角色的其他 collector)接收事件。通常会部署一组网关实例,并使用开箱即用的负载均衡器在它们之间分发负载。

这种架构的目的是将计算密集型处理从 agent 上卸载,从而最大程度减少其资源使用。这些网关可以执行原本需要由 agent 完成的转换任务。此外,通过聚合来自多个 agent 的事件,网关可以确保向 ClickHouse 发送大批量数据,从而实现高效写入。随着添加更多 agent、事件吞吐量增加,这些网关 collector 也可以轻松扩展。下面展示了一个网关配置示例,以及一个关联的 agent 配置,该 agent 用于消费示例结构化日志文件。请注意 agent 与网关之间使用 OTLP 进行通信。
clickhouse-gateway-config.yaml
可以通过以下命令应用这些配置。
该架构的主要缺点是管理一组 collector 所带来的成本和运维开销。
关于如何管理更大规模的网关型架构及其相关经验总结,我们推荐阅读这篇博客文章。
添加 Kafka
读者可能已经注意到,上述架构并未使用 Kafka 作为消息队列。
在日志架构中,使用 Kafka 队列作为消息缓冲区是一种常见的设计模式,并由 ELK 技术栈推广开来。它带来了一些好处;主要是有助于提供更强的消息投递保证,并帮助应对回压。消息由采集代理发送到 Kafka 并写入磁盘。理论上,一个 Kafka 集群应当能够提供高吞吐量的消息缓冲区,因为顺序写入数据到磁盘所需的计算开销要小于对消息进行解析和处理——例如在 Elastic 中,分词和索引会产生显著的额外开销。通过将数据从代理侧移走,你也可以降低由于源端日志轮转而导致消息丢失的风险。最后,它还提供一定程度的消息重放和跨区域复制能力,这在某些使用场景下可能颇具吸引力。
然而,ClickHouse 插入数据的速度非常快——在中等硬件上即可达到每秒数百万行。来自 ClickHouse 的回压情况十分罕见。很多时候,引入 Kafka 队列只会带来更多的架构复杂性和成本。如果你能够接受这样一个原则:日志并不需要与银行交易或其他关键业务数据相同级别的投递保证,那么我们建议避免引入 Kafka 所带来的这层复杂性。
但是,如果你需要极高的投递保证,或者需要重放数据(可能重放到多个目标),Kafka 仍然可以是一个有用的架构扩展组件。

在这种情况下,可以通过 Kafka exporter 将 OTel 代理配置为向 Kafka 发送数据。随后,网关实例则通过 Kafka receiver 来消费消息。更多细节建议参考 Confluent 和 OTel 的官方文档。
预估资源
OTel collector 的资源需求取决于事件吞吐量、消息大小以及执行的处理量。OpenTelemetry 项目维护了一套基准测试,用户可以用来预估资源需求。
根据我们的经验,一台具有 3 个 CPU 核心和 12GB 内存的网关实例,大约可以处理每秒 60k 个事件。这是在假设使用最小化处理管道的前提下得出的,该管道仅负责重命名字段且不使用正则表达式。
对于负责将事件发送到网关、且只在事件上设置时间戳的 Agent 实例,我们建议用户根据预期的每秒日志量进行容量规划。以下是可作为起点的近似参考值:
| 日志速率 | 分配给 collector agent 的资源 |
|---|---|
| 1k/second | 0.2 CPU, 0.2GiB |
| 5k/second | 0.5 CPU, 0.5GiB |
| 10k/second | 1 CPU, 1GiB |