1. 分布式追踪基础

1.1 分布式系统面临的挑战

现代微服务架构将一个单体应用拆分为数十甚至数百个独立部署的服务。这种架构带来了弹性、独立部署和团队自治等优势,但也引入了传统单体架构中不存在的运维与排查难题。

核心痛点: 当一个用户请求跨越多个服务节点时,任何一个环节的延迟或错误都会影响整体体验。传统日志和单机监控无法将跨服务的调用链关联起来,导致故障排查如同“大海捞针”。

分布式系统在可观测性方面面临的主要挑战包括:

挑战维度 具体表现 传统手段的局限
调用拓扑模糊 服务间依赖关系复杂,调用路径动态变化 静态架构图无法反映运行时真实调用链
性能瓶颈定位难 请求延迟可能由任意下游服务或网络引起 单机指标无法关联端到端延迟
错误根因分散 异常可能发生在调用链的任意节点 日志分散在不同主机,缺乏上下文关联
数据孤岛 Metrics、Logs、Traces 各自独立 缺乏统一视角,排查需手动切换多个系统

分布式追踪正是为解决上述挑战而生。它通过为每个请求注入全局唯一的 Trace ID,将跨服务的调用记录串联起来,形成一条完整的调用链路。

分布式系统调用拓扑与追踪对比 传统视角:调用关系混乱 服务A 服务B 服务C 服务D 服务E 无法快速定位故障点 追踪视角:调用链清晰 服务A 服务B 服务C 服务D 服务E Trace ID 串联所有调用

1.2 追踪的核心概念:Trace 与 Span

分布式追踪模型围绕两个核心抽象构建:Trace(追踪)和 Span(跨度)。理解这两个概念是掌握 Jaeger 的基础。

1.2.1 Trace(追踪)

一个 Trace 代表一条端到端的请求路径,贯穿整个分布式系统。它由一个全局唯一的 Trace ID 标识。Trace 可以看作是一棵由多个 Span 组成的树,树的根节点是入口服务(如 API 网关或前端服务)。

1.2.2 Span(跨度)

Span 是 Trace 中的基本工作单元,代表一个服务内部或一次跨服务调用所执行的操作。每个 Span 包含以下关键信息:

  • Span ID:当前 Span 的唯一标识。
  • Parent Span ID:父 Span 的 ID,用于构建调用层级关系。
  • Trace ID:所属 Trace 的全局 ID。
  • 操作名称:描述该 Span 执行的操作,如 HTTP GET /api/users。
  • 开始时间与持续时间:记录操作的耗时。
  • Tags(标签):键值对形式的元数据,用于描述 Span 的属性(如 HTTP 状态码、错误信息)。
  • Logs(日志):时间戳标记的结构化日志,用于记录 Span 生命周期中的关键事件。
类比理解: 将 Trace 想象成一次完整的快递配送过程(从下单到签收),每个 Span 就是配送过程中的一个环节(揽件、运输、派送)。Trace ID 是快递单号,Span ID 是每个环节的操作编号。

1.2.3 Trace 与 Span 的关系

Trace 由 Span 通过父子关系组织成树形结构。根 Span 没有父 Span,代表请求的入口。子 Span 代表入口服务调用的下游操作。这种层级关系使得我们可以精确分析请求在每一跳上的耗时。

Trace 与 Span 的树形结构 根 Span:API 网关 Trace ID: abc-123-def Span:用户服务 Span:订单服务 Span:数据库查询 Span:支付服务 Span:库存服务 Span:第三方 API

1.3 OpenTracing 标准简介

在 Jaeger 出现之前,不同的追踪系统(如 Zipkin、AppDynamics)各自定义了私有 API,导致应用代码与特定厂商绑定。为了解决这个问题,CNCF 孵化并推广了 OpenTracing 标准——一套厂商中立的分布式追踪 API 规范。

1.3.1 OpenTracing 的核心作用

  • 解耦应用与追踪系统:开发者只需面向 OpenTracing API 编程,底层可切换不同的追踪实现(Jaeger、Zipkin 等)。
  • 统一数据模型:定义了 Trace、Span、SpanContext、Tags、Logs 等标准数据结构。
  • 跨进程传播:定义了如何通过 HTTP 头或 RPC 元数据将 SpanContext 从上游服务传递到下游服务。

1.3.2 OpenTracing 数据模型

概念 说明 示例
SpanContext 跨进程传递的追踪上下文,包含 Trace ID、Span ID 和 Baggage Items trace_id=abc, span_id=123
Tags 键值对,用于描述 Span 的元数据 http.status_code=200
Logs 带时间戳的结构化事件 event=error, message=timeout
Baggage Items 随 Trace 传播的键值对,用于传递业务上下文 user_id=9527

1.3.3 跨进程传播(Propagation)

OpenTracing 定义了 Inject(注入)和 Extract(提取)操作,用于在服务间传递 SpanContext:

  • Inject:客户端将当前 SpanContext 序列化并注入到 HTTP 头或 RPC 元数据中。
  • Extract:服务端从请求中反序列化出 SpanContext,并创建子 Span。
常见误区: 很多团队在接入 Jaeger 时,只关注单机内的 Span 创建,却忽略了跨进程的 Propagation 配置。这会导致 Trace 在服务边界处断裂,无法形成完整的调用链。务必确保每个 RPC/HTTP 客户端都正确执行了 Inject 操作。

1.3.4 代码示例:使用 OpenTracing API 创建 Span

以下示例展示了如何在 Go 语言中使用 OpenTracing API 创建根 Span 并添加标签:

import (
    "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/log"
)

func handleRequest(ctx context.Context) {
    // 创建根 Span
    span := opentracing.StartSpan("handleRequest")
    defer span.Finish()

    // 设置标签
    span.SetTag("http.method", "GET")
    span.SetTag("http.url", "/api/users")

    // 记录日志事件
    span.LogFields(
        log.String("event", "request_received"),
        log.Int64("payload_size", 1024),
    )

    // 将 Span 存入 Context,传递给下游
    ctx = opentracing.ContextWithSpan(ctx, span)

    // 调用下游服务
    callDownstreamService(ctx)
}
Jaeger 与 OpenTracing 的关系: Jaeger 是 OpenTracing 标准的原生实现之一。Jaeger 客户端库直接实现了 OpenTracing API,因此使用 Jaeger 时,你实际上就是在使用 OpenTracing 标准。这也意味着,如果未来需要切换到其他兼容 OpenTracing 的追踪系统,你的代码改动量会非常小。

1.4 本章小结