news 2026/9/23 20:24:45

Agent Substrate 分布式追踪实践指南:基于 OpenTelemetry 的 Span 生成、采样策略与端到端链路实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent Substrate 分布式追踪实践指南:基于 OpenTelemetry 的 Span 生成、采样策略与端到端链路实现
  • 人工智能
  • AI Agent
  • Agent 沙箱
  • 云原生
  • 容器运行时
  • 零信任

【免费下载链接】substrate

Agent Substrate: the core system

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

本指南聚焦 Agent Substrate(substrate)项目基于 OpenTelemetry 的分布式追踪(Tracing)最佳实践,覆盖 span/trace 的基本模型、服务端与客户端两侧的接入方式、按组件设定的采样默认值及其环境变量覆盖机制,以及为性能/压测临时禁用追踪的完整方法。读完本文,你将掌握如何在 substrate 的控制平面组件(ateapi、atelet、ateom 系列、atenet router)中正确初始化 TracerProvider、接入 OTLP 导出器与 TraceContext 传播器,并为 gRPC/HTTP 服务与客户端补齐端到端的链路追踪能力。

为什么需要追踪(Tracing)

在 Agent Substrate 这类由 ateapi、atecontroller、atelet、ateom(gvisor/microvm)、atenet router 等多个长驻进程协同工作的系统中,一次用户请求往往要穿越控制面、数据面与沙箱运行时多个组件。追踪的价值在于:

  • 调试:能够还原一次请求在系统中的完整处理路径,定位是哪一跳出错、哪一段异常;
  • 性能优化:量化每个步骤的耗时,找出瓶颈所在——例如请求在 ateapi 排队、在 atenet router 的 ext_proc 往返、还是在 ateom 沙箱内执行;
  • 成本控制:通过合理的采样率,在可观测性与存储/带宽成本之间取得平衡。

什么是追踪:Span 与 Trace 模型

追踪是对一次请求在系统中流动过程的跟踪。理想情况下,一次追踪应该覆盖请求从客户端到服务端再返回的完整旅程,并包含沿途被调用的所有服务。

追踪由两个核心概念组成:

  • Span(跨度):一个单一操作。Span 有开始时间和结束时间,可以携带属性(attribute,即键值对)来描述该操作的附加信息,例如操作的输入输出、错误码、资源标识等。
  • Trace(追踪):一组相互关联的 Span 的集合。

每个 Trace 拥有一个trace ID,用于标识整条链路;每个 Span 拥有一个span ID,用于在链路内标识自己。父 Span 通过引用子 Span 的 ID 构建出层级关系,最终形成一棵完整的调用树。

追踪数据如何传播

追踪数据在 Go 中存放在 context 对象里,随调用栈一路传递,因此任何函数只要拿到ctx,就能启动子 Span 并保持父子关系。

跨进程传播时:

  • HTTP:追踪数据通过 HTTP 头(即 W3C 的traceparent/tracestate)传递;
  • gRPC:追踪数据放在 gRPC 的 metadata 对象中。

Otel 中间件(otelgrpc的 StatsHandler、otelhttp的 Handler/Transport)会自动完成追踪数据的提取(extraction)注入(injection),业务代码通常无需手工解析或构造传播头。

服务端一侧则运行着一个导出器(exporter)服务:Span 先被批量缓冲(batcher),再推送到远端 collector 进行分析。在 substrate 中,这一导出链路由serverboot.InitTracing统一装配。

服务端如何实现追踪

初始化导出器与 TracerProvider

所有长驻服务进程都需要在启动阶段初始化 OpenTelemetry 导出器和 tracer provider。参考 internal/serverboot/serverboot.go 中的InitTracing(其典型用法见 cmd/ateapi/main.go):

tp, err := serverboot.InitTracing(ctx, serverboot.TracingOptions{ ServiceName: "ateapi", Sampling: serverboot.ResolveTraceSampling(ctx, serverboot.ParentRatioSampling(serverboot.ControlPlaneTraceRatio)), }) if err != nil { serverboot.Fatal(ctx, "Failed to initialize tracing", err) } defer serverboot.ShutdownProvider("TracerProvider", tp.Shutdown)

从源码看(internal/serverboot/serverboot.go),InitTracing会完成以下装配:

  1. Resource:通过newResource写入service.name(来自TracingOptions.ServiceName)与service.instance.id(进程内生成的 UUID),并调用resource.WithFromEnv()OTEL_*环境变量可以覆盖默认值;
  2. OTLP 导出器:使用otlptracegrpc创建,默认走WithInsecure()——substrate 明确不验证 collector 的 TLS 证书,因为 GKE 托管 traces 不支持该校验(源码注释原文说明);
  3. Sampler:使用opts.Sampling.Sampler()作为 SDK 采样器(关于如何构造,见下文采样一节);
  4. Batchersdktrace.WithBatcher(exporter)让 Span 批量导出;
  5. 全局注册otel.SetTracerProvider(tp)otel.SetTextMapPropagator(propagation.TraceContext{}),即只注册 W3C TraceContext 传播器;
  6. 错误处理otel.SetErrorHandler把 SDK 内部错误(包括环境变量解析告警)统一落入 slog,避免绕过 JSON 日志写到 stderr。

注意两点:

  • 必须defer关闭 provider(如上),保证进程退出时缓冲中的 Span 能被冲刷到 collector;
  • 每个组件通过 ServiceName 标识自己,collector 据此区分 Span 来自哪个进程。

TracingOptions还支持两个可选字段(internal/serverboot/serverboot.go):

  • ExporterConn:传入一个已建立的*grpc.ClientConn时,导出器改走该连接而不再自行拨号OTEL_EXPORTER_OTLP_ENDPOINT。ateom 工作进程正是借此把 unix socket 交给 atelet 的 relay(见 internal/otlprelay),让没有独立网络路径的工作 Pod 也能导出 Span;
  • RelayCapable:标记该组件本应走 relay 导出,用于在 span 资源上打上导出路径属性(relay 或 direct),方便区分"正常走 relay"与"relay 拨号失败、退化为直连"这两种状态。

各组件如何调用 InitTracing

从 cmd 下的入口文件可以看到,substrate 的各服务都遵循同一套样板:

组件ServiceName默认采样是否经 relay
ateapi(cmd/ateapi/main.go)ateapiparentbased_traceidratio0.1
atecontroller(cmd/atecontroller/main.go)运行时 serviceNameparentbased_traceidratio0.1
atelet(cmd/atelet/main.go)ateletparentbased_traceidratio0.1
ateom-gvisor(cmd/ateom-gvisor/main.go)运行时 serviceNameparentbased_traceidratio0.1是(ExporterConn + RelayCapable)
ateom-microvm(cmd/ateom-microvm/main.go)运行时 serviceNameparentbased_traceidratio0.1是(ExporterConn + RelayCapable)
atenet router(cmd/atenet/internal/router/router.go)extproc ServiceNameparentbased_traceidratio0.01
glutton(cmd/benchmarking/glutton/main.go)gluttonparentbased_always_off

其中ControlPlaneTraceRatio = 0.1定义在 internal/serverboot/sampling.go,注释说明了理由:控制面流量小、其链路正是生命周期调试所需要的,所以默认给得相对慷慨。

采样默认值与覆盖机制

所有采样器都通过serverboot.ResolveTraceSampling解析:它会在组件默认值之上,应用标准的OTEL_TRACES_SAMPLER/OTEL_TRACES_SAMPLER_ARG环境变量。必须把组件默认值传进去,绝不能把裸 sampler 直接交给 provider——一旦显式指定 sampler,环境变量就会被 SDK 静默忽略。

各组件默认值汇总:

组件默认值
ateapi、atelet、ateom-gvisor、ateom-microvmparentbased_traceidratio0.1
atenet router(数据面根)parentbased_traceidratio0.01,并镜像到 Envoy 的RandomSampling
glutton(benchmarking)parentbased_always_off
boomer(benchmarking)由 dynconfig 运行时控制,忽略环境变量

这些默认值全部基于ParentBased(父级采样),因此:已经带采样标记到达的请求,在每一跳都保持采样;显式标记为未采样的请求也一路保持未采样。只有"无父级"(parentless)的请求才受比例影响,由该链路在哪个组件上生根来决定(internal/serverboot/sampling.go 中的ParentRatioSamplingParentNeverSampling实现)。

解析行为上有两个关键点(对应 internal/serverboot/sampling.go 的resolveTraceSampling):

  1. 无效输入保持默认并告警:采样器名无效、OTEL_TRACES_SAMPLER_ARG缺失或比例无法解析/超出[0, 1]区间时,保留组件默认值并记录一条 warning。这与 OTel SDK 自身的处理有意的不同——SDK 在无效输入时回退到 100% 采样、缺 arg 时把比例读作 1.0,对生产环境过于激进;
  2. 空值视为未设置:与 SDK 不同,OTEL_TRACES_SAMPLER被设置为空字符串时按未设置处理,兼容模板渲染出空环境变量的场景。

支持的采样器名:always_onalways_offtraceidratioparentbased_always_onparentbased_always_offparentbased_traceidratio(后两者为比例类,必须配套OTEL_TRACES_SAMPLER_ARG)。

关于数据面根采样率还有一个特殊点:agentgateway 模式下,数据面根比例存放在 agentgateway 的 ConfigMap 中(randomSampling键,同样默认 0.01)。它不像 Envoy 的RandomSampling那样会被 router 上的环境变量覆盖——它是静态配置,调整时必须两处一起改。

这些都是头部(head)采样比例,用于限制离开进程的数据量。任何基于请求结果(错误、延迟)的决策——即尾部(tail)采样——应当放在 collector 管线中完成,而不是放进 substrate 二进制里。

router 侧还有一个保证"两边不漂移"的细节:TraceSampling结构把根比例(rootRatio)与 sampler 一起保存(internal/serverboot/sampling.go),router 在初始化时解析一次采样策略,再通过XdsServer.SetTraceRootSamplingPercent把百分比镜像进 Envoy 的RandomSampling(见 cmd/atenet/internal/router/xds.go)。这样 router 自己的 SDK 采样器与 Envoy 对无traceparent请求的根采样决策不会发生漂移,对应测试见 cmd/atenet/internal/router/xds_test.go。

禁用追踪(性能/压测场景)

在性能测试或负载压测时,需要彻底关闭被压组件的追踪:

  • 在被测组件上设置OTEL_TRACES_SAMPLER=always_off;对于 ateom 工作进程,通过 controller 的--otel-traces-sampler标志传递;
  • 仅设parentbased_always_off不够:boomer 和 locust 这类压测发生器会发送带比例采样的 trace context,而基于父级的采样器会遵从它。此时要么把发生器的trace_probability设为 0 而保持服务端不动,要么两者同时处理;
  • 在 kind 环境上,还需要额外覆盖 ateapi 的parentbased_always_on固定值(kind 部署中 ateapi 被钉为全采样,需显式改回)。

通过 ConfigMap 注入导出端点

服务的 YAML 清单必须设置OTEL_EXPORTER_OTLP_ENDPOINT,导出器才知道把 Span 推到哪里。不要硬编码该地址——统一通过envFrom消费共享的ate-otel-configConfigMap,让服务在哪个环境部署就自动跟随该环境的 collector 地址:

containers: - name: ateapi image: ko://github.com/agent-substrate/substrate/cmd/ateapi ports: - containerPort: 443 # Supplies OTEL_EXPORTER_OTLP_ENDPOINT (and, on kind, the metric # export tunables) for every control plane component. envFrom: - configMapRef: name: ate-otel-config

该 ConfigMap 的 GKE 版本定义在 manifests/ate-install/ate-otel-config.yaml,指向http://opentelemetry-collector.gke-managed-otel.svc.cluster.local:4317;kind 环境用同名 ConfigMap 替换为集群内 collector 地址(manifests/ate-install/kind/ate-otel-config.yaml,http://opentelemetry-collector.otel-system.svc:4317),并额外调短指标导出间隔、开启日志导出器。实际部署中 manifests/ate-install/ate-api-server.yaml、manifests/ate-install/ate-controller.yaml、manifests/ate-install/atelet.yaml 均通过envFrom引用该 ConfigMap。

注意:编辑 ConfigMap 不会自动重启消费它的 Pod——Pod 模板没有变化,不会触发新的 rollout。修改后需要手动执行kubectl rollout restart使改动生效。

关于 collector 的部署方式——GKE 托管选项、自管 DaemonSet,以及 substrate 可访问端点方面的约束——参见 OpenTelemetry Collector 最佳实践。

gRPC 服务端中间件

实现 gRPC 服务端时,应在创建 server 时挂上 otelgrpc 的 StatsHandler:

server := grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler()) )

otelgrpc.NewServerHandler会自动提取入站 metadata 中的 trace context、启动服务端 Span 并在响应中回传。注意它在构造时捕获全局 TracerProvider,因此必须先完成InitTracing再创建 gRPC server/client——substrate 各组件入口都在代码注释中强调了这一点。

HTTP 服务端中间件

实现 HTTP 服务端时,用otelhttp.NewHandler包裹根 mux:

tracedMux := otelhttp.NewHandler( mux, "/", )

但这种做法只保证所有请求"有资格"被追踪,并不会把请求的性质写进 Span。因此你需要在业务 handler 中手工创建 Span 来刻画请求的语义:

tracer := otel.Tracer("my-server-name") func someHandler(w http.ResponseWriter, r *http.Request) { ctx, span := tracer.Start(r.Context(), "operationIdentifier") defer span.End() // ... rest of your handler }

子 Span(Sub-Spans)

如果需要暴露服务内部的处理细节,可以在任意位置创建子 Span:

tracer := otel.Tracer("my-package-name") func someFunc(ctx context.Context) { ctx, span := tracer.Start(ctx, "operationIdentifier") defer span.End() }

只要ctx来自上层已追踪的请求,新 Span 就会自动成为其子节点,从而在 trace 中呈现完整的内部调用层级。

客户端如何实现追踪

客户端不要求实例化导出器,但应当提供把追踪元数据注入请求的能力,让用户有机会发起一条追踪。

Golang 客户端

与服务端一样,客户端也需要初始化并关闭 tracer provider,但不需要导出器。关键约束:当没有请求追踪时,什么都不要安装。如果装一个带NeverSample采样器的 provider,它会注入一个显式"未采样"的 trace context,从而把所有下游基于ParentBased的服务端采样器钉死在未采样状态,把服务端比例采样彻底废掉。而保持 OTel 全局为 noop 时,不会注入任何 context,服务端会自己作为链路根按比例采样:

func initTracing(ctx context.Context, enabled bool) (*sdktrace.TracerProvider, error) { if !enabled { return nil, nil } res, err := resource.New(ctx, resource.WithAttributes( semconv.UserAgentOriginal("my-client-name"), ), ) if err != nil { return nil, fmt.Errorf("failed to create resource: %w", err) } tp := sdktrace.NewTracerProvider( sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.AlwaysSample()), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.TraceContext{}) return tp, nil }

如果你的服务同时扮演客户端角色,这一步是冗余的,可以省略(直接复用服务端已初始化的全局 provider)。

属性约定:这里设置UserAgentOriginal是因为假设这是面向用户的客户端;如果是系统服务,则应设置ServiceName属性。

gRPC 客户端

使用 gRPC 客户端时,加入 stats handler:

clientConn, err := grpc.NewClient( serverAddr, grpc.WithStatsHandler(otelgrpc.NewClientHandler()), )
HTTP 客户端

HTTP 客户端则在 transport 上套一层 Otel 的包装:

client := &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), }

substrate 的 kubectl-ate 命令行工具就提供了开箱即用的按需追踪:--trace标志会生成 trace ID 并通知服务端对本次请求进行追踪(详见 cmd/kubectl-ate/README.md)。在 kind 环境下,hack/install-ate-kind.sh --deploy-ate-system已预置集群内 OpenTelemetry Collector 与 Jaeger all-in-one,执行kubectl port-forward -n otel-system svc/jaeger 16686:16686后即可在 Jaeger UI 中检索最近一次kubectl ate get actor my-counter-1 --trace产生的链路。

Python 客户端

与 Go 相同,Python 端也需要初始化 provider(因为 Python 只用于压测场景,这里使用基于概率的采样):

from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.sampling import TraceIdRatioBased from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.propagate import set_global_textmap, inject from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator def init_tracing(probability: float = 1.0): sampler = TraceIdRatioBased(probability) resource = Resource(attributes={ SERVICE_NAME: "my-locust-service" }) provider = TracerProvider(sampler=sampler, resource=resource) trace.set_tracer_provider(provider) set_global_textmap(TraceContextTextMapPropagator())
gRPC 客户端

gRPC 客户端只需实例化一个 Span,然后把注入好的 headers 作为 metadata 发送:

from opentelemetry import trace from opentelemetry.propagate import inject tracer = trace.get_tracer("my-service") def call_with_trace(stub, method, request): with tracer.start_as_current_span("operationIdentifier") as span: headers = {} inject(headers) metadata = list(headers.items()) response = stub.GetActor( ateapi_pb2.GetActorRequest(actor_ref=ateapi_pb2.ActorRef(atespace="default", name="my-actor")), metadata=metadata )
HTTP 客户端

HTTP 客户端同样先实例化 Span,再把注入的 headers 放进 HTTP 请求:

from opentelemetry import trace from opentelemetry.propagate import inject tracer = trace.get_tracer("my-service") def call_with_trace(stub, method, request): with tracer.start_as_current_span("operationIdentifier") as span: headers = {} inject(headers) response = requests.get( "http://example.com", headers=headers )

压测场景中的追踪特例:boomer 与 glutton

作为对照,两个压测组件展示了与生产服务相反的追踪策略:

  • glutton(cmd/benchmarking/glutton/main.go)默认使用ParentNeverSampling(),即parentbased_always_off,压测本身不产生 Span;
  • boomer不走环境变量,而是通过 dynconfig 的TraceProbability字段(见 internal/benchmarking/boomer/dynconfig/dynconfig.go)在运行时动态调整采样概率——这正对应文档"boomer 由运行时控制、忽略环境变量"的说明。其采样出的 Span 通过 internal/benchmarking/boomer/boomerutil/telemetry.go 的LogSampledTrace以结构化日志逐条输出(含 trace_id、duration_ms 等字段),供 fluentbit 等工具重新拼装 trace 流。

理解这一点对"禁用追踪"章节很关键:压测发生器会主动注入采样过的 trace context,如果服务端只配了parentbased_always_off,父级采样器会遵从发生器注入的已采样 context,导致你以为关了其实没关——必须使用always_off这种无条件关闭的采样器。

小结

Agent Substrate 的追踪体系可以概括为几条主线:serverboot.InitTracing统一装配 OTLP 导出器、资源与 TraceContext 传播器;ResolveTraceSampling在组件默认值之上应用标准环境变量、并对无效输入保持默认;控制面默认parentbased_traceidratio0.1、数据面默认 0.01 且与 EnvoyRandomSampling镜像同步;客户端侧遵循"不需要时不安装 provider"以保护服务端比例采样;压测场景通过always_off与发生器侧trace_probability双管齐下彻底关闭追踪。在此基础上,服务端按 gRPC/HTTP 接入中间件、客户端按语言(Go/Python)接入传播,即可获得贯穿 ateapi、router、ateom 全链路的端到端可观测性。

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

【免费下载链接】substrate

Agent Substrate: the core system

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

相关推荐

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 20:24:33

FY-4A卫星云图识别实战:HDF5数据处理与轻量U-Net云分类

简介:本资源是一份面向高校计算机、遥感或人工智能方向本科生的课程设计实践项目,聚焦卫星云层图像的理解与识别任务,提供从传统图像处理到深度学习建模的双路径解决方案。资源共148个文件,包含70个Python源码(含U-Net…

作者头像 李华
网站建设 2026/9/23 20:22:54

PDT团队KPI指标库搭建指南:从统一口径到落地避坑

简介:面向PDT(产品开发团队)绩效考核场景的KPI指标库文档,将财务、客户、内部业务三大维度的核心指标整理为可直接参考的评估体系。内容涵盖销售收入、毛利率、目标成本完成率、缺陷密度、问题解决率、NPD流程符合度、软件开发生产…

作者头像 李华
网站建设 2026/9/23 20:13:15

PyTorch人脸性别识别毕设:从数据划分到GUI部署的完整实战

简介:这份资源面向计算机相关专业的本科生与自学者,提供一套基于PyTorch实现人脸性别识别的完整课程设计或毕业设计参考方案。数据集涵盖白种人、黄种人、黑种人等多种族样本,并包含姿态、光照、年龄等干扰因素,需按40%、10%、50%…

作者头像 李华
网站建设 2026/9/23 20:12:36

为什么国内厂商卖的服务器配置低价格贵?

厂商典型配置价格(月)带宽/流量特点腾讯云轻量2核2G / 50-60GB SSD≈45-52元4-5Mbps,300-500GB流量国内访问优秀,稳定,备案方便阿里云轻量2核2G / 40-50GB SSD≈40-60元3-5Mbps生态最大,活动多百度智能云 B…

作者头像 李华