Go 链路追踪入门 Opentelemetry

2024-06-03T14:11:02+08:00 | 6分钟阅读 | 更新于 2024-06-03T14:11:02+08:00

@

学习目标

学完本章你应该能够:

  1. 用自己的话讲清 OpenTelemetryJaeger 分别解决什么问题、两者是什么关系。
  2. 说出分布式链路追踪(Distributed Tracing)要解决的痛点:跨服务调用为什么"看不见"。
  3. 在本地用 Docker 把 Jaeger 跑起来,并说清 16686 / 4317 / 4318 三个端口各自干什么。
  4. 用 Go 代码(otel SDK)手动创建 TracerProvider、导出器,以及 root-span / child-span,把链路推到 Jaeger。
  5. 在面试里把"一条调用链 = 一串有父子关系的 Span"讲成图,而不是念概念。

前置知识

  • 会跑一条 docker run 命令,知道端口映射 -p 的含义。
  • 能看懂简单的 Go 代码(contexttime.Sleep、错误处理)。
  • 了解 HTTP 与 gRPC 是两种常见的进程间通信协议即可,不需要会写 gRPC 服务。

本章你会动手做的事

  • 拉起一个 Jaeger 容器,浏览器打开面板确认安装成功。
  • 复制文中的 HTTP / gRPC 两段代码,把 srv.com 换成你的 Jaeger 地址跑通。
  • 在 Jaeger UI 里亲眼看到 root-spanchild-span 的层级关系。

前言

Opentelemetry

分布式链路跟踪( Distributed Tracing )的概念最早是由 Google 提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在 Tracing技术这块比较有影响力的是两大开源技术框架:Netflix 公司开源的 OpenTracing和 Google 开源的 OpenCensus。两大框架都拥有比较高的开发者群体。为形成统一的技术标准,两大框架最终磨合成立了 OpenTelemetry 项目,简称 otel

类比:把一次用户请求想象成快递从下单到签收的全过程。在单体应用里,这一个"包裹"全程在你自己仓库里,查起来很容易;但在微服务里,包裹要经过"下单服务 → 库存服务 → 支付服务 → 物流服务",每个服务是不同的仓库。问题来了:包裹卡在哪个仓库?耗时花在哪一站?链路追踪就是给每个包裹贴一张全程流转单(Trace),每一站盖一个章(Span),这样你就能在后台一眼看穿整条链路。

白话:OpenTelemetry 干的事,就是制定"流转单长什么样、每个仓库怎么盖章"的统一标准。以前 OpenTracing 和 OpenCensus 各自一套标准,谁也不服谁,后来合并成 OpenTelemetry,相当于快递行业终于统一了面单格式。

Jaeger

Jaeger\ˈyā-gər\ 是 Uber 开源的分布式追踪系统,是支持 OpenTelemetry 的系统之一,也是 CNCF 项目。

类比:OpenTelemetry 是"统一面单标准 + 盖章工具",而 Jaeger 是收单的邮局兼查询后台——各个仓库按标准把面单(链路数据)发到 Jaeger,Jaeger 负责存起来,并在网页上让你按服务、按时间查某次请求的完整流转图。

下面这张图把"应用 → OTel SDK → 导出器 → Jaeger"的数据流向串起来:

flowchart LR
    A[业务应用
Go 代码] --> B[OpenTelemetry SDK
Tracer 创建 Span] B --> C[Exporter
HTTP 4318 / gRPC 4317] C --> D[Jaeger
收集 / 存储 / 展示]

安装 Jaeger

Jaeger 为我们准备了 Docker 镜像,我们可以很容易的安装。

docker run --rm --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318

简单的介绍以下这三个端口。16686 用做 Jaeger 服务的 Web 面板,一会我们可以在浏览器中访问它;4317 和 4318 都用做上传追踪数据,不同之处在于前者是 gRPC 协议,后者是 HTTP 协议。

Jaeger 还有很多可用的端口,本篇只介绍和 otel 相关的,具体可以查看 Jaeger 官方文档哦。

安装后,在浏览器中输入 IP:16686:

image-20250425021724488

看到 gopher 侦探在追踪足迹的可爱图片就代表 Jaeger 安装成功咯。

下面这张图把三个端口的用途一图说清,记住它面试就不会混:

flowchart TB
    P16686[16686 端口
Web 面板:浏览器查看链路] P4317[4317 端口
gRPC 协议:上传追踪数据] P4318[4318 端口
HTTP 协议:上传追踪数据]

编写 Go 代码

安装依赖:

go get "go.opentelemetry.io/otel" \
  "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" \
  "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" \
  "go.opentelemetry.io/otel/propagation" \
  "go.opentelemetry.io/otel/sdk/metric" \
  "go.opentelemetry.io/otel/sdk/resource" \
  "go.opentelemetry.io/otel/sdk/trace" \
  "go.opentelemetry.io/otel/semconv/v1.24.0" \
  "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

我这里贴出 HTTP 和 gRPC 的全部代码,直接复制过去,改成自己的地址即可:

HTTP

func TestTraceHttp(t *testing.T) {
	ctx := context.Background()

	// 步骤 1:创建 OTLP HTTP 导出器,连接到 Jaeger
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpointURL("http://srv.com:4318/v1/traces"))
	if err != nil {
		log.Fatalf("创建导出器失败: %v", err)
	}

	// 步骤 2:创建资源(标识这是哪个服务上报的数据)
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String("otel-traces-demo-http"),
		),
	)
	if err != nil {
		log.Fatalf("创建资源失败: %v", err)
	}

	// 步骤 3:创建 Tracer 提供器(SDK 总入口)
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)

	// 步骤 4:设置为全局 Tracer 提供器
	otel.SetTracerProvider(tp)

	// 步骤 5:创建 root-span(一条链路的根)
	tracer := otel.Tracer("example-tracer")
	ctx, span := tracer.Start(ctx, "root-span")
	time.Sleep(100 * time.Millisecond) // 模拟业务耗时 100ms
	span.End()

	// 步骤 6:创建 child-span(挂在 root 之下)
	_, childSpan := tracer.Start(ctx, "child-span")
	time.Sleep(50 * time.Millisecond) // 模拟子任务耗时 50ms
	childSpan.End()

	// 步骤 7:确保所有的 spans 都被发送
	if err := tp.Shutdown(ctx); err != nil {
		log.Fatalf("关闭 Tracer 提供器失败: %v", err)
	}
}

gRPC

func TestTraceGrpc(t *testing.T) {
	ctx := context.Background()

	// 步骤 1:创建 OTLP gRPC 导出器,连接到 Jaeger
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint("srv.com:4317"),
		otlptracegrpc.WithInsecure(),
	)
	if err != nil {
		log.Fatalf("创建导出器失败: %v", err)
	}

	// 步骤 2:创建资源(标识服务名)
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String("otel-traces-demo-grpc"),
		),
	)
	if err != nil {
		log.Fatalf("创建资源失败: %v", err)
	}

	// 步骤 3:创建 Tracer 提供器
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)

	// 步骤 4:设置为全局 Tracer 提供器
	otel.SetTracerProvider(tp)

	// 步骤 5:创建 root-span
	tracer := otel.Tracer("example-tracer")
	ctx, span := tracer.Start(ctx, "root-span")
	time.Sleep(100 * time.Millisecond)
	span.End()

	// 步骤 6:创建 child-span
	_, childSpan := tracer.Start(ctx, "child-span")
	time.Sleep(50 * time.Millisecond)
	childSpan.End()

	// 步骤 7:确保所有的 spans 都被发送
	if err := tp.Shutdown(ctx); err != nil {
		log.Fatalf("关闭 Tracer 提供器失败: %v", err)
	}
}

类比:上面两段代码里 root-spanchild-span 的关系,就像前面说的"快递流转单"。root-span 是整张单子(一次完整请求),child-span 是其中某一站盖的章(一个子步骤)。因为 child-span 是在 root-spanctxStart 出来的,所以它天然认 root-span 当爹——这就是链路的父子层级,Jaeger 面板里看到的就是这棵树。

flowchart TB
    Root[root-span
耗时约 100ms] Root --> Child[child-span
耗时约 50ms
挂在 root 之下]

效果

执行后,在面板中即可看到我们上传的数据。

image-20250425021831696

image-20250425021919134

可以看到我们的两个 span 已经上传到 Jaeger 中了,就是如此的简单!文中的代码开源在 Github

⚠️ 新手必踩的坑:WithEndpoint 地址写错 / 端口用混。HTTP 导出器要走 4318,路径是 /v1/traces;gRPC 导出器走 4317。如果你把 HTTP 导出器指向了 4317,或者忘记 tp.Shutdown(ctx),很可能 Jaeger 面板里"啥也没有"——数据要么没发对地方,要么还在缓冲区没刷出去。


自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. OpenTelemetry 和 Jaeger 各自扮演什么角色?一句话区分。
  2. 为什么说 OpenTelemetry 是"标准"、Jaeger 是"实现"?它俩能不能换别的组合?
  3. Jaeger 的三个端口 16686 / 4317 / 4318 分别干什么?哪个是给人看的,哪个是给程序上报的?
  4. child-span 是怎么"认 root-span 当爹"的?关键在哪一行代码?
  5. 如果跑完代码 Jaeger 面板里看不到 span,至少列出两个可能原因。

动手练习(建议真做一遍)

  1. 把代码里的 srv.com 换成你本机 Jaeger 地址(如 localhost),分别用 HTTP 和 gRPC 两种导出器各跑一次,对比面板显示。
  2. root-spanchild-span 之间再加一个 grandchild-span,观察 Jaeger 里是否出现三层嵌套。
  3. 故意把 HTTP 导出器的端口写成 4317,运行并观察报错或空面板,验证"端口用混"这个坑。

本章小结

  • OpenTelemetry 是分布式追踪的统一标准与 SDK,负责生成、传递、导出链路数据;Jaeger 是支持该标准的收集 + 存储 + 展示系统
  • 一条调用链 = 一个 Trace,由一棵有父子关系的 Span 树组成;child-span 必须在 root-spanctxStart 才会形成层级。
  • 本地跑通只需四步:Docker 起 Jaeger → go get 装依赖 → 建 TracerProvider + 导出器 → Start/End Span 并 Shutdown 刷数据。
  • 三个端口记牢:16686 看面板、4317 走 gRPC 上报、4318 走 HTTP 上报。

下一章可以接着学:怎么在真实 HTTP / gRPC 微服务里用 otelhttp 等 instrumentation 自动埋点,而不用手动 Start/End 每个 Span。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!