- 人工智能
- AI Agent
- Agent 沙箱
- 云原生
- 容器运行时
- 零信任
【免费下载链接】substrate
Agent Substrate: the core system
本指南聚焦 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会完成以下装配:
- Resource:通过
newResource写入service.name(来自TracingOptions.ServiceName)与service.instance.id(进程内生成的 UUID),并调用resource.WithFromEnv()让OTEL_*环境变量可以覆盖默认值; - OTLP 导出器:使用
otlptracegrpc创建,默认走WithInsecure()——substrate 明确不验证 collector 的 TLS 证书,因为 GKE 托管 traces 不支持该校验(源码注释原文说明); - Sampler:使用
opts.Sampling.Sampler()作为 SDK 采样器(关于如何构造,见下文采样一节); - Batcher:
sdktrace.WithBatcher(exporter)让 Span 批量导出; - 全局注册:
otel.SetTracerProvider(tp)与otel.SetTextMapPropagator(propagation.TraceContext{}),即只注册 W3C TraceContext 传播器; - 错误处理:
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) | ateapi | parentbased_traceidratio0.1 | 否 |
| atecontroller(cmd/atecontroller/main.go) | 运行时 serviceName | parentbased_traceidratio0.1 | 否 |
| atelet(cmd/atelet/main.go) | atelet | parentbased_traceidratio0.1 | 否 |
| ateom-gvisor(cmd/ateom-gvisor/main.go) | 运行时 serviceName | parentbased_traceidratio0.1 | 是(ExporterConn + RelayCapable) |
| ateom-microvm(cmd/ateom-microvm/main.go) | 运行时 serviceName | parentbased_traceidratio0.1 | 是(ExporterConn + RelayCapable) |
| atenet router(cmd/atenet/internal/router/router.go) | extproc ServiceName | parentbased_traceidratio0.01 | 否 |
| glutton(cmd/benchmarking/glutton/main.go) | glutton | parentbased_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-microvm | parentbased_traceidratio0.1 |
| atenet router(数据面根) | parentbased_traceidratio0.01,并镜像到 Envoy 的RandomSampling |
| glutton(benchmarking) | parentbased_always_off |
| boomer(benchmarking) | 由 dynconfig 运行时控制,忽略环境变量 |
这些默认值全部基于ParentBased(父级采样),因此:已经带采样标记到达的请求,在每一跳都保持采样;显式标记为未采样的请求也一路保持未采样。只有"无父级"(parentless)的请求才受比例影响,由该链路在哪个组件上生根来决定(internal/serverboot/sampling.go 中的ParentRatioSampling、ParentNeverSampling实现)。
解析行为上有两个关键点(对应 internal/serverboot/sampling.go 的resolveTraceSampling):
- 无效输入保持默认并告警:采样器名无效、
OTEL_TRACES_SAMPLER_ARG缺失或比例无法解析/超出[0, 1]区间时,保留组件默认值并记录一条 warning。这与 OTel SDK 自身的处理有意的不同——SDK 在无效输入时回退到 100% 采样、缺 arg 时把比例读作 1.0,对生产环境过于激进; - 空值视为未设置:与 SDK 不同,
OTEL_TRACES_SAMPLER被设置为空字符串时按未设置处理,兼容模板渲染出空环境变量的场景。
支持的采样器名:always_on、always_off、traceidratio、parentbased_always_on、parentbased_always_off、parentbased_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
相关推荐
res-downloader:视频号、抖音、m3u8 资源下载零门槛完整指南
res downloader:视频号、抖音、m3u8 资源下载零门槛完整指南 res downloader 是一款免费的跨平台网络资源下载器,通过本地代理自动嗅
桌面应用网络音视频Synapse 分布式追踪实践:基于 OpenTracing 与 Jaeger 的端到端链路观测指南
Synapse 分布式追踪实践:基于 OpenTracing 与 Jaeger 的端到端链路观测指南 导读 Synapse(Matrix 协议的服务端实现,使用
后端即时通讯基于 mcp-agent 的 SSE 客户端—服务端分布式链路追踪实战指南
基于 mcp agent 的 SSE 客户端—服务端分布式链路追踪实战指南 导读 本指南围绕开源仓库 mcp agent https://link.gitcod
人工智能AI AgentAgent 框架MCP ClientsAgent 工作流
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考