ClickHouse Cloud如何通过OpenTelemetry实现每秒5000万事件接入
DataHot 速览
ClickHouse Cloud公开其内部可观测性平台LogHouse的OpenTelemetry接入架构演进,从常见的agent-to-gateway两层架构起步,现已达到每秒5000万事件、存储177 PiB未压缩数据的规模。文章详细说明了面对数据库背压时,默认内存队列无法承受10GBps压缩数据吞吐,因此设计自定义S3-backed管道来保证持久性。重点讨论了大规模遥测场景下生产者速率不可控带来的工程挑战与权衡。
为什么值得关注:数据从业者可从中学习大规模遥测数据接入的架构演化、背压处理及成本性能权衡,是数据平台工程实践的高质量参考。
译文
AI 逐段翻译我们曾公开发文介绍 LogHouse,这是我们用于 ClickHouse Cloud 的内部可观测性平台。我们上次发文提到整个平台超过一千万亿行数据,而 OpenTelemetry 在其中占有重要且快速增长的比例,它作为日志、指标和追踪的统一采集层,覆盖了我们技术栈中的所有组件。
如今,我们的 OTel 管道每秒摄入 5000 万事件,存储 177 PiB 的未压缩数据,从 ClickHouse 服务器和 keeper、基于 Kubernetes 的内部服务、外部云提供商基础设施等处采集数据。我们系列博客的上一篇文章预告了一个自定义的基于 S3 的管道,用于确保基于采集器的摄入的持久性。在这篇文章中,我们将分享完整的架构以及我们如何走到这一步的历史。
历史
迭代 1:agent 到 gateway
我们的第一次迭代是你在每个 OTel 参考部署中都能找到的:两层管道,节点本地 agent 将数据发送给一组共享的 gateway。
agent 在每个 Kubernetes 节点上作为 DaemonSet 运行,抓取 pod 日志并接收指标和追踪。这是一个资源受限的环境,因为我们希望将尽可能多的节点内存分配给实际工作负载,所以 agent 只附加资源属性,进行非常简单的批处理,然后将数据转发给 gateway 层。
gateway 是可扩展层,用于更重量级的处理。在那里,我们可以分配更多内存来积累更大的插入批次,这是 ClickHouse 最喜欢的。
我们选择这个方案的原因和所有人一样:这是一个无聊的默认方案,但它适用于 99% 的用例。
在压力下崩溃
我们很快发现这种架构无法应对来自数据库的反压。默认情况下,gateway 在将数据发送到远程下游 ClickHouse 目的地之前,会把外发数据放入内存队列。每秒 5000 万事件意味着 10GBps 的压缩数据量,这意味着即使是最短的下游中断,也无法经济地吸收。
我们遇到反压的原因是,我们无法控制或可靠预测生产者生成遥测数据的速率。LogHouse 从 ClickHouse 服务器和 keeper、Kubernetes 服务、云提供商基础设施以及我们舰队中的其他系统收集数据。在任何时刻,这些生产者可能以比下游系统配置的摄入速度更快的速度产生数据。
持续增长相对容易管理。当遥测数据量随时间增加时,我们可以手动添加容量或通过自动扩缩容来响应。短期峰值则更难处理。影响部分机群的错误可能会突然产生大量日志,或者一个使用频繁的服务可能在特定区域遭遇突发活动。这些事件可能很庞大,但往往过于短暂,不足以证明扩缩容的合理性。为吸收每一个可能的峰值而配置容量,也会导致这些容量在大部分时间闲置。
因此,我们根据预期的持续吞吐量来调整 LogHouse 的大小,并留有适当的余量,而不是针对最大可能突发进行配置。摄入管道必须吸收遥测生产速率与 ClickHouse 可以接受速率之间的暂时不匹配——不丢失数据,不让新遥测数据在积压之后延迟,也不要求我们针对峰值需求永久配置容量。这一约束决定了本文其余部分的所有架构决策。
迭代 2:本地预写日志
当内存不足时,显而易见的下一步是基于磁盘的存储。OTel 采集器有一个内置功能,叫 file_storage 扩展,正是为此目的。启用后,采集器的所有外发数据在发送到下游之前会写入本地预写日志。
从理论上讲,这解决了我们的问题。磁盘比内存更便宜、密度更高,因此我们可以配置足够的缓冲区来吸收即使是长时间的数据库中断。此外,重启不再丢失在途批次。
不幸的是,这也没有达到我们的需求。用 PVC 支持 WAL 意味着我们必须将 gateway 部署为 StatefulSet。大多数 pod 规范变得不可变,以前例行的推出现在必须仔细规划和执行。
其次,它不支持我们所需的性能。当下游数据库健康时吞吐量稳定,但当 WAL 大小在下游反压期间和之后增长时,吞吐量下降。在我们的观察中,清空 1 TiB 的积压大约需要 4 小时。以我们的遥测数据量,即使是数据库的短暂暂停也会导致 WAL 积压,需要数小时才能排空。在这些情况下水平扩展无济于事,因为 WAL 是每 pod 的,无法拆分或分布。
更糟糕的是,WAL 大致按 FIFO 顺序消费。中断后,新的遥测数据落在日志的末尾,位于暂停开始以来积累的所有数据之后。而正是在这些情况下,新数据才是最关键的。
这导致了一个令人不适的变通方法。当我们的 WAL 变得足够大,新数据将延迟数小时时,我们就截断它们。我们承受了为遥测数据构建持久存储的麻烦和成本,然后却例行公事地丢弃数据。这标志着采集器 WAL 并不是适合我们的解决方案。
Kafka 怎么样?
任何熟悉大规模遥测管道的人现在都可能想到 Kafka。标准的做法是在 agent 和 gateway 之间放置一个流系统,如 Kafka、Warpstream 或 Pulsar。gateway 变成一个简单的无状态层,消费流,该流吸收反压并独立扩展。这是一个被充分理解的模式,能够在大规模下运行,并能解决我们的许多问题。
我们认真考虑过,但决定不走那条路。这是一个运营决策,而不是对该技术的否定。我们不会在基础设施的其他任何地方部署 Kafka。采用它意味着要在我们所有的云区域中建立一个全新的零级服务。这将需要大量的专业知识和努力:部署、容量规划、调优、值班、升级,所有这些都是为了这一个用例。
相反,我们退一步问了一个问题:我们到底希望从遥测摄取管道中得到什么?
设计迭代 3
在收集器架构的两次迭代以及我们对基于 Kafka 的替代方案的研究之间,我们的需求形态已经清晰。我们的第三次迭代必须满足四个约束:
- 原始遥测没有外部排队系统。我们不会承担运行另一个有状态服务(如 Kafka)的运维负担,仅仅为了在热路径上缓冲数据,也不能接受 Kafka 的队头阻塞。
- 规模经济。遥测是一个成本中心,我们部署的一切都计入我们的云利润。
- 故障时可靠。管道必须能够吸收来自 ClickHouse 的任意时期的背压,而不丢失数据。
- 开源。我们希望继续与客户运行 ClickHouse 和 OTel 的方式保持一致。
两个洞察塑造了我们的设计。
第一个是对象存储。S3、GCS 和 Azure Blob 是无底洞、持久且免维护的,并且已经是我们云的核心组件。
第二个是基于优先级的故障转移。OTel 收集器附带一个 failoverconnector,它路由到主要目标,并且仅当检测到主要目标故障时才回退到次要目标。这给了我们一种方法,让 ClickHouse 保持在热路径上,同时将对象存储视为仅在背压期间激活的释放阀。
这对成本控制至关重要。以我们的完整摄取速率将所有内容写入对象存储将是令人望而却步的:在每秒 5000 万事件的情况下,PUT 操作成本本身就使存储账单相形见绌。对象存储存储数据成本低,但以高吞吐量写入成本高。我们预计在当前容量下,我们自己的工作负载每年的 API 成本在 10 万到 50 万美元之间,以我们的指数级增长率来看完全不可持续。基于优先级的故障转移意味着我们只在 ClickHouse 不可用的间歇性窗口期间支付这些 PUT 成本。在正常操作中,遥测直接进入 ClickHouse。
一旦数据溢出到对象存储中,我们依靠事件通知将这些 blob 拉出,并通过单独的收集器写回 ClickHouse。这是对象存储的另一个基本功能,其中某些操作(如新对象)的记录可以随对象元数据发布到队列中。这提供了一种比列出存储桶和检查点进度更简单、更具可扩展性的模型。
如果对象存储宕机怎么办?
在这一点上这是一个合理的问题,尤其是因为 ClickHouse Cloud 本身由对象存储支持。对象存储的硬停机同时会关闭主存储和备份,并且溢出机制在这里无法挽救我们。
然而,完全的 S3 停机很少见,并且与我们要准备的故障类型不同。我们曾考虑将溢出存储桶放置在与它们支持的 LogHouse 集群不同的区域,但跨区域传输溢出批次的出口成本对于如此罕见的情况来说太高了。
相反,我们围绕更常见的故障模式进行设计。对象存储经常遭受部分中断,表现为特定键或前缀的严重节流。我们将溢出的批次分散在广泛的键空间中,这样在故障转移期间就不会有单个前缀集中负载。这之所以有效,是因为事件通知包含对象的完整键,因此键的结构可以完全不透明,并且不需要针对范围扫描进行优化。
新管道
只有几个额外的活动部件。在云侧,我们引入了一个带有每个遥测信号前缀的 S3 存储桶。每个前缀都有一个关联的事件通知和 SQS 队列。在我们的 Kubernetes 基础设施内,代理层保持不变。网关层是无状态的,故障转移连接器负责路由到健康目标。然后,我们有一个单独的追赶接收器,轮询来自 SQS 的事件通知并将它们插入到 ClickHouse。当数据库不健康时,通知只是在 SQS 中积累。
故障转移连接器
在 OTel 中,连接器是一个同时充当接收器和导出器的组件。故障转移连接器接收按优先级排序的管道列表,并将传入批次路由到优先级最高的健康管道。它不执行健康检查;相反,它检查每个管道的结果。
有一个陷阱:必须禁用 ClickHouse 导出器上的发送队列,否则任何错误都会对连接器隐藏,并且故障转移将被延迟。在所有 OTel 导出器上启用发送队列是最佳实践,但这样做会将同步管道变成异步管道。这将延迟背压信号到达故障转移连接器。为了避免这种情况,连接器本身支持发送队列,因此它位于路由决策的上游。
我们也不希望来自 ClickHouse 的任何瞬时网络错误或单个查询失败触发完全故障转移,因此我们仍然在导出器中通过 retry_on_failure 设置启用内联同步重试。
在blob存储导出器上,我们反其道而行之:我们希望故障对故障转移连接器隐藏。由于我们有两个优先级级别,我们希望避免所有级别都向故障转移连接器报告不健康的情况。这会导致故障转移连接器停止向下游发送数据,并在其发送队列中缓冲,直到它恢复健康的目标。因此,我们通过启用blob存储导出器上的发送队列来隐藏单个批次故障。如上所述,我们设计了密钥格式以避免blob存储常见的限制。结合积极的重试策略,这为我们提供了足够强的持久性保证,同时避免了中断期间OOM的风险。
这篇内容对你有用吗?
反馈只用于改善内容筛选,不等同于收藏