学习目标
学完本章你应该能够:
- 用自己的话讲清 OpenTelemetry 和 Jaeger 分别解决什么问题、两者是什么关系。
- 说出分布式链路追踪(Distributed Tracing)要解决的痛点:跨服务调用为什么"看不见"。
- 在本地用 Docker 把 Jaeger 跑起来,并说清 16686 / 4317 / 4318 三个端口各自干什么。
- 用 Go 代码(otel SDK)手动创建
TracerProvider、导出器,以及root-span/child-span,把链路推到 Jaeger。 - 在面试里把"一条调用链 = 一串有父子关系的 Span"讲成图,而不是念概念。
前置知识:
- 会跑一条
docker run命令,知道端口映射-p的含义。 - 能看懂简单的 Go 代码(
context、time.Sleep、错误处理)。 - 了解 HTTP 与 gRPC 是两种常见的进程间通信协议即可,不需要会写 gRPC 服务。
本章你会动手做的事:
- 拉起一个 Jaeger 容器,浏览器打开面板确认安装成功。
- 复制文中的 HTTP / gRPC 两段代码,把
srv.com换成你的 Jaeger 地址跑通。 - 在 Jaeger UI 里亲眼看到
root-span和child-span的层级关系。
前言
Opentelemetry
分布式链路跟踪( Distributed Tracing )的概念最早是由 Google 提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在 Tracing技术这块比较有影响力的是两大开源技术框架:Netflix 公司开源的 OpenTracing和 Google 开源的 OpenCensus。两大框架都拥有比较高的开发者群体。为形成统一的技术标准,两大框架最终磨合成立了 OpenTelemetry 项目,简称 otel
Jaeger
Jaeger\ˈyā-gər\ 是 Uber 开源的分布式追踪系统,是支持 OpenTelemetry 的系统之一,也是 CNCF
项目。
下面这张图把"应用 → 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:

看到 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-span 和 child-span 的关系,就像前面说的"快递流转单"。root-span 是整张单子(一次完整请求),child-span 是其中某一站盖的章(一个子步骤)。因为 child-span 是在 root-span 的 ctx 里 Start 出来的,所以它天然认 root-span 当爹——这就是链路的父子层级,Jaeger 面板里看到的就是这棵树。flowchart TB
Root[root-span
耗时约 100ms]
Root --> Child[child-span
耗时约 50ms
挂在 root 之下]效果
执行后,在面板中即可看到我们上传的数据。


可以看到我们的两个 span 已经上传到 Jaeger 中了,就是如此的简单!文中的代码开源在 Github
。
WithEndpoint 地址写错 / 端口用混。HTTP 导出器要走 4318,路径是 /v1/traces;gRPC 导出器走 4317。如果你把 HTTP 导出器指向了 4317,或者忘记 tp.Shutdown(ctx),很可能 Jaeger 面板里"啥也没有"——数据要么没发对地方,要么还在缓冲区没刷出去。自测题与动手练习
traceparent)以及生成、传递、导出数据的 API;Jaeger 是"后端实现":负责接收、存储、在 UI 里查询展示。一句话:OTel 管"怎么产生和搬运数据",Jaeger 管"收下来存好给人看"。
人和程序上报分开:4317 / 4318 都给程序上报,16686 给人看。
child-span 是怎么"认 root-span 当爹"的?关键在哪一行代码?child-span 是在 root-span 的 ctx 里 tracer.Start 出来的——关键在 ctx, span := tracer.Start(ctx, "root-span") 之后,用同一个 ctx 再 tracer.Start(ctx, "child-span"),SpanContext 携带 parent 信息,于是天然形成父子层级。两个常见原因:
- 端口 / 协议用混:HTTP 导出器指向了
4317,或路径不是/v1/traces; - 没调
tp.Shutdown(ctx):span 还在批处理缓冲区里没刷出去。
当然也可能地址写错(比如srv.com没换成真实 Jaeger 地址)。
动手练习(建议真做一遍):
- 把代码里的
srv.com换成你本机 Jaeger 地址(如localhost),分别用 HTTP 和 gRPC 两种导出器各跑一次,对比面板显示。 - 在
root-span和child-span之间再加一个grandchild-span,观察 Jaeger 里是否出现三层嵌套。 - 故意把 HTTP 导出器的端口写成
4317,运行并观察报错或空面板,验证"端口用混"这个坑。
本章小结
- OpenTelemetry 是分布式追踪的统一标准与 SDK,负责生成、传递、导出链路数据;Jaeger 是支持该标准的收集 + 存储 + 展示系统。
- 一条调用链 = 一个
Trace,由一棵有父子关系的 Span 树组成;child-span必须在root-span的ctx中Start才会形成层级。 - 本地跑通只需四步:Docker 起 Jaeger →
go get装依赖 → 建TracerProvider+ 导出器 →Start/EndSpan 并Shutdown刷数据。 - 三个端口记牢:16686 看面板、4317 走 gRPC 上报、4318 走 HTTP 上报。
下一章可以接着学:怎么在真实 HTTP / gRPC 微服务里用
otelhttp等 instrumentation 自动埋点,而不用手动Start/End每个 Span。