OpenTelemetry核心概念

一、Trace 与 Span 模型

1.1 Span 是什么

Span 是链路的基本单元,表示一段有起止时间的操作。一次 HTTP 请求的链路:

Go 复制代码
Trace (trace_id: 0af765...)
└─ span: GET /checkout            [SERVER]   1200ms  (gateway)
   ├─ span: AuthService.verify    [CLIENT]    50ms
   │  └─ span: verify             [SERVER]   45ms   (auth 服务内部)
   ├─ span: OrderService.create   [CLIENT]   900ms
   │  ├─ span: redis.get          [CLIENT]     5ms
   │  └─ span: mysql INSERT       [CLIENT]   800ms  ← 慢在这里
   └─ span: notify                [PRODUCER]  10ms   (发 Kafka 消息)

同一个 trace 内所有 Span 共享 trace_id ;父子关系靠 parent_span_id 串起来(存储上是时间线上的一组 Span,树形由 parent 关系推导)。

1.2 Span 的组成

部分 说明 示例
trace_id 16 字节,整条链路唯一 0af7651916cd43dd8448eb211c80319c
span_id 8 字节,单个 Span 唯一 b7ad6b7169203331
parent_span_id 父 Span;根 Span 此字段为空
name 操作名,语义约定建议低基数(不要拼参数) GET /users/{id} 而非 GET /users/42
kind SpanKind,见下表
start/end 起止时间戳(纳秒)
attributes 键值属性,描述本次操作 http.request.method=GET
events 时间点事件(无时长) exception 事件记录报错堆栈
links 指向其他 Span 的链接(批处理、异步扇出场景)
status Unset / Ok / Error

1.3 SpanKind 五种取值

Kind 含义 典型场景
INTERNAL 进程内部操作,不跨边界 一个内部函数调用
SERVER 服务端处理入站请求 HTTP handler、gRPC server
CLIENT 客户端发起出站请求 调下游 HTTP/gRPC
PRODUCER 异步生产消息 发 Kafka/AMQP 消息
CONSUMER 异步消费消息 消费者收到消息处理

CLIENT/SERVER 成对出现时,两个 Span 共享 trace_id------CLIENT 端在请求头里注入 traceparent,SERVER 端提取,链路就接上了(见下节)。

1.4 属性、事件、状态的使用姿势

Go 复制代码
// 属性:描述"这次操作"的可检索维度(低基数!)
span.SetAttributes(attribute.String("http.route", "/users/{id}"))

// 事件:时间点记录,常用于异常
span.AddEvent("cache.miss", trace.WithAttributes(attribute.String("key", k))
// 记录 error 是标准做法(会生成 exception 事件)
recordError(span, err)

// 状态:标记整个 Span 失败
span.SetStatus(codes.Error, "db query failed")

基数(cardinality)警告 :属性的取值组合会成为后端的索引维度。user.id、完整 URL 这种高基数值放 attributes 会拖垮后端------放日志或 events,attributes 只放 http.route 这类有限集合。

二、Context 传播

2.1 进程内:Go 的 context.Context

OTel 没有发明新的上下文,直接复用 context.Context:

Go 复制代码
ctx, span := tracer.Start(ctx, "child-op")   // 从 ctx 取出当前 Span 作为 parent
defer span.End()

doWork(ctx)  // 必须把 ctx 传下去,链路才不断

新手最常见的断链原因:函数里没接住 ctx 又起了新 ctx ,导致下游拿不到 parent。规则:埋点函数永远把 ctx 作为第一个参数传递。

2.2 跨服务:Propagator 与 W3C TraceContext

出站请求时把 trace 状态注入 HTTP 头,入站时提取:

Go 复制代码
// 客户端:注入
propagator := propagation.TraceContext{}  // otel.GetTextMapPropagator() 全局默认也是它
propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))
// → 请求头里出现: traceparent: 00-0af765...-b7ad6b...-01

// 服务端:提取
ctx = propagator.Extract(ctx, propagation.HeaderCarrier(r.Header))
ctx, serverSpan := tracer.Start(ctx, "GET /api", trace.WithSpanKind(trace.SpanKindServer))

traceparent 头格式(W3C TraceContext 标准):

Go 复制代码
00 - 0af7651916cd43dd8448eb211c80319c - b7ad6b7169203331 - 01
版本   trace_id (32位hex)               parent_span_id (16位hex)  采样标志位

最后两位 01 表示 sampled=true。跨服务采样一致性就靠它 :下游看到 01 才记录,保证一条链路要么完整、要么整体不采(这是头采的天然特性)。

2.3 Baggage

Baggage 在 W3C 标准里与 traceparent 并行(baggage 头),用于跨服务传播业务键值 ,例如把 user.id=42 从网关带到所有下游,日志里统一打出来。注意:Baggage 会随每个请求到处飞,不要塞敏感/大字段。

三、Resource 与语义约定

Resource 描述"数据从哪个实体产生",同一条链路所有 Span 共享:

Go 复制代码
res := resource.NewWithAttributes(
    semconv.SchemaURL,
    semconv.ServiceName("order-service"),     // 最重要,必填级属性
    semconv.ServiceVersion("v1.2.3"),
    semconv.DeploymentEnvironment("prod"),
)
  • resource.Default() 会自动带上 SDK 名/版本、进程 PID、host 名,并读取环境变量:

    • OTEL_SERVICE_NAME=my-svc → service.name

    • OTEL_RESOURCE_ATTRIBUTES=k8s.cluster=prod,team=trade → 任意自定义属性

  • 生产环境通常再叠加 resourcedetection(Collector 端)自动补齐 K8s/云厂商元数据。

语义约定(Semantic Conventions) 是属性命名的"词典",例如 HTTP 服务端 Span 必有的:http.request.method、url.path、http.response.status_code、url.scheme、network.protocol.version、server.address/port。Go 里在 go.opentelemetry.io/otel/semconv/v1.x.x 包(版本随约定演进,用仓库当前最新)。遵守它的意义:换语言、换后端,属性名一致,面板和查询可以复用。

四、采样

全量上报成本高(内存、网络、存储),采样决定"哪些 trace 留下"。

4.1 头部采样(Head-Based)------ SDK 端,请求开始时决定

在链路第一个 Span 创建前就决策,整条链路一致采或不采:

采样器 行为
AlwaysOn 全采(开发环境)
AlwaysOff 全不采
TraceIDRatioBased(0.1) 按 trace_id 哈希确定性采样 10%(同一 trace 结果稳定)
ParentBased(子采样器) 默认值。父已采样→跟着采;父未采样→不采;根 trace→用子采样器
Go 复制代码
// 生产常见配置:根请求采 10%,但跟随上游决策
sdktrace.WithSampler(
    sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1)),
)

ParentBased 是保证"链路完整"的关键:只要入口采了,下游全采;入口没采,下游一个都不采,不会出现半截链路。

4.2 尾部采样(Tail-Based)------ Collector 端,请求结束时决定

链路走完再判断:错误链路 100% 保留、慢请求(>1s)保留、其余采 1%。这是 Collector tail_sampling processor 的能力(见 05 章)。代价是必须在 Collector 缓存完整链路,资源开销大,一般部署在 Gateway 层。

4.3 怎么选

  • 开发/测试:AlwaysOn

  • 生产常规:SDK 端 ParentBased(TraceIDRatioBased(p)),简单稳定

  • 错误链路必须全留 + 想按响应时间采样:SDK 全采 + Collector 尾部采样(成本转嫁给 Collector,需评估容量)

五、Metrics 深入

5.1 指标工具(Instrument)类型

类型 语义 Go API
Counter 只增不减的计数 meter.Int64Counter("http.requests")
Gauge 瞬时值,可升可降 meter.Float64Gauge / Int64Gauge
Histogram 值分布(桶统计),算 P99 用它 meter.Float64Histogram("http.server.duration")
Exponential Histogram 指数桶直方图,桶数少精度高 meter.Float64ExponentialHistogram
Summary(仅兼容) 分位数,OTel 不推荐新增 ---
Observable* 回调式:注册函数定期返回当前值 meter.Int64ObservableGauge
Go 复制代码
meter := otel.Meter("myapp")
duration, _ := meter.Float64Histogram(
    "http.server.duration",
    metric.WithUnit("s"),
    metric.WithDescription("服务端处理耗时分布"),
)
duration.Record(ctx, 0.042, metric.WithAttributes(
    attribute.String("http.route", "/checkout"),
    attribute.Int("http.response.status_code", 200),
))

5.2 Temporality(时间性)

  • Cumulative(累积):从进程启动累计到现在的值;Prometheus 生态默认,重启即清零;

  • Delta(增量):每个时间区间的增量;OTLP 默认推荐,适合流式聚合。

用 Prometheus 抓取时 SDK 的 prometheus exporter 自动用 cumulative;用 OTLP 推送到支持 delta 的后端时可配置。初学阶段知道有这个概念即可。

5.3 指标 vs 手动打点

指标是预聚合 的:SDK 在进程内就把上万次请求聚合成几十个时间序列再发出去,成本远低于每请求一条 Span。所以:不是所有信号都要上 trace ------高频健康度用 metrics,排查问题用 trace,两者属性都用 http.route 等相同维度命名,才能在面板间互相下钻。

六、Logs 与 trace 关联

OTel 日志的核心价值:每条日志自动带上 trace_id / span_id,从链路一键跳到日志。

两种接入方式:

方式一:桥接现有日志库(推荐起步)

把 zap/logrus/slog 桥接到 OTel(自动注入 trace 信息、经 OTLP 发送):

Go 复制代码
// contrib 仓库的 slog 桥
import "go.opentelemetry.io/contrib/bridges/otelslog"

logger := slog.New(otelslog.NewHandler("my-app"))

方式二:日志里手动打 trace_id(最小侵入)

Go 复制代码
type traceHandler struct{ slog.Handler }

func (h traceHandler) Handle(ctx context.Context, r slog.Record) error {
    if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
        r.AddAttrs(
            slog.String("trace_id", sc.TraceID().String()),
            slog.String("span_id", sc.SpanID().String()),
        )
    }
    return h.Handler.Handle(ctx, r)
}

关键前提 :打日志时必须传 ctx(logger.InfoContext(ctx, ...)),否则拿不到当前 Span。这也是生产上"日志和链路对不上"的头号原因。

七、Provider / Processor / Exporter 关系图

Go 复制代码
TracerProvider(全局一个,持有 Resource、Sampler、SpanProcessor 列表)
 └─ Tracer(按 instrumentation scope 名区分)
     └─ Span
         │ End() 时触发
         ▼
SpanProcessor 链(可多个)
 ├─ BatchSpanProcessor(生产标配:攒批、定时、限队列)
 └─ SimpleSpanProcessor(同步立即导出,调试用)
     │
     ▼
Exporter(OTLP gRPC/HTTP、stdout、控制台...)
     │
     ▼
Collector 或后端

Metrics 同构:MeterProvider → Meter → Instrument → Reader(批量导出或 Prometheus 抓取)。

生命周期铁律 :进程退出前必须调用 tp.Shutdown(ctx),否则批量队列里没发完的 Span 会丢------这是新手丢数据的常见原因。

相关推荐
IT_陈寒1 小时前
Java项目实战经验分享:一次由JPA懒加载引发的生产事故
前端·人工智能·后端
紧跟先前的步伐1 小时前
Go AI Agent 实战 20|接入 DeepSeek 原生 Tool Calling
人工智能·ai·golang·1024程序员节
环境栈笔记1 小时前
浏览器多开同步怎么做?先分清设置、登录态和窗口操作
前端·人工智能·后端·自动化
deviant-ART1 小时前
java注解简写规则
java·后端
开开心心就好1 小时前
电脑语音实时转文字工具,会议记录很省事
运维·服务器·前端·c#·ocr·perl·1024程序员节
波力海苔夹心脆6751 小时前
C# 固高 GTS 运动控制卡入门:伺服上下电、Jog 点动与二维插补(WinForms 实战)
服务器·开发语言·c#·运动控制·伺服电机·winforms·固高
会编程的吕洞宾1 小时前
Spring Boot 镜像瘦身实战:从 450MB 瘦到 180MB
java·后端·spring
紧跟先前的步伐1 小时前
# Go AI Agent 实战 21|多工具选择、参数校验与工具执行安全
人工智能·ai·golang