在 GCP 上使用 EDOT Cloud Forwarder 进行 OpenTelemetry 日志摄取的规模测试

0 阅读12分钟

作者:来自 Elastic Constanca Manteigas

学习我们如何在 Google Cloud Run 上对用于 GCP 的 EDOT Cloud Forwarder 进行负载测试,并识别每个实例的实际容量上限。我们展示了运行时调优如何提升稳定性,并将结果转化为具体的配置和扩展指导。

用于 GCP 的 EDOT Cloud Forwarder( ECF )是一个事件触的 serverless OpenTelemetry Collector 部署,面向 Google Cloud。它在 Cloud Run 上运行 OpenTelemetry Collector,从 Pub/Sub 和 Google Cloud Storage 摄取事件,将 Google Cloud 服务日志解析为 OpenTelemetry 语义约定,并将生成的 OTLP 数据转发到 Elastic,同时依赖 Cloud Run 进行扩展、执行和基础设施生命周期管理。

要在规模化场景下自信地运行用于 GCP 的 ECF,你需要了解它的容量特性和规模行为。对于作为更广泛 ECF 架构一部分的用于 GCP 的 ECF,我们通过可重复的负载测试并基于测量数据来回答这些问题。

我们将介绍测试设置,解释每个运行时设置,并分享我们在单个实例上观察到的容量数据。

我们如何对用于 GCP 的 EDOT Cloud Forwarder 进行负载测试

架构

负载测试架构模拟了一个真实的高吞吐量流水线:

  • 我们开发了一个负载测试服务,以尽可能快的速度将生成的日志文件上传到一个 GCS bucket。

  • 该 Google Cloud Storage( GCS )bucket 中的每次文件创建都会触发一个事件通知到 Pub/Sub。

  • Pub/Sub 将推送消息发送到一个 Cloud Run 服务,在该服务中 EDOT Cloud Forwarder 获取并处理这些日志文件。

我们的设置暴露了两个主要的可调参数,它们直接影响 Cloud Run 的扩展行为和内存压力:

  • 通过 concurrency 设置请求压力(每个 ECF 实例可以处理的并发请求数)。

  • 通过 log count 设置每个请求的工作量(每个上传对象中每个文件包含的日志数量)。

在我们的测试中,我们使用了一个测试系统,该系统:

  • 部署完整的测试基础设施。这包括完整的 ECF 基础设施、一个 mock backend 等。

  • 根据配置的 log count 生成日志文件,使用大小约为 1.4 KB 的 Cloud Audit log。

  • 在 concurrency 和日志数量的所有组合上运行一组矩阵测试。

  • 为每个测试的 concurrency 级别生成一份报告,其中包含多项统计数据,例如 CPU 使用率和内存消耗。

为了保证可复现性和隔离性,EDOT Cloud Forwarder 中的 otlphttp exporter 使用一个始终返回 HTTP 200 的 mock backend。这确保所有观察到的行为都归因于 ECF 本身,而不是下游系统或网络波动。

步骤 1:在测量容量之前建立稳定的运行时

在询问单个实例可以处理多少负载之前,我们首先建立了一个稳定的运行时基线。

我们很快发现,一个名为 cpu_idle 的标志就可能让 Cloud Run 陷入垃圾回收( GC )饥饿的陷阱。这一点又被 ECF 当前架构中的一个已知限制所放大:现有的 OpenTelemetry 实现会在处理之前将整个日志文件读入内存。我们的目标是消除配置带来的副作用,使容量测试能够反映 ECF 的真实极限。

我们重点关注了三个运行时参数:

设置控制内容对 ECF 的重要性
cpu_idleCPU 是始终分配还是仅在请求期间分配决定 垃圾回收器( GC )获得多少后台时间来回收内存
GOMEMLIMIT容器内 Go 堆大小的上限防止进程悄然增长直到 Cloud Run 因 OOM 将其终止
GOGCGo 中堆增长和回收的激进程度以更高的 CPU 消耗换取更低的内存使用

所有参数隔离测试都使用单个 Cloud Run 实例( min 0,max 1 ),为所研究的场景固定 concurrency,并在各次运行中保持输入文件和测试矩阵完全一致。这样的设计使我们能够将差异直接归因于被测试的参数本身。

CPU 分配:停止让 垃圾回收器 挨饿

Cloud Run 提供两种 CPU 分配模式:

  • 基于请求(受限)。通过 cpu_idle: true 启用。CPU 仅在请求被主动处理时可用。

  • 基于实例(始终开启)。通过 cpu_idle: false 启用。CPU 在空闲时仍然可用,从而允许 垃圾回收 等后台工作运行。

测试在完全相同的条件下对这两种模式进行了对比:

参数
vCPU1
Memory4 GiB(足够高以消除 OOM 作为因素)
GOMEMLIMIT内存的 90%
GOGC默认(未设置)
Concurrency10

我们观察到的结果

当 CPU 仅在请求期间分配( cpu_idle: true )时:

  • 内存波动极大(±71% RSS,±213% 堆内存)。

  • 在最差的运行中,堆内存峰值达到约 304 MB。

  • 样本中出现请求拒绝(成功率 90%)。

当 CPU 始终分配( cpu_idle: false )时:

  • 内存波动变得严格受控(±8% RSS,±32% 堆内存)。

  • 在最差的运行中,堆内存峰值降至约 89 MB。

  • 样本中没有出现请求拒绝(成功率 100%)。

从这些运行中我们看到:

  • 当 CPU 被限制时,Go 垃圾回收器实际上被饿死,导致堆积累和运行间大幅波动。

  • 当 CPU 始终可用时,垃圾回收跟得上分配速度,导致内存使用更低且更可预测。

结论:对于这组测试, cpu_idle: false 是最稳定的基线配置。基于请求的 CPU 限制引入了人为的不稳定性,使容量规划更加困难。

Go 内存限制:受限容器中的 GOMEMLIMIT

Cloud Run 在容器级别强制执行硬内存限制。如果进程超出限制,实例将被 OOM 杀死。

我们测试了 Cloud Run 配置如下:

参数
Container memory512 MiB
vCPU1
Concurrency20
GOGC默认(未设置)
cpu_idlefalse

测试比较了:

  • 无 GOMEMLIMIT(Go 依赖操作系统压力)。

  • GOMEMLIMIT=460MiB(或容器内存的 90%)。

结果非常明确:

GOMEMLIMIT结果说明
未设置不稳定;多次 OOM 杀死服务从未产生稳定结果
460MiB稳定;运行完成最差情况下峰值 RSS 达到约 505 MB,但进程仍在容器限制内

结论:在像 Cloud Run 这样的内存受限环境中,将 GOMEMLIMIT 设置接近(但低于)容器限制对于在负载下获得可预测行为至关重要。

GOGC:内存节省与可靠性

GOGC 参数控制堆在 GC 周期之间可以增长的百分比(%):

  • 较低值(如 GOGC=50):更频繁的回收,内存较低,CPU 较高。

  • 较高值(如 GOGC=100):回收较少,内存较高,CPU 较低。

测试覆盖了:

  1. GOGC=50(激进)

  2. GOGC=75(适中)

  3. GOGC=100(默认/未设置)

设置:

参数
Container memory4 GiB(足够高以消除 OOM 作为因素)
vCPU1
Concurrency10(安全水平)
GOMEMLIMIT内存的 90%
cpu_idlefalse

我们观察到的结果

从这些运行中:

GOGC峰值 RSS(样本)CPU 行为失败率说明
50~267 MB非常高;经常饱和30%GC 消耗了摄取所需的周期
75~454 MB~83.5% 平均10%GC 消耗了摄取所需的周期
100(默认)~472 MB~83.5% 平均;为突发保留余量

这些运行的结论很明确:降低 GOGC 以换取内存会降低可靠性,而这种权衡对 ECF 并不划算。

结论:对于此工作负载,默认的 GOGC=100 提供了最佳平衡。通过降低 GOGC 来优化内存的尝试直接降低了可靠性。

步骤 2:找到容量和临界点

在运行时稳定后,我们通过增加 concurrency 直到出现失败,评估单个实例可以承受的流量。

如何阅读表格:每个 concurrency 水平进行了 20 次运行,覆盖轻负载(每个文件 240 条日志,约 362KB 文件大小)和重负载(每个文件超过 6k 条日志,约 8MB 文件大小)。表格报告了轻负载的基线 RSS 以及最差运行的峰值。

Concurrency 5:稳定基线

在 concurrency 5 时,服务运行稳定。

情况内存(RSS)CPU 利用率请求被拒绝
基线(最轻负载平均)99.89 MB
最差运行211.02 MB86.43%

这证明单个实例可以轻松处理中等负载,内存使用保持在安全范围内。

Concurrency 10:安全但波动较大

在 concurrency 10 时,系统仍然可用,但波动较大。

情况内存(RSS)CPU 利用率请求被拒绝
基线(最轻负载平均)100.33 MB
最差运行424.80 MB94.10%否(样本中)

我们还注意到内存使用显示出极端波动:

  • 最佳运行 RSS:178 MB

  • 最差运行 RSS:425 MB

这种行为主要由两个因素造成:

  • Pub/Sub 突发交付:10 个重负载请求几乎同时到达。

  • Collector 内部使用 io.ReadAll:每个请求将整个日志文件读入内存。

当所有 10 个请求同时到达时,我们实际上在 GC 清理之前堆叠了约 10 倍文件大小的内存。当请求稍微错开时,GC 有时间在请求之间回收内存,从而导致峰值大幅下降。

这带来了一个关键的容量规划洞察:

  • 不要使用平均内存(例如约 260 MB)来进行服务容量规划。

  • 应根据观察到的最差突发情况(约 425 MB)进行容量规划,以避免 OOM 或 GC 停顿。

  • 实践中,在 concurrency 10 时,每个实例的内存限制应至少设置为 512 MiB。

Concurrency 20:不稳定,出现系统性负载削减

在 concurrency 20 时,系统持续开始削减负载。

情况内存(RSS)CPU 利用率请求被拒绝
基线(最轻负载平均)97.44 MB
最差运行482.42 MB88.90%是(每次运行)

即使内存和 CPU 指标看起来并没有比 concurrency 10 时明显恶化,行为却发生了质的变化:服务开始持续拒绝请求。

Concurrency 40:失败模式

在 concurrency 40 时,实例完全崩溃。内存和 CPU 超负荷,摄取可靠性崩溃。

情况内存(RSS)CPU 利用率请求被拒绝
基线(最轻负载平均)100.20 MB
最差运行1234.28 MB96.57%是(所有运行)

破坏点:1 vCPU 实例的实际极限

Concurrency峰值 RSS(MB)稳定性请求被拒绝?状态
5211.02低波动稳定基线
10424.80高波动安全但波动较大
20482.42高波动是(频繁)不稳定(削减负载)
401234.28极端波动是(总是)失败(内存爆炸)

结合 CPU 数据(concurrency 10 时峰值 94%),这支持一个实际规则:对于此工作负载和架构,每个 1 vCPU 实例最多可处理 10 个并发重负载请求。

将发现转化为具体建议

这些实验得出了明确、可操作的建议,用于在 Cloud Run 上运行 ECF OpenTelemetry collector,作为更广泛 Elastic Cloud Forwarder 部署的一部分。

范围:这些建议适用于我们测试的工作负载和测试环境(轻负载与重负载日志文件,最大 8MB,以及 Pub/Sub 突发交付),使用下列调优后的运行时设置。如果你的日志大小、请求突发性或管道结构有显著差异,请针对自身流量验证这些限制。

运行时和容器配置

区域建议原因
CPU 分配设置 cpu_idle: false(始终开启 CPU)避免 GC 饥饿,稳定内存波动,并消除因长时间 GC 暂停导致的请求失败
Go 内存限制将 GOMEMLIMIT 设置为约容器内存的 90%强制堆边界与 Cloud Run 限制对齐,使 Go 在操作系统之前做出反应,防止 OOM 杀死
垃圾回收保持 GOGC 为 100(默认)降低 GOGC 会以更高 CPU 消耗和可测失败率为代价降低内存使用

容量和每实例限制

对于运行 ECF OpenTelemetry collector 且已调优的 1 vCPU Cloud Run 实例:

限制建议原因
硬并发每个实例限制在 10 个请求在 concurrency 10 时,CPU 在最差运行已达到约 94%;更高并发会引发不稳定(请求被拒、GC 停顿)
内存每个实例至少使用 512 MiB(针对 concurrency 10)最差观察到的 RSS 约为 425 MB;512 MiB 为突发对齐提供了狭窄但可用的安全余量

扩展策略:水平扩展,而非垂直扩展

  • 垂直扩展(增加每实例并发)很快就会触及 CPU 和内存极限,不适合此工作负载。

  • 水平扩展更合适:将每个实例视为工作单元,硬限制为 10 个并发重任务。

实际操作:

  • 配置服务,使任何实例的并发请求不超过 10。

  • 让自动扩展通过增加实例应对负载增加,而不是增加每实例的并发数。

关键结论

  • 调优的运行时设置与原始资源同样重要:像 cpu_idle 这样的单个标志就可能决定行为是可预测还是由 GC 驱动的混乱。

  • Go 在容器中需要明确的限制:在内存受限环境中必须设置 GOMEMLIMIT,否则在重负载摄取下 OOM 杀死不可避免。

  • “更低内存” 并不总是更好:激进的 GC 调优(GOGC < 100)确实降低了内存使用,但直接提高了失败率。

  • 对于 1 vCPU 的 ECF 实例,concurrency 10 是现实上限;超过此值,请求拒绝和不稳定将成为常态。

  • 水平扩展是正确模型:每个实例应视为一个 10 请求的工作单元,更高的总吞吐量应通过增加工作实例数量实现,而不是提高每个实例的并发数。

原文:www.elastic.co/observabili…