Agent Substrate 遥测基准测试全解析:用 OTel 场景阶梯量化 telemetry 开销与 Collector 成本
2026/9/24 4:13:29 网站建设 项目流程
  • 人工智能
  • AI Agent
  • Agent 沙箱
  • 云原生
  • 容器运行时
  • 零信任

【免费下载链接】substrate

Agent Substrate: the core system

项目地址:https://gitcode.com/GitHub_Trending/substrate7/substrate
点击查看免费下载

本文是 Agent Substrate(benchmarking/observability.md)的遥测基准测试实战指南。它回答一个工程上最实际的问题:substrate 的控制面与 actor 运行时到底向外发送多少遥测数据,这些数据又让 OpenTelemetry Collector 付出多少 CPU、内存与副本代价。读完本文,你将掌握:如何用 S0–S3 四个场景阶梯(scenario ladder)一次性跑完空闲基线、用户数扫描、采样率扫描与长时间浸泡四种测量;如何部署一个"tee 式"遥测计量器(telemetry meter)按服务统计 spans 与 datapoints;以及如何用一组 PromQL 查询和通过标准判断一次运行是否有效、Collector 是否还能承受更多负载。

1. 遥测基准测试测量什么

遥测基准测试回答两个问题:

  1. volume(遥测量):substrate 各组件每个服务发送多少 spans 与 metric datapoints;
  2. cost(成本):这些遥测让承载它们的 OTel Collector 付出多少 CPU、内存和副本数。

做一次测量,需要三份资料配合:

  • 测量步骤见 benchmarking/telemetry/README.md:它说明如何安装计量器、把控制面与 actor 负载指向它,以及如何读取数字;
  • 体积模型与 Collector 规模指引见 docs/dev/best-practices/otel-collector.md;
  • 本页(benchmarking/observability.md)则提供场景阶梯——即跑哪些负载组合、每一步持续多久、通过标准是什么。

一个值得注意的设计决定是:本仓库的 observability 页面不存放任何测量结果。自动化会把每次运行的值写入 GCS。原因是结果一旦写进仓库,任何一次采样率或 instrument 的修改都会让旧结果失效,而读者无法分辨哪份拷贝才是正确的。结果以运行产物(artifact)的形式存在,而不是以文档的形式固化。

2. 测量前置:tee 式遥测计量器(telemetry meter)

GKE 的托管 Collector 只把自己的自指标上报到 Cloud Monitoring,不提供按数据源(每个 service)的拆分数据。因此 benchmarking/telemetry/meter.yaml 部署了第二个 Collector,专门按service.name统计 spans 与 datapoints。

2.1 meter 是一个 tee,不是终点

meter 的关键设计是"tee(分路)":它统计遥测数据,然后把数据原样转发给托管 Collector,因此托管 Collector 仍然承担运行的负载,它的 CPU、内存与副本数在测量期间保持真实有效。meter 不是托管 Collector 的替代品。

kubectl apply -f benchmarking/telemetry/meter.yaml METER=http://telemetry-meter.benchmarking.svc.cluster.local:4317 ./hack/install-ate.sh --deploy-ate-system --otlp-endpoint "${METER}"

--otlp-endpoint会 patchate-otel-configConfigMap 并重启读取它的工作负载。因为ateapiate-controllerateletatenet-router都通过envFrom读取该 ConfigMap,所以一次 patch 即可覆盖所有控制面组件ate-controller还会把值复制到它创建的 ateom worker pod。

actor 容器则不同:substrate 不在 actor 容器里放任何 OTLP 配置,它们使用 ActorTemplate 的env

./benchmarking/workloads/deploy.sh --deploy

不给--otlp-endpoint时,benchmarking/workloads/deploy.sh 会从ate-otel-configConfigMap 读取端点(resolve_otlp_endpoint()函数);上面一步已经改写了该 ConfigMap,因此 actor 会跟着控制面一起指向 meter。只有当你想让 actor 与 control plane 发往不同地址时才显式传--otlp-endpoint

重要:让负载生成器(locust/boomer)继续走平时的 Collector。否则 meter 会把负载生成器自身的遥测也计入 substrate 的遥测总量。

2.2 meter 的内部结构(源码级)

从 benchmarking/telemetry/meter.yaml 可以看到 meter 的核心管线:

  • otlpreceiver 监听 4317(gRPC)与 4318(HTTP);
  • count connector为 spans 和 datapoints 各生成一个计数指标substrate_spans_total/substrate_datapoints_total,按service.name(缺失时归入unknown)聚合;connector 声明MutatesData=false,因此不会改动转发给托管 Collector 的数据;
  • deltatocumulativeprocessor 把 connector 产生的 delta 计数转成累积计数——否则 Prometheus exporter 会把每个新 start timestamp 当成一次计数器重置,每次抓取只能看到最后一个 batch;
  • prometheusexporter 在8889端口暴露计数,在8888端口暴露 meter 自身的otelcol_*自指标;
  • otlp/managedexporter 把数据转发给托管 Collector(opentelemetry-collector.gke-managed-otel.svc.cluster.local:4317)。

此外,traces/countmetrics/count两条计数管线、traces/forwardmetrics/forward两条转发管线共享同一个 receiver;memory_limiter在每个读取 receiver 的管线中排在首位,但metrics/count-out管线故意不加 limiter——在那里拒绝数据会直接毁掉这次运行的测量。

2.3 在 kind 集群上测量

不要在 kind 上安装 meter。kind 里的 Collector 是自有的,manifests/ate-install/kind/otel-collector.yaml 已经内置了同样的 count connector,能给出同样的两个计数。在它前面再加一个 meter 只是多一跳,测量不到任何新东西。meter 只服务于 GKE——那里的托管 Collector 无法容纳 connector。

hack/install-ate.sh会在 kind 上同时安装 Collector 与 Prometheus,因此 kind 集群无需额外步骤:

kubectl port-forward -n otel-system svc/prometheus 9090:9090

查询语句与 GKE 完全相同,只有三点差异:

  • Prometheus 与 Collector 位于otel-system命名空间,而不是benchmarking
  • 不传--otlp-endpoint:kind 的ate-otel-configConfigMap 已指名 Collector,benchmarking/workloads/deploy.sh会读取它,actor 无需任何 flag 即跟随控制面;
  • kind 的 ConfigMap 把OTEL_METRIC_EXPORT_INTERVAL设为 10s(GKE 为 60s),因此 kind 上查询范围[1m]就够用。

注意 benchmarking/monitoring.yaml 与 cAdvisor 仅用于 GKE:kind 集群只测 volume——它的 Collector 是单副本、无 HPA,节点内存就是整台机器的内存,成本数字无法外推到真实集群。

3. 场景阶梯(Scenario Ladder):S0–S3

阶梯定义在 benchmarking/automation/tests.yaml 中,是四个名称以observability_开头的测试。每个测试是"一个或多个阶梯步"的一次运行:

#测试名时长阶梯步
S0observability_s0_idle_floor5m无负载(空闲底线)
S1observability_s1_user_sweep每步 3m用户数 5 → 10 → 15,--trace-probability 0.1
S2observability_s2_sample_rate_sweep每步 3m10 个用户,采样率 0 → 0.1 → 1.0
S3observability_s3_soak10m12 个用户(S1 最大值的 80%)

对应的 tests.yaml 条目揭示了每个测试的实际参数。例如 S1:

- name: observability_s1_user_sweep type: locust file: /app/tests/glutton.py,/app/shapes/ladder_shape.py duration: 9m users: 5 workerCount: 10 flags: - "--ladder" - "5:3m,10:3m,15:3m" - "--trace-probability" - "0.1"

S2 的阶梯步带有采样率变化:--ladder 10:3m:0,10:3m:0.1,10:3m:1.0;S0 是--ladder 0:5m并带--allow-empty-stats(因为空闲底线不发任何请求,locust 不会产生测量行,运行结果只存在于遥测计数中);S3 是--ladder 12:10m

3.1 为什么负载必须来自 boomer,而不是 Python

每个测试都使用GluttonUser,因此负载来自boomer worker(Go 实现的 locust worker)。不要把阶梯放在 Python 用户类上——Python 与 gRPC 在高用户数下会互相钳制,此时的延迟是负载生成器自身的延迟,运行测到的是生成器而不是 substrate。

在 benchmarking/locust/runner.py 中可以看到这一机制:needs_boomer()判断测试文件不在PYTHON_TESTS闭集(ate_api.pycounter_demo.pysleep.pyusermem.pykernelmem.py)内时,就以--master --expect-workers 1启动 locust,并同时以子进程启动 boomer worker(BOOMER_BINARY = "/app/boomer-worker")。glutton.py 的阶梯自然落在 boomer 上。

3.2--ladder:一次运行跑完整个扫描

一次运行的步进是--ladder users:duration[:trace_probability],由 benchmarking/locust/shapes/ladder_shape.py 解析。parse_ladder()用正则^(?P<users>\d+):(?P<duration>\d+)(?P<unit>[smh]?)(?::(?P<probability>[0-9.]+))?$解析每个步,步长单位支持 s/m/h,概率必须落在 0.0–1.0,任何解析失败都会直接抛ValueError——因为"跑错了场景的阶梯"产出的表格,读者无法与正确运行区分开。

形状(shape)让每个步保持其时长然后进入下一步,因此一次运行即可得到完整的扫描,且只有一次部署、一个基线。LadderShape.tick()按累积时间边界推进步进,--ladder-spawn-rate(默认 5.0 用户/秒)控制步进切换时的生成速率。

命名了概率的步会在步开始时改变采样率:值写入 master 的 parsed options,boomer 在步切换触发的 spawn 消息时从/boomer-config读取。--trace-probability给出第一个命名概率步之前的值;若阶梯所有步都不含概率,则全程保持该 flag 的值。

3.3 三条硬性规则

  • 步长不得小于 3 分钟。指标推送间隔是 60 秒,更短的步会让每条序列拿不到三个数据点,你就无法区分趋势与噪声。
  • S1 的用户数止步于 15。因为运行的 worker 池是 10 个 worker(workerCount: 10)。超过这个数的步测到的是调度器队列,而不是工作系统的遥测。想测更高用户数,必须同时提高workerCount
  • locust web UI 只用于人工检查,不承载阶梯。且有两个特殊条件:boomer-workersidecar 会为你(在表单中)选择的每个用户类制造自己的负载;表单能改 boomer worker 的采样率但改不了 Python worker 的采样率——benchmarking/locust/manifests/locust.yaml 给 boomer 传了--master-web-port=8089,boomer 在每次 spawn 消息时从 master 读取/boomer-config(该端点由benchmarking/locust/common/boomer_config.py在 master 上提供)。

4. 读取数字:Prometheus 查询与通过标准

benchmarking/monitoring.yaml 负责抓取 meter(对telemetry-meterpod 的 8889 端口做注解自动发现,另设telemetry-meter-self任务抓取 8888 的自指标)。不要手工比较原始 scrape 结果,通过 Prometheus 读取:

kubectl apply -f benchmarking/monitoring.yaml kubectl port-forward -n benchmarking svc/prometheus 9090:9090

4.1 核心查询

要查什么查询
每个服务的 spans/秒sum by (service_name) (rate(substrate_spans_total[5m]))
每个服务的 datapoints/分60 * sum by (service_name) (rate(substrate_datapoints_total[5m]))
数据是否到达sum(rate(otelcol_receiver_accepted_spans[5m]))
Collector 是否拒绝数据sum(rate(otelcol_receiver_refused_spans[5m]))
count 是否丢弃序列sum(otelcol_deltatocumulative_datapoints{error="limit"})
距 stream 上限有多近otelcol_deltatocumulative_streams_tracked / otelcol_deltatocumulative_streams_limit

范围至少取推送间隔的三倍:60s 推送下[5m]是不错的默认值,小于 3m 的范围只会带来噪声。

4.2 正控制(positive control):零与"没来"的区别

始终读取最后四个计数器。一个零体积的服务本身不产生数据:必须在同一窗口内accepted大于零且refused为零。若不然,那个零说明的是"数据没有到达",而不是"服务没有发送"。不检查正控制,一个什么都没发的 exporter 看起来和一次好运行一模一样。

后两个计数器覆盖max_streams这种不同的失效模式:count connector 把 resource 复制到每个计数上,serverboot给每个进程独立的service.instance.id,因此一个进程在每种计数上持有一条 stream。超过上限后deltatocumulative会静默丢弃每条新 stream(没有日志行),序列悄悄离开 scrape——体积读起来偏低,却没有任何错误报告。因此:丢弃计数必须为零,比值在运行全程必须低于 1。

4.3 通过标准(Pass Criteria)

一次合格的运行必须同时满足:

  • otelcol_receiver_refused_*必须为零;
  • otelcol_exporter_enqueue_failed_*必须为零;
  • 客户端queue_full必须为零;
  • Collector 副本数必须保持在maxReplicas之下;
  • 在 S3 中,Collector working set 的斜率与otelcol_exporter_queue_size的斜率必须等价于零(即长时间浸泡下不增长)。

同时检查正控制:每个必须发送数据的服务,其otelcol_receiver_accepted_*必须大于零。

5. meter 的三种边界条件(Three Conditions of the Tee)

meter 是 tee,这让测量正确,但也引入了三个必须理解的条件:

① 托管 Collector 会把遥测归因到 meter。它应用k8sattributesfrom: connection,每个转发项现在都来自 meter pod。体积与 CPU 仍然正确,但在托管侧按 pod 分组的查询会失真。应改按 resource 中的service.name分组——该值在跳转后仍然正确。

② 负载形状会改变。通常很多进程各自维持一条到托管 Collector 的连接;有了 tee 之后变成一个发送方、更大的 batch。因此总量正确,但跨副本的分布不再真实,连接粘性(connection stickiness)的效应消失。

③ meter 本身可能成为瓶颈。它是单副本。把它的自指标加入通过标准:

sum(rate(otelcol_receiver_refused_spans[5m])) # 必须为 0 sum(rate(otelcol_exporter_sent_spans[5m])) # 必须大于 0 max_over_time(otelcol_exporter_queue_size[5m]) # 必须保持水平 absent(otelcol_exporter_sent_spans) # 必须无结果 sum(otelcol_deltatocumulative_datapoints{error="limit"}) # 必须为 0 otelcol_deltatocumulative_streams_tracked / otelcol_deltatocumulative_streams_limit # 必须低于 1

不要单独使用otelcol_exporter_send_failed_spans:默认retry_on_failure.max_elapsed_time是 5 分钟,exporter 会先重试那么久才计入失败——一个正在丢数据的 meter 在短步长内看起来完全正常。队列深度变化立即发生,因此它才是要盯的信号。absent()捕获另一种情况:meter 什么都不上报。

如果 meter 丢弃了数据,volume 与 cost 会同时错误,且两个错误互相掩盖

memory_limiterGOMEMLIMIT(meter.yaml 中GOMEMLIMIT=3000MiB)让 meter 在到达内存上限时以"拒绝数据"的方式失败——显式暴露在otelcol_receiver_refused_*里,而不是被内核杀死。注意:limiter 用百分比会跟随容器上限,但GOMEMLIMIT是绝对值,扩大容器限制时需要同步更新它。

要让 meter 变成终点(terminal):删除 meter.yaml 中的两条 forward 管线即可(计数继续工作)。长 soak 测试建议用 terminal meter,把遥测挡在 Cloud Monitoring 之外;但此时托管 Collector 的资源值变成空闲 Collector 的值,不得记录它们

6. 客户端丢弃计数器:meter 看不见的丢失

meter 只显示"到达"的数据,不显示客户端丢弃的数据。要打开 SDK 里的插桩:

kubectl set env -n ate-system deployment/ate-api-server OTEL_GO_X_OBSERVABILITY=true

抓取进程上的:9090/metrics——该端点是 Prometheus 读取器,不走 OTLP 路径,因此在它测量的拥塞期间仍能正常工作。

指标说明
otel_sdk_span_started_total{otel_span_parent_origin,otel_span_sampling_result}采样决策(根 span 与继承 span)
otel_sdk_processor_span_processed_total{error_type="queue_full"}客户端丢弃的 spans
otel_sdk_processor_span_queue_sizevs_capacity丢弃开始前的余量

没有这个丢弃计数器,客户端侧的丢失看起来和一个稳定的平台期一模一样。该 flag 是实验性的,位于sdk/internal/x,指标名会随 SDK 升级而变化——每次升级后要重新核对名称。

7. 移除 meter

测量结束后按原路径恢复:

./hack/install-ate.sh --deploy-ate-system ./benchmarking/workloads/deploy.sh --deploy kubectl delete -f benchmarking/telemetry/meter.yaml

两个脚本在不传--otlp-endpoint时都会回到默认端点。

8. 开放事项(Open Items)

benchmarking/observability.md 列出了当前流程尚未覆盖的改进点,可作为继续深化该基准测试的路线图:

  • 把每个服务的 volume 作为每次运行的产物runner.py只写 locust 统计,读者仍须自己查询 Prometheus 才能拿到本次运行的substrate_*计数;
  • 用 working set 测量 Collector 的 CPU 与内存:不要用kubectl top,它计算的是平均值,会掩盖峰值(这也呼应了 docs/dev/best-practices/otel-collector.md 中"用container_memory_working_set_bytes,它是 OOM killer 实际使用的值"的结论);
  • 每张 volume 表都记录otelcol_receiver_accepted_*作为正控制
  • 记录副本数:记录 CPU 与内存时若不记录副本数,一个没有容量的 Collector 和一个容量富余的 Collector 会给出完全相同的表;
  • 测量 actor volumeglutton模板现在设置了 OTLP endpoint,actor 遥测第一次真正到达(这是 benchmarking/workloads/deploy.sh 中--otlp-endpoint与 ActorTemplateenv机制带来的新能力);
  • 测量 Collector 规模指引中的 DaemonSet 配置:目前所有测量都基于 Deployment 模式,DaemonSet 模式的结论仍是外推,需要对照实测确认(见 docs/dev/best-practices/otel-collector.md 末尾的警告)。

9. 与 Collector 规模指引的衔接

本页面只负责"怎么测","测得的数据意味着什么"由 docs/dev/best-practices/otel-collector.md 解释。两个关键结论直接指导阶梯设计:

  • metrics 随组件数量增长datapoints/min ≈ 节点数×atelet_dp + ateapi 副本×ateapi_dp + router 副本×router_dp + worker pod 数×ateom_dp + 插桩 actor 数×actor_dp。每个二进制在 SDK 默认 60s 的PeriodicReader上推送且无抖动(见 internal/serverboot/serverboot.go),因此一起启动的进程也会一起推送;请求速率对这个项影响很小。这也是 S0 空闲底线存在的意义——组件数量本身就有遥测成本。
  • traces 随操作速率增长spans/sec ≈ (actor 生命周期操作/秒 + actor 请求/秒) × P(根采样) × spans_per_op。自ParentBased(TraceIDRatioBased)成为默认以来(ateapi/atelet/ateom-* 为 0.1,atenet-router 与 Envoy 为 0.01),只有"没有 traceparent 到达"的操作才由本地采样率决定,客户端一旦带traceparent就为整条链路做了决定。这正解释了 S2 为什么要在固定 10 用户下扫描采样率 0 → 0.1 → 1.0:它在单独刻画 traces 这个成本轴。

因此阶梯的两个维度(S1 用户数扫描 + S2 采样率扫描)分别对应 volume 模型的两个独立驱动轴,S0 提供组件基数基线,S3 则验证在 80% 峰值负载下长期浸泡时 Collector 资源曲线是否保持水平——这正是判断 Collector 是否需要扩容(Deployment 换 DaemonSet、提升副本、降低采样率)所必需的证据。

  • 人工智能
  • AI Agent
  • Agent 沙箱
  • 云原生
  • 容器运行时
  • 零信任

【免费下载链接】substrate

Agent Substrate: the core system

项目地址:https://gitcode.com/GitHub_Trending/substrate7/substrate
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询