OpenTelemetry 2026深度实战:从架构原理到生产级可观测性的完整指南
"你无法优化你无法测量的事物。" 在分布式系统动辄横跨数十个微服务的今天,这句话的分量比任何时候都重。OpenTelemetry 已经成为云原生可观测性的事实标准,但你真的理解它的架构设计吗?你知道为什么 tail-based sampling 比 head-based sampling 在生产环境中更有价值吗?本文从原理到实战,给你一套完整的生产级可观测性落地方案。
一、背景:为什么可观测性在2026年依然是工程难题
1.1 从监控到可观测性:范式转移
传统监控的思路是"我知道什么会出问题,所以我提前配置告警"。但现代分布式系统的问题是:你不知道会出现什么问题。一个请求可能因为某个数据库连接池耗尽、某个中间件的偶发超时、或者某个依赖服务的尾延迟而在深夜告警。等你收到告警时,三组人对着各自的日志互相甩锅——90分钟后才定位到根因。
可观测性(Observability)的核心理念与此相反:系统应该主动暴露自己的内部状态,让你在不修改代码、不重启服务的情况下,回答任意关于系统行为的临时问题。
这个理念的落地需要三类数据的支撑,统称为"可观测性三大支柱":
- Traces(链路追踪):请求在分布式系统中的完整调用路径
- Metrics(指标):聚合后的数值型数据(QPS、延迟、错误率)
- Logs(日志):离散的事件记录
在 OpenTelemetry 出现之前,这三类数据由不同的工具负责:Jaeger 管 Trace、Prometheus 管 Metrics、Fluentd/Fluent Bit 管 Logs。每个工具都有自己的 SDK、自己的数据格式、自己的后端。工程团队要在多个系统之间维护多套埋点逻辑,痛苦不堪。
1.2 OpenTelemetry 的诞生与定位
OpenTelemetry(简称 OTel)由 OpenTracing 和 OpenCensus 两个项目在 2019 年合并而来,由 CNCF 托管。它不只是一个 SDK,而是一套完整的可观测性标准:
OpenTelemetry = 统一的 API/SDK + Collector + 语义约定(Semantic Conventions)
它的核心价值在于三点:
- 厂商中立:数据可以导出到任何后端(Jaeger、Zipkin、Grafana Tempo、Datadog 等)
- 多语言支持:Java、Go、Python、.NET、JavaScript、C++、Rust 等主流语言均有官方 SDK
- 三层分离:数据生成(SDK)→ 数据处理(Collector)→ 数据存储(Backend)完全解耦
1.3 2026年 OpenTelemetry 生态现状
截至 2026 年第一季度,OpenTelemetry 已成为 CNCF 毕业项目,在全球生产环境中采用率超过 67%。几个值得关注的趋势:
- OTLP 成为行业传输标准,连 AWS X-Ray、Azure Monitor 都陆续支持 OTLP 摄入
- eBPF + OTel 的融合:通过 eBPF 实现零代码侵入的自动埋点,正在改变 AI 推理服务的观测方式
- Log 和 Trace 的深度关联:OTel Logs 开始支持
trace_id、span_id字段的标准化注入 - Tail-based Sampling 从实验性进入生产稳定:配合 OpenTelemetry Collector 1.35+ 使用
二、核心概念:三大支柱的底层模型
2.1 Trace:链路追踪的数据模型
Trace(链路)描述的是一次请求在分布式系统中的完整生命周期。在 OTel 的数据模型中,Trace 由多个 Span 组成的有向无环图(DAG)。
Span 是链路追踪的基本单元,每个 Span 记录以下信息:
// Span 的核心字段(简化版)
type Span struct {
// 身份
TraceID []byte // 128位,全链路唯一标识
SpanID []byte // 64位,当前操作唯一标识
ParentID []byte // 父 SpanID(根 Span 无父)
// 业务信息
Name string // 操作名称,如 "http.request" 或 "db.query"
Kind SpanKind // CLIENT / SERVER / PRODUCER / CONSUMER / INTERNAL
// 时间
StartTime time.Time
EndTime time.Time
// 属性(键值对)
Attributes []AttributeKeyValue
// 事件(时间点上的快照)
Events []SpanEvent
// 状态
Status SpanStatus // OK / ERROR + 可选错误信息
}
SpanKind 的实际意义常常被忽视,但它对 Trace 可视化的正确性至关重要:
// SpanKind 定义了 Span 在调用链中的角色
SpanKindServer // 接收请求方(如 HTTP 服务器处理请求)
SpanKindClient // 发起请求方(如 HTTP 客户端发出请求)
SpanKindProducer // 消息发送方(如向 MQ 投递消息)
SpanKindConsumer // 消息消费方(如从 MQ 消费消息)
SpanKindInternal // 内部操作(如本地计算,不涉及网络)
理解 SpanKind 的关键在于:同一物理网络调用,在调用方记录为 Client Span,在被调用方记录为 Server Span。它们通过 trace_id 和 parent_span_id 串联成完整链路。
一个典型的分布式 Trace 的结构如下:
[前端Span: GET /checkout] (Server)
├──[库存服务Span] (Client → Server)
│ └──[DB查询Span]
├──[支付服务Span] (Client → Server)
│ └──[外部支付API Span]
└──[通知服务Span] (Producer → Consumer)
2.2 Metrics:指标的数据模型
OTel Metrics 借鉴了 Prometheus 的数据模型,支持四种指标类型:
1. Counter(计数器):记录只会增加的值,如请求总数。
from opentelemetry import metrics
meter = metrics.get_meter("checkout_service")
request_counter = meter.create_counter(
name="http.requests",
description="Total HTTP requests",
unit="1"
)
request_counter.add(1, {"http.method": "POST", "http.route": "/checkout"})
2. Histogram(直方图):记录值的分布,是分析延迟的不二之选,支持自定义 buckets。
histogram := meter.MustMakeHistogram(
"db.query.duration",
metric.WithUnit("ms"),
)
histogram.Record(ctx, queryDuration.Milliseconds(), metric.WithAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.operation", "SELECT"),
))
3. Observable Gauge(可观测量规):记录瞬时值,如当前活跃连接数,由应用在回调中报告。
4. UpDownCounter(上下计数器):记录可增可减的值,如队列深度。
2.3 Logs:日志的数据模型与语义约定
OTel Logs 的设计目标是让日志成为可观测性数据流的一部分。最关键的进步:OTel 支持结构化日志,并且可以通过 trace_id 和 span_id 将任意日志行与具体链路关联起来。
import "go.opentelemetry.io/otel/log"
logger :=otel.LoggerProvider().Logger("checkout-service")
// 自动注入 trace_id、span_id
logger.Info("Payment processed",
log.String("payment.id", paymentID),
log.Float64("amount", 99.99),
)
生成的日志结构大致如下:
{
"timestamp": "2026-05-19T08:15:30.123Z",
"severity": "INFO",
"message": "Payment processed",
"trace_id": "7b3e8c2d1a0f4e5b6c7d8e9f0a1b2c3d",
"span_id": "a1b2c3d4e5f60718",
"attributes": {
"payment.id": "pay_abc123",
"amount": 99.99
}
}
三、架构核心:OTel Collector 深度解析
3.1 为什么需要 Collector
很多团队一开始只在应用代码中埋点,数据直推后端。但这种方式有几个严重问题:
- 后端耦合:换后端需要改所有应用的 SDK 配置
- 性能影响:每个应用独立连接多个后端,增加网络开销
- 采样困难:无法跨应用做全局采样决策
- 格式不统一:不同语言 SDK 的数据格式可能存在差异
Collector(收集器)作为中间层解决了所有这些问题:
[应用1]──┐
[应用2]──┼──→ [OTel Collector]──→ [Jaeger]
[应用3]──┤ ├──→ [Prometheus]
... ──┘ ├──→ [Grafana Tempo]
└──→ [任意后端]
3.2 Collector 的 pipeline 架构
OTel Collector 的数据流由三个阶段组成,每个阶段都高度可插拔:
service:
pipelines:
traces:
receivers: [otlp, jaeger, zipkin]
processors: [batch, tail_sampling]
exporters: [jaeger, otlp/tempo]
metrics:
receivers: [otlp, prometheus]
processors: [batch]
exporters: [prometheus, otlp]
logs:
receivers: [otlp, fluentforward]
processors: [batch, resource/telemetry-log]
exporters: [otlp]
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
tail_sampling:
decision_wait: 10s
num_traces: 50000
policies:
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-traces-policy
type: latency
latency: {threshold_ms: 2000}
- name: probabilistic-policy
type: probabilistic
probabilistic: {sampling_percentage: 10}
exporters:
otlp:
endpoint: "http://tempo:4317"
tls:
insecure: true
3.3 Tail-based Sampling:为什么它改变了游戏规则
这是 OTel 2026 年最有价值的特性之一。Tail-based Sampling(尾部采样) 与传统的 Head-based Sampling(头部采样) 有着本质区别。
Head-based Sampling:请求进来时立即决定是否保留。在 Trace 刚开始时,采样器只能看到 Header 中的 trace_id,完全无法判断这条请求最终是否值得保留。结果是:正常的 99% 的请求被保留,有问题的异常请求反而被丢弃了——恰好你最需要的那些数据。
// Head-based Sampling 的问题:随机丢弃
func headSampler trace.Sampler {
return trace.SamplerProbability(0.01) // 1% 采样
// 99% 的慢请求、错误请求被丢弃
// 你永远无法分析那 1% 里有问题的请求
}
Tail-based Sampling:等待整个 Trace 完成后,根据 Trace 的全局特征(是否错误、延迟是否超过阈值、是否涉及特定服务)来决定是否保留。
processors:
tail_sampling:
decision_wait: 10s
policies:
# 策略1:保留所有错误 Trace(最关键!)
- name: errors-in-trace
type: status_code
status_code: {status_codes: [ERROR]}
# 策略2:保留超过 2 秒的慢 Trace
- name: slow-traces
type: latency
latency: {threshold_ms: 2000}
# 策略3:保留所有涉及 paymentservice 的 Trace
- name: business-critical-service
type: attributes
attributes:
- key: service.name
value: {regexp: ".*payment.*"}
# 策略4:其余的按 1% 概率保留
- name: probabilistic
type: probabilistic
probabilistic: {sampling_percentage: 1}
在日均 1000 万次请求的服务中:
| 采样策略 | 存储量/天 | 错误 Trace 保留率 | 慢 Trace 保留率 |
|---|---|---|---|
| 头部 1% 随机采样 | 100万条 | ~1% | ~1% |
| 尾部采样(上述策略) | 50万条 | 100% | 100% |
尾部采样将错误和慢请求的可见性提升了 100 倍,同时将存储量降低了一半。
3.4 Resource 与 Semantic Conventions
Resource(资源)描述的是产生数据的"实体",如服务名、运行时版本、部署环境。
// Go 中设置 Resource
resource, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("checkout-service"),
semconv.ServiceVersion("2.4.1"),
semconv.DeploymentEnvironment("production"),
attribute.String("host.name", os.Getenv("HOSTNAME")),
attribute.Int("replica.count", 3),
),
)
Semantic Conventions(语义约定) 定义了一套标准化的属性命名规范:
| 领域 | 属性键 | 示例值 |
|---|---|---|
| HTTP | http.method | GET, POST |
| HTTP | http.route | /api/users/{id} |
| HTTP | http.status_code | 200, 404, 500 |
| 数据库 | db.system | postgresql, mysql, redis |
| 数据库 | db.operation | SELECT, INSERT |
| RPC | rpc.method | GetUser |
| RPC | rpc.system | grpc, thrift |
使用语义约定的意义:不同团队、不同语言、不同服务,都用同一套属性名。这在微服务规模超过 50 个时尤为重要——没有统一语义,你根本无法跨服务聚合数据。
四、自动埋点:零侵入观测的工程实践
4.1 自动埋点的原理
"零代码侵入"是 OTel 相对于传统 APM 方案的最大优势之一。
方式一:SDK 自动拦截(Auto-Instrumentation)
Java 中使用 -javaagent 启动参数,无需修改任何代码:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=checkout-service \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://collector:4317 \
-jar checkout-service.jar
Python 中使用 opentelemetry-instrument 命令行工具:
opentelemetry-instrument \
--service-name checkout-service \
--exporter-otlp-endpoint http://collector:4317 \
python checkout.py
方式二:eBPF 自动埋点(2026年重点)
对于 AI 推理服务、Serverless 函数等难以注入 agent 的场景,eBPF 可以在内核层面拦截系统调用和内核函数,自动提取 trace 信息。
otel-ebpf --library=libssl --function=SSL_do_handshake \
--service-name ai-inference-service
4.2 手动埋点的正确姿势
以下是 Go 中的完整示例:
package main
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
ctx := context.Background()
// 1. 配置 OTLP 导出器
traceExporter, _ := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("collector.otel:4317"),
otlptracegrpc.WithTransportCredentials(insecure.NewCredentials()),
)
// 2. 配置资源信息
res, _ := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("payment-service"),
semconv.ServiceVersion("1.0.0"),
attribute.String("deployment.region", "ap-shanghai"),
),
)
// 3. 配置 Propagator(跨服务上下文传播)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
// 4. 创建 tracer provider
tracerProvider := oteltrace.NewTracerProvider(
oteltrace.WithBatcher(traceExporter),
oteltrace.WithResource(res),
oteltrace.WithSampler(oteltrace.ParentBased(
oteltrace.TraceIDRatioBased(0.1),
)),
)
defer tracerProvider.Shutdown(ctx)
otel.SetTracerProvider(tracerProvider)
tracer := otel.Tracer("payment-service")
// 5. 在业务逻辑中创建 Span
processPayment(ctx, tracer, "pay_abc123", 99.99)
}
func processPayment(ctx context.Context, tracer trace.Tracer, paymentID string, amount float64) {
ctx, span := tracer.Start(ctx, "payment.process",
trace.WithAttributes(
attribute.String("payment.id", paymentID),
attribute.Float64("payment.amount", amount),
),
trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
start := time.Now()
// 调用支付网关(Client Span)
gatewayCtx, gatewaySpan := tracer.Start(ctx, "payment.gateway",
trace.WithAttributes(
attribute.String("gateway.provider", "stripe"),
),
trace.WithSpanKind(trace.SpanKindClient),
)
result, err := callStripeGateway(gatewayCtx, paymentID, amount)
gatewaySpan.SetAttributes(attribute.Bool("payment.success", err == nil))
gatewaySpan.End()
if err != nil {
span.SetStatus(oteltrace.StatusError, err.Error())
span.RecordError(err)
return
}
// 更新库存
invCtx, invSpan := tracer.Start(ctx, "inventory.reserve",
trace.WithSpanKind(trace.SpanKindClient),
)
reserveInventory(invCtx, paymentID)
invSpan.End()
span.SetAttributes(
attribute.Float64("payment.duration_ms", float64(time.Since(start).Milliseconds())),
attribute.String("payment.status", "completed"),
)
}
4.3 跨服务上下文传播的坑
Propagator(传播器)是 OTel 中最容易出问题的组件。它负责在 HTTP/gRPC headers 中注入和提取 trace context。如果传播器配置不一致,链路就会在服务边界处断开。
HTTP 场景:确保所有服务使用相同的 Propagator(推荐 W3C TraceContext)
// 服务A:注入
req, _ := http.NewRequest("POST", "http://inventory-service/api/reserve", body)
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
client.Do(req)
// 服务B:提取
ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header))
gRPC 场景:使用 otelgrpc interceptor 自动处理传播:
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
)
// 自动:提取 context → 创建 Server Span → 注入响应 metadata
五、生产级部署:Collector 高可用配置
5.1 Collector 部署模式对比
OTel Collector 有三种部署模式:
Agent 模式(每机部署):每个 Kubernetes Pod 中运行一个 OTel Collector sidecar。优点:减少应用出口连接数,支持本地批处理和压缩。
Gateway 模式(集群级部署):一组专门的 Gateway Collector 实例(通常 2-3 个副本)。优点:可以跨 Pod 看到完整的 Trace 数据(对 Tail Sampling 至关重要)。
混合模式(推荐生产方案):
[应用] → [Agent Collector (每节点)] → [Gateway Collector (集群级)] → [后端存储]
↓ ↓
基础批处理、压缩 尾部采样、全局去重、多后端转发
5.2 Kubernetes 部署配置
# Agent Collector (DaemonSet)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otelcol-agent
spec:
selector:
matchLabels:
app: otelcol-agent
template:
spec:
hostNetwork: true
containers:
- name: otelcol
image: otel/opentelemetry-collector-contrib:0.109.0
args: ["--config=/etc/otelcol-agent/config.yaml"]
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
ports:
- containerPort: 4317 # OTLP gRPC
- containerPort: 4318 # OTLP HTTP
- containerPort: 8888 # Collector 自身 metrics
Gateway Collector(Deployment):2 副本,推荐 CPU 2000m、内存 2Gi。
5.3 OTLP 负载均衡
Gateway 模式横向扩展的关键是 LoadBalancer exporter:
exporters:
loadbalancer:
protocol: otlp
endpoint: ${env:GATEWAY_ENDPOINTS}
balancer: round_robin
在 Kubernetes 中,通过 DNS SRV 记录自动发现所有 Gateway Pod:
# DNS: otelcol-gateway.default.svc.cluster.local
# 自动解析为所有 Pod 的 IP
六、性能优化:生产中的实际考量
6.1 SDK 性能开销分析
根据 CNCF 2025 年的基准测试数据(Java SDK 1.35.0):
| 操作 | 延迟开销 | 吞吐量影响 |
|---|---|---|
| 无埋点基线 | 0ms | 100% |
| 自动埋点(HTTP/DB) | +0.3ms/p95 | -3% |
| 手动 Span 创建(简单) | +0.05ms | -1% |
| 手动 Span + 10 属性 | +0.15ms | -2% |
| 启用 1000 属性/事件 | +1.2ms | -8% |
结论:默认配置下,OTel SDK 的性能影响在 3% 以内,是可以接受的。真正的性能问题通常来自:
- 导出(Export)阻塞:Span.End() 同步等待导出
- Span 数量爆炸:未配置采样时,高并发系统每秒生成数百万 Span
- 属性过多:每个 Span 携带大量冗余属性
解决方案:使用 Batch Span Processor + 合理采样
tp := oteltrace.NewTracerProvider(
oteltrace.WithBatcher(traceExporter,
WithMaxExportBatchSize(512),
WithBatchTimeout(5*time.Second),
WithMaxQueueSize(2048),
),
)
// Span.End() 立即返回,不阻塞业务线程
6.2 Lazy Span:按需计算的魔法
对于不想每次都创建重型数据的场景,OTel 提供了 Lazy Span 概念:
// 传统方式:无论是否被采样都会执行
span := tracer.StartSpan("process")
defer span.End()
expensiveMetadata := computeExpensiveMetadata() // 浪费!
span.SetAttributes(attribute.String("metadata", expensiveMetadata))
// Lazy 方式:仅在被采样时执行
span.SetAttributes(
attribute.String("metadata", lazyAttr(func() string {
return computeExpensiveMetadata() // 只有被保留时才执行
})),
)
6.3 Span 数量的数学
假设系统 QPS:10,000,每个请求 8 个 Span,Span 平均 1KB,目标存储 10GB/天:
Span/天 = 10,000 × 8 × 86400 = 6.91 亿个 Span
总大小/天 = 6.91亿 × 1KB ≈ 6910 GB
采样率 = 10 / 6910 ≈ 0.14%
加上 Tail Sampling:
- 100% 保留错误 Trace(0.1% 请求)→ 69万 Span
- 100% 保留 >1s 慢请求(1% 请求)→ 690万 Span
- 其余按 0.14% 采样 → 约 10万 Span
总计:约 770万 Span/天 ≈ 7.7GB ✓
这就是尾部采样的核心价值:用极低的采样率保留最有价值的异常数据。
七、与 AI/LLM 系统的集成
7.1 LLM 调用追踪的独特挑战
AI 推理服务的可观测性有独特挑战:LLM 调用延迟高(几秒到几十秒)、Token 计费敏感、Prompt/Response 可能含敏感信息。
OTel 2026 新增了 gen_ai.* 语义约定:
span.SetAttributes(
attribute.String("gen_ai.system", "openai"),
attribute.String("gen_ai.operation.name", "chat"),
attribute.Int("gen_ai.prompt.token_count", promptTokens),
attribute.Int("gen_ai.completion.token_count", completionTokens),
attribute.Float64("gen_ai.usage.total_tokens", float64(promptTokens+completionTokens)),
attribute.Float64("gen_ai.latency.first_token_ms", firstTokenLatency.Milliseconds()),
)
7.2 PII 过滤
OTel Collector 提供 filter processor 用于在数据离开应用前过滤敏感信息:
processors:
filter/genai:
error_mode: ignore
logs:
exclude:
match_type: regexp
record_attributes:
- key: message
value: "(?i)\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b"
生产中推荐使用专门的 PII 检测库(如 Presidio)作为 OTel 的 Custom Processor。
八、总结与展望
8.1 核心实践清单
✅ 1. 在 Kubernetes 中部署 OTel Agent(DaemonSet)+ Gateway(Deployment)
✅ 2. 应用 SDK 配置 Resource(服务名、版本、环境)
✅ 3. 使用 W3C TraceContext Propagator,统一 HTTP/gRPC 上下文传播
✅ 4. 开启 Tail-based Sampling,保留所有错误 Trace + 慢请求 Trace
✅ 5. 配置 Batch Span Processor,避免 Span 导出阻塞业务线程
✅ 6. 在日志中注入 trace_id/span_id,实现端到端关联
✅ 7. 对 LLM 调用使用 gen_ai.* 语义约定
✅ 8. 配置合理的 Attribute 命名(使用 Semantic Conventions)
8.2 未来展望
OTel 的下一步有几个值得关注的方向:
- eBPF 自动埋点的成熟化:预计 2026 年底将进入 GA,AI 推理服务将迎来零侵入观测的成熟方案
- Profiles(性能剖析)集成:将 CPU/Memory Profile 数据与 Trace 关联,实现"点击 Trace 直接看火焰图"
- Native Log-Traces Merge:直接在内核 OTel 数据层实现 Log 和 Trace 的自动合并
- LLM Observability 标准化:gen_ai.* 语义约定正在从草案走向正式规范
可观测性不是一个可以"完成"的项目,而是一个持续演进的工程能力。OpenTelemetry 给了我们一个厂商中立、功能完整的起点,剩下的,就是把它真正落地到生产环境中。
下一次深夜告警响起时,希望你的团队不再是互相对着日志甩锅,而是淡定地点开 Trace,找到那根链路的瓶颈所在。 这,才是可观测性应该有的样子。
本文代码示例基于 OpenTelemetry Go SDK 1.35+ / Python OTel 1.25+ / Java Agent 1.35+。