找回密码
立即注册
搜索
发回帖 发新帖

6491

积分

0

好友

796

主题
发表于 昨天 23:02 | 查看: 4| 回复: 0

一、仓库结构与模块划分

opentelemetry-go/
├── otel.go / version.go        # 全局 API 入口(Tracer/Meter/版本)
├── trace/                      # 【API 层】链路接口定义
│   ├── trace.go                #   Tracer/TracerProvider/Span 接口 + SpanContext
│   ├── noop.go                 #   空实现(未注入 SDK 时用)
│   ├── nonrecording.go         #   不记录的 Span(采样 Drop 时返回它)
│   └── config.go               #   SpanConfig(WithAttributes 等选项)
├── metric/                     # 【API 层】指标接口
├── logs/ (log/)                # 【API 层】日志接口(较新)
├── baggage/  propagation/      # Baggage 与传播器(W3C 实现)
├── codes/  attribute/  semconv/# 枚举、属性类型、语义约定常量
├── sdk/
│   ├── trace/                  # 【SDK 层】★ 核心,本章主角
│   ├── metric/                 # 【SDK 层】指标实现(聚合、Reader、视图)
│   ├── log/                    # 【SDK 层】日志实现
│   └── resource/               # Resource 检测与合并
├── exporters/
│   ├── otlp/otlptrace/         # OTLP 链路导出(gRPC/HTTP 两种 client)
│   ├── stdout/                 # 打到控制台(调试用)
│   └── ...
├── internal/global/            # ★ 全局实例与"委托"机制(理解 API/SDK 解耦的关键)
└── example/                    # 官方示例,读源码起步点

这是 Go 多模块(multi-module)仓库,核心 import 路径如下:

模块路径 内容
go.opentelemetry.io/otel API + 全局实例(业务代码只应依赖这个)
go.opentelemetry.io/otel/sdk/trace 链路 SDK 实现
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc OTLP gRPC 导出器
go.opentelemetry.io/contrib/...(另一仓库) 框架自动埋点、桥接器

二、分层设计:API / 全局实例 / SDK

2.1 为什么分三层

业务代码和 otelgin 等埋点库只依赖 trace.TracerProvider 接口,真正的 SDK 实现由 main 注入。如果没有人注入,就使用 noop 空实现。埋点代码因此永远不会 panic,禁用时也零成本,这正是 OTel 的设计基石。

2.2 全局委托机制(internal/global)

问题在于:otel.Tracer("x") 被调用时,otel.SetTracerProvider(tp) 可能还未执行,因为包初始化顺序不可控。解法就是“委托(delegate)”三步:

  1. otel.Tracer(...) 返回的是 internal/global 里的占位 tracer;
  2. otel.SetTracerProvider(tp) 被调用后,全局状态切换,并对已创建的占位 tracer 执行 SetDelegate,级联下发给它们创建过的 Span;
  3. 之后所有操作转发给真正的 SDK tracer。

关键源文件:

  • internal/global/state.go —— 全局单例与 SetXxx 系列函数
  • internal/global/tracer.go —— 占位 tracer 与委托转发(tracer.Ids()、delegate 方法)
  • internal/global/tracer_provider.go、meter.go、handler.go(错误处理器 otel.Handle)

读代码技巧:在 IDE 里对 otel.Tracer 按 F12,先跳到 otel.go 的全局函数,再跟进 internal/global,就能看到整条委托链。

三、Span 创建与生命周期(sdk/trace/span.go)

3.1 tracer.Start() 做了什么

sdk/trace/tracer.go 的 Start(ctx, name, opts...) 流程如下:

1. 解析 SpanConfig(kind、属性、links、时间戳)
2. 生成新的 span id + 继承/生成 trace id
   └─ id_generator.go(默认随机;可自定义雪花等单调 id)
3. 执行采样决策 sampler.ShouldSample(SamplingParams)
   ├─ Decision == Drop
   │    → 返回 nonRecordingSpan(接口满足、啥也不记,业务代码无感)
   └─ Decision == RecordAndSample → 继续创建真实 span
4. 组装 span:属性、links、parent(来自 ctx 里的 SpanContext)
5. 构造新 ctx = context.WithValue(ctx, currentSpanKey, span)
6. 逐个调用 spanProcessor.OnStart(span)   ← 注意:OnStart 是同步的
7. 返回 (ctx, span)

细节亮点:

  • Drop 时不产生任何内存开销。nonRecordingSpan 是接口空实现,这也是“采样率成为性能开关”的原理。
  • SamplingParams 里携带 ParentContext,父已采或未采的信息就是 ParentBased 采样器的输入。
  • trace id 生成规则:如果 ctx 里已有 SpanContext,也就是有父 Span,则 trace id 继承父,span id 新生成。

3.2 span.End() 做了什么

sdk/trace/span.go:

1. ended 原子标记(cas),防止重复 End → 重复 End 是 no-op
2. 若 SetStatus 为 Error 且没有 exception 事件,部分版本会补记
3. 截断超限数据:属性/事件/links 按 SpanLimits 裁剪(超出计数进 dropped_*_count)
4. 逐个调用 spanProcessor.OnEnd(span) ← BatchSpanProcessor 从这里接手
5. End 之后 SetAttributes 等方法全部变 no-op

内部同步方面,span 结构体用一把 sync.Mutex 保护属性切片,atomic.Uint32 表达状态。Span 是热路径对象,源码里大量出现“先检查原子状态再进锁”的写法,非常值得学习。

3.3 属性的存储:evictedqueue

sdk/trace/evictedqueue.go 是一个固定容量的环形队列,超出容量时丢掉最老的数据并累加 dropped 计数。Span 的 attributes / events / links 都用它承载,这就是 SpanLimits 的底层实现。

四、BatchSpanProcessor 精读(重点)

文件:sdk/trace/batch_span_processor.go。这是生产环境最重要的组件,也是面试高频。

4.1 设计:生产者(业务 goroutine)与消费者(后台 goroutine)解耦

业务 goroutine                          后台 goroutine
─────────────                          ──────────────
span.End()
  → bsp.OnEnd(span)
      → queue 满了吗?
        ├─ 满:dropped++(丢弃,默认非阻塞)
        └─ 未满:入队(batch 累积)
                                          loop:
                                            每 BatchTimeout(5s) 或攒够
                                            MaxExportBatchSize(512) →
                                            exportSpans(batch)
                                              → exporter.ExportSpans(ctx, batch)
                                              → 失败:按重试策略重试/丢弃计数
Shutdown() → 停止 ticker、关闭队列、drain 剩余、最后一次 export

4.2 默认参数(规范定义,源码常量)

参数 默认值 说明
MaxQueueSize 2048 队列上限,满了开始丢
BatchTimeout 5s 定时导出间隔
MaxExportBatchSize 512 触发导出的批量大小
ExportTimeout 30s 单次导出超时

对应环境变量:OTEL_BSP_MAX_QUEUE_SIZE、OTEL_BSP_SCHEDULE_DELAY、OTEL_BSP_MAX_EXPORT_BATCH_SIZE、OTEL_BSP_EXPORT_TIMEOUT,可以查看 sdk/trace/env.go。

4.3 定制示例

sdktrace.WithBatcher(exp,
    sdktrace.WithMaxQueueSize(10000),
    sdktrace.WithBatchTimeout(2*time.Second),
    sdktrace.WithMaxExportBatchSize(1000),
)

4.4 读源码时关注的 4 个问题

  1. 丢数据语义:队列满时直接丢弃,默认只把 dropped 计数打到日志或指标。因此监控 OTel 自身指标很重要,尤其是队列使用率、dropped spans。
  2. 关停正确性:Shutdown 如何保证进程收到 SIGTERM 时不丢队列里的 Span,重点看 drain 逻辑。
  3. 与 SimpleSpanProcessor 的选择:simple_span_processor.go 在 OnEnd 里同步调用 exporter,仅适合调试,生产环境同步导出会阻塞业务请求。
  4. 单飞批处理:exportSpans 期间新数据继续入队,不会互相阻塞。

五、采样器源码(sdk/trace/sampling.go)

文件结构一目了然,每个采样器是一个小结构体:

// TraceIDRatioBased:按 trace_id 前若干 bit 与阈值比较
// 同一 trace_id 永远得到同一决策(确定性,这是不产生"半截链路"的根本)
func (ts TraceIDRatioBased) ShouldSample(p SamplingParams) SamplingResult {
    // x = traceID 的前 64bit / 2^64 → [0,1)
    // x < ratio → RecordAndSample,否则 Drop
}

// ParentBased:看父的采样位
func (ps ParentBased) ShouldSample(p SamplingParams) SamplingResult {
    if 父存在 {
        父 sampled → root(默认 AlwaysOn)……即"跟随父"
        父未采    → remoteNotSampled/localNotSampled(默认 AlwaysOff)
    }
    // 无父 → root sampler
}

对照前面文章的概念表再读一遍源码,概念会立刻落地。也可以阅读 sdk/trace/sampling.go 顶部的注释,规范引用写得很全。

六、Resource 自动检测

sdk/resource/ 目录:

  • resource.go:Merge、Default、NewWithAttributes 的实现,合并冲突时后(高优先级)覆盖前;
  • env.go:读 OTEL_SERVICE_NAME、OTEL_RESOURCE_ATTRIBUTES;
  • builtin.go:进程内置属性(SDK 语言/版本、PID 等);
  • host*.go:hostname / host id 检测;
  • auto.go:resource.Default() 的编排入口。

优先级链:代码显式传入 > 环境变量 > 自动检测 > 内置默认。K8s 或云厂商元数据一般不放 SDK 端,而是由 Collector 用 resourcedetection processor 补齐。

七、OTLP Exporter 与重试

目录:exporters/otlp/otlptrace/

otlptrace/
├── otlptrace.go          # Client 接口 + NewExportStream 组合入口
├── otlptracegrpc/        # gRPC 传输实现
│   ├── options.go        #   WithEndpoint / WithTLSCredentials / WithHeaders...
│   ├── client.go         #   启动 gRPC 连接、Export 调用
│   └── internal.go       #   reattempt 调度(导出失败的重试节奏)
├── otlptracehttp/        # HTTP/protobuf 传输实现(结构对称)
└── internal/             # proto 转换:SDK 的 ReadOnlySpan → pb.Span
    └── otlp/transform/   # ★ 属性/SpanContext 转 protobuf 的地方

三个阅读点:

  1. transform:SDK 内部 Span 结构如何映射为 OTLP 的 ExportTraceServiceRequest,这部分衔接后续的 proto 内容;
  2. 重试:导出失败按指数退避重试,受 MaxAttempts、backoff 控制,并尊重服务端返回的 retry 建议;重试仍失败则丢弃这批数据并计数;
  3. partial success:服务端可能返回“接收了 1000 条、拒绝 3 条”,exporter 会上报相应指标或日志。

八、性能控制:SpanLimits

sdk/trace/provider.go 的 WithSpanLimits 及相关 limits 代码:

sdktrace.WithSpanLimits(sdktrace.SpanLimits{
    AttributeCountLimit:         128, // 单 Span 属性上限,默认 128
    EventCountLimit:             256,
    LinkCountLimit:              128,
    AttributeValueLengthLimit:   -1,  // 无限制;可设字节数
})

配合 evictedqueue,保证单个 Span 的内存占用有上界,这是“埋点不会拖垮业务”的内存侧保障。SDK 的性能设计可以总结为三条:采样前置、批量异步、上限裁剪。

九、contrib 仓库:otelhttp 源码走读

仓库: https://github.com/open-telemetry/opentelemetry-go-contrib 目录:instrumentation/net/http/otelhttp/

9.1 客户端 Transport(出站)

// transport.go / client.go
client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}

RoundTrip 流程:

1. 从 req.Context() 取当前 SpanContext
2. 创建 CLIENT Span(name = 请求方法+URL 路径模板)
3. Inject(ctx, req.Header)         ← 03 章 4.3 手写的那行
4. 真正发起 http.DefaultTransport.RoundTrip
5. defer:按响应状态码设置 Span 状态、记录语义约定属性、End()

9.2 服务端 Handler(入站)

// handler.go / server.go
http.Handle("/api", otelhttp.NewHandler(h, "api"))

流程:Extract(r.Header) → 创建 SERVER Span → 把带 Span 的 ctx 放进 r.Context() → 业务 handler 从 r.Context() 拿到链路上下文。

9.3 阅读建议

先读 example/ 下的两个最小示例,再顺着 client.go / server.go 的注释读属性注入,理解语义约定的应用。读懂 otelhttp 后,otelgin、otelgrpc 都是同构的“包一层 + 注入/提取 + 语义属性”模式。

十、推荐阅读顺序与关键文件清单

顺序 文件 目标
1 example/ + 03 章代码 建立直觉
2 trace/trace.go 只读接口,建立词汇表
3 internal/global/tracer.go 理解委托机制(30 分钟)
4 sdk/trace/provider.go Provider 装配了什么
5 sdk/trace/tracer.go → span.go Span 生命周期主线 ★
6 sdk/trace/batch_span_processor.go 批处理与丢数据语义 ★
7 sdk/trace/sampling.go 采样落地
8 exporters/otlp/otlptrace/(transform + grpc client) 数据出门
9 contrib:instrumentation/net/http/otelhttp/ 自动埋点范式
10 sdk/metric/(aggregation、reader、view) 指标进阶(可选)

注意:OpenTelemetry 迭代很快,文件名或函数名可能随版本微调,例如重构进 client.go / server.go,一切以当前仓库为准。本清单给的是稳定的阅读路径,不是逐行地图。




上一篇:DeepSeek 最有价值的 10 个开源项目盘点:从算子到 Agent 全栈解析
下一篇:组织智能:AI胜任力与协同力如何改变企业规模与工资
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-2 01:04 , Processed in 0.101579 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表