学习目标
学完本章你应该能够:
- 用自己的话讲清 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
类比:把一次用户请求想象成快递从下单到签收的全过程。在单体应用里,这一个"包裹"全程在你自己仓库里,查起来很容易;但在微服务里,包裹要经过"下单服务 → 库存服务 → 支付服务 → 物流服务",每个服务是不同的仓库。问题来了:包裹卡在哪个仓库?耗时花在哪一站?链路追踪就是给每个包裹贴一张全程流转单(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:

看到 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 面板里"啥也没有"——数据要么没发对地方,要么还在缓冲区没刷出去。
自测题与动手练习
自测题(合上书能答出来,才算懂):
- OpenTelemetry 和 Jaeger 各自扮演什么角色?一句话区分。
- 为什么说 OpenTelemetry 是"标准"、Jaeger 是"实现"?它俩能不能换别的组合?
- Jaeger 的三个端口 16686 / 4317 / 4318 分别干什么?哪个是给人看的,哪个是给程序上报的?
child-span是怎么"认root-span当爹"的?关键在哪一行代码?- 如果跑完代码 Jaeger 面板里看不到 span,至少列出两个可能原因。
动手练习(建议真做一遍):
- 把代码里的
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。