Envoy Jaeger Tracing 示例详解

本文通过详细解析 Envoy 官方 Jaeger Tracing 示例,展示了如何构建分布式追踪架构,涵盖架构设计、文件配置及运行流程,帮助读者深入理解服务网格中的追踪上下文传播与数据可视化。

本文详细解析了 Envoy 官方示例 jaeger-tracing 的构建流程、相关文件及其功能,旨在补充 Envoy 官方文档中对 Jaeger tracing 说明的不足之处。该示例将服务与 Envoy 部署在同一容器中,由 Envoy 负责服务发现、追踪和网络逻辑,初步展现了服务网格的架构理念。这种设计不仅有助于理解分布式追踪的实现,还为深入学习如 Istio 等服务网格技术奠定了基础。

1. 架构概览

此示例通过 docker-compose 启动了一个包含多个容器的微服务环境。其架构并非传统的 Sidecar 模式,而是采用一个入口代理结合内部路由代理的方式。

核心架构如下:

  1. 一个入口 Envoy 代理: 名为 front-envoy,是所有外部请求的统一入口,也是追踪的发起者。
  2. 两个后端服务: service1service2。这两个服务容器内部都运行着一个 Envoy 代理,但它们的作用是进行服务间的路由并传播追踪上下文,而不是生成追踪数据。
  3. 一个 Jaeger All-in-One 实例: 用于接收、存储和可视化由 front-envoy 发送过来的追踪数据。

请求与追踪流程: 一个外部请求首先到达 front-envoyfront-envoy 在此处发起追踪,生成 Span,并添加追踪头,然后将请求路由到 service1service1 内部的 Envoy 在收到请求后,会传播追踪头,并将请求路由到 service2。最终,所有 Span 数据由 front-envoy 统一发送给 Jaeger。

请求与追踪流程示意图
请求与追踪流程示意图

2. 文件作用详解

下面我们来逐一分析每个文件的作用。

docker-compose.yaml

这是整个示例的“总指挥”,定义了四个核心服务:jaeger, service1, service2, 和 front-envoy

  • front-envoy: 使用 envoy.yaml 作为配置,是所有流量的入口代理,并暴露 10000 端口。
  • service1 & service2: 后端服务。它们各自将 service1-envoy-jaeger.yamlservice2-envoy-jaeger.yaml 挂载到容器内,供其内部的 Envoy 进程使用。
  • jaeger: Jaeger 服务,用于接收和展示追踪数据。

envoy.yaml (front-envoy 的配置)

这是入口代理 front-envoy 的核心配置文件。

  • 关键配置:
    • tracing: 这是启用和发起追踪的关键部分
      • provider: 指定了追踪服务提供商为 zipkin (兼容 Jaeger)。
      • collector_cluster: jaeger,指明了将追踪数据发送到 jaeger 服务。
    • route_config: 定义了路由规则,将所有收到的请求 (/) 都路由到 service1 集群。
    • clusters: 定义了上游集群,包括 service1jaeger

service1-envoy-jaeger.yaml (service1 内部 Envoy 的配置)

此配置文件由 service1 容器内部的 Envoy 加载。

  • 作用: 纯粹的路由功能。
  • 关键配置:
    • 没有 tracing 配置块,因此它不会发起追踪,只会传播上游 (front-envoy) 传来的追踪头。
    • route_config: 将所有收到的请求路由到 service2 集群。

service2-envoy-jaeger.yaml (service2 内部 Envoy 的配置)

service1 的配置类似,由 service2 容器内部的 Envoy 加载。

  • 作用: 请求的终点路由。
  • 关键配置:
    • 同样没有 tracing 配置块。
    • route_config: 将请求路由到 service_log(在本示例中可视为终点)。

verify.sh

用于验证示例是否正常工作的自动化脚本。

  • 执行流程:
    1. curl localhost:10000/: 向 front-envoy 发送一个请求。
    2. 等待几秒,让 front-envoy 将追踪数据异步发送到 Jaeger。
    3. 向 Jaeger API (http://localhost:16686/api/traces?service=front-proxy) 查询由 front-proxy 服务(front-envoyenvoy.yaml 中定义的集群名)生成的追踪数据,并验证其完整性。

3. 构建与运行流程

  1. 启动所有服务:

    docker-compose up --build -d
    
  2. 发送测试请求:

    ./verify.sh
    
  3. 在 Jaeger UI 中查看追踪:

    • 在浏览器中打开 Jaeger UI: http://localhost:16686
    • 在左上角的 “Service” 下拉菜单中选择 front-proxy
    • 点击 “Find Traces” 按钮。
    • 点击找到的追踪记录,你将看到一个包含多个 Span 的完整调用链,清晰地展示了从 front-envoyservice1 再到 service2 的完整流程。

4. 容器内部进程分析

下面我们来逐一分析每个容器内部运行的核心进程。

jaeger-tracing-front-envoy-1

  • 容器角色: 入口 Envoy 代理。
  • 运行进程:
    • envoy: 这是该容器的唯一核心进程。docker-entrypoint 脚本启动了 Envoy 代理。它会加载 envoy.yaml 配置文件,监听 10000 端口,处理所有传入的变量,并负责发起和上报追踪数据。

jaeger-tracing-service1-1

  • 容器角色: 后端应用服务 service1
  • 运行进程:
    • envoy (后台进程): 容器内的启动脚本 (/usr/local/bin/start_service.sh) 首先会在后台启动一个 Envoy 代理进程。这个 Envoy 进程加载的是 service1-envoy-jaeger.yaml 配置,负责将流入的请求路由到 service2
    • python (前台进程): 脚本接着会启动一个 Python 应用服务器(基于 aiohttp),这个 Python 进程是 service1 的业务逻辑本身,它会监听一个内部端口(如 8000),等待并处理由其内部 Envoy 代理转发过来的请求。

jaeger-tracing-service2-1

  • 容器角色: 后端应用服务 service2
  • 运行进程:
    • envoy (后台进程): 与 service1 完全相同,容器内的启动脚本会先在后台启动一个 Envoy 代理。这个 Envoy 进程加载的是 service2-envoy-jaeger.yaml 配置。
    • python (前台进程): 同样,脚本会启动一个 aiohttp Python 应用服务器作为 service2 的业务逻辑。

jaeger-tracing-jaeger-1

  • 容器角色: Jaeger 分布式追踪系统。
  • 运行进程:
    • /go/bin/all-in-one: 这是 Jaeger 官方提供的“一体化”可执行文件。这个单一进程包含了 Jaeger 的所有核心组件,方便在开发和测试环境中快速部署:
      • Jaeger Agent: 监听并接收 Span 数据(例如,通过 Zipkin 协议的 9411 端口)。
      • Jaeger Collector: 从 Agent 接收数据,进行验证、处理和存储。
      • Jaeger Query: 提供 API 接口用于查询和检索追踪数据。
      • Jaeger UI: 提供一个 Web 界面(在 16686 端口),用于可视化展示追踪数据。

5. 深入探讨:为什么 Service 容器内需要 Envoy?

这是一个非常核心的问题,触及了“服务网格”(Service Mesh)架构的精髓。简单来说,在 service 容器内运行 Envoy 进程是为了将复杂的网络通信逻辑从应用代码中剥离出来

尽管在这个示例中,front-envoy 处理了追踪的“发起”,但 service1service2 内部的 Envoy 进程仍然扮演着至关重要的角色:

1. 服务发现 (Service Discovery)

  • 问题: service1 的 Python 代码如何知道 service2 在哪里?在动态的容器环境中,IP 地址是会变化的,硬编码地址是不行的。
  • 解决方案: service1 的 Python 代码被配置为将所有出站请求都发送给它自己容器内的 Envoy 代理(通常是发往 localhost)。然后,这个内部的 Envoy 代理根据它的配置文件 (service1-envoy-jaeger.yaml),知道 service2 这个服务的逻辑名称,通过 Docker 的内部 DNS 将其解析到正确的容器地址。
  • 好处: 应用代码变得极其简单,它不需要关心网络拓扑,只需要知道下一个服务的逻辑名称即可。

2. 追踪上下文的传播 (Trace Context Propagation)

  • 问题: front-envoy 发起了追踪并生成了追踪头(如 x-b3-traceid)。如果 service1 直接调用 service2,谁来保证这些追踪头能被正确地传递下去?
  • 解决方案: service1 内部的 Envoy 代理在接收到请求时,会自动识别这些追踪头,并在将请求转发给 service2 时,确保这些头信息被完整地包含在内。
  • 好处: 这保证了分布式追踪的链条不会断裂。开发者无需在 Python 代码中编写任何用于提取和重新注入追踪头的逻辑。Envoy 透明地处理了这一切。

3. 统一的网络控制层 (Unified Network Control)

  • 问题: 如果你想为服务间的调用添加更复杂的网络策略,比如重试、超时、熔断、流量加密 (mTLS) 等,应该在哪里实现?
  • 解决方案: 这一切都可以在 Envoy 的配置文件中声明式地完成。你不需要在 Python、Java、Go 等每一种语言的服务中都重复实现一遍这些复杂的逻辑。
  • 好处:
    • 语言无关: 无论你的服务是用什么语言写的,网络行为都由 Envoy 统一控制。
    • 简化应用: 应用开发者可以专注于业务逻辑,而不是处理复杂的网络故障场景。
    • 集中管理: 运维人员可以通过修改 Envoy 配置来调整整个系统的网络策略,而无需改动或重新部署任何应用代码。

总结

在这个示例中,service 容器内的 Envoy 进程虽然看起来只是做了一个简单的路由,但它实际上是构建了一个微型但功能完备的“服务网格”的基石。它充当了一个智能的、可配置的本地网络代理,将服务本身(Python 应用)与服务之间的复杂通信(网络)彻底解耦。

6. Dockerfile 与 Envoy 配置深度解析

Dockerfile 解析 (基于通用实践)

此示例中的 Dockerfile 位于 ../shared/ 目录,体现了镜像复用的思想。

../shared/envoy/Dockerfile (用于 front-envoy)

这个 Dockerfile 的作用是构建一个纯粹的 Envoy 代理镜像。

# 使用官方的 Envoy 镜像作为基础
FROM envoyproxy/envoy:v1.23-latest 
# (v1.23-latest 是一个示例版本号)

# 将 docker-entrypoint.sh 脚本复制到容器中并赋予执行权限
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh

# 设置容器启动时执行的命令
ENTRYPOINT ["/docker-entrypoint.sh"]

# 默认启动命令,会被 docker-compose.yaml 中的 command 覆盖
CMD ["/usr/local/bin/envoy", "-c", "/etc/envoy/envoy.yaml"]
  • 核心: 它基于官方的 envoyproxy/envoy 镜像,这个镜像已经包含了编译好的 Envoy 二进制文件。
  • ENTRYPOINT: 使用一个自定义的 docker-entrypoint.sh 脚本作为入口,这通常是为了在启动 Envoy 之前执行一些预处理任务(比如等待其他服务就绪)。

../shared/python/Dockerfile (用于 service1service2)

这个 Dockerfile 更复杂,它需要将 Python 应用和 Envoy 代理打包到同一个镜像中。

# 使用一个包含 Python 环境的基础镜像
FROM python:3.9-slim

# 安装 Envoy
# (这里会包含从网络下载并安装 Envoy 二进制文件的步骤)
# 例如:
# RUN apt-get update && apt-get install -y curl
# RUN curl -L https://getenvoy.io/cli | bash -s -- -b /usr/local/bin
# RUN getenvoy fetch envoy:v1.23-latest --path /usr/local/bin/envoy

# 安装 Python 依赖
COPY requirements.txt .
RUN pip install -r requirements.txt

# 复制应用代码和启动脚本
COPY app.py /
COPY start_service.sh /
RUN chmod +x /start_service.sh

# 设置容器启动时执行的命令
CMD ["/start_service.sh"]
  • 多阶段构建: 它首先确保了 Python 环境,然后安装了 Envoy。
  • 打包: 将 Python 应用 (app.py)、依赖 (requirements.txt) 和启动脚本 (start_service.sh) 全部复制到镜像中。
  • CMD: 容器启动时执行 start_service.sh 脚本,该脚本会负责先启动后台的 envoy 进程,再启动前台的 python 应用进程。

Envoy 配置关键字段解析

envoy.yaml (front-envoy)

# ...
tracing:
  provider:
    name: envoy.tracers.zipkin
    typed_config:
      "@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig
      collector_cluster: jaeger
      collector_endpoint: "/api/v2/spans"
      shared_span_context: false
      collector_endpoint_version: HTTP_JSON
# ...
clusters:
- name: service1
  type: STRICT_DNS
  lb_policy: ROUND_ROBIN
  load_assignment:
    cluster_name: service1
    endpoints:
    - lb_endpoints:
      - endpoint:
          address:
            socket_address:
              address: service1
              port_value: 8000
  • tracing:
    • provider: 定义了追踪器。envoy.tracers.zipkin 是 Envoy 内置的与 Zipkin 协议兼容的追踪器,Jaeger 正好支持该协议。
    • collector_cluster: 关键连接。它告诉 Envoy 应该将追踪数据发送到名为 jaeger 的上游集群。
    • collector_endpoint: 指定了 Jaeger Collector 接收数据的具体 API 路径。
  • clusters:
    • type: STRICT_DNS: 表示 Envoy 会使用标准的 DNS 查询来发现这个集群中的后端地址。在 Docker Compose 环境中,服务名(如 service1)会自动注册到 Docker 的内部 DNS 中。
    • address: service1: Envoy 会去 DNS 查询名为 service1 的主机,Docker DNS 会将其解析为 jaeger-tracing-service1-1 容器的 IP 地址。

service1-envoy-jaeger.yaml

# ...
route_config:
  name: local_route
  virtual_hosts:
  - name: backend
    domains:
    - "*"
    routes:
    - match:
        prefix: "/"
      route:
        cluster: service2
      decorator:
        operation: checkStock
  • decorator:
    • operation: checkStock: 这是一个非常重要的字段。它为这个路由操作(即调用 service2)赋予了一个业务名称 checkStock。当你在 Jaeger UI 中查看追踪数据时,代表这个步骤的 Span 将会以 checkStock 命名,而不是一个模糊的 HTTP URL。这极大地提升了追踪数据的可读性。

7. 端到端追踪流程详解:一个请求的完整生命周期

为了完全理解其工作原理,让我们跟随一个外部请求,看看追踪信息是如何诞生、传递并最终被展示的。

请求追踪流程
请求追踪流程

第 1 步:追踪的诞生 (Initiation)

  1. 外部请求: 客户端(例如 curl)向 http://localhost:10000/ 发送一个普通的 HTTP 请求。这个请求不包含任何追踪信息。
  2. front-envoy 的决策: 请求到达 front-envoy。由于其 envoy.yaml 中配置了 tracing,Envoy 会检查请求头。
  3. 生成追踪上下文: front-envoy 发现没有传入的追踪头(如 x-b3-traceid),它判断这是一个新请求的开始。于是,它会:
    • 生成一个全局唯一的 Trace ID。这个 ID 将贯穿整个请求链路。
    • 为自己即将执行的第一个操作(将请求路由到 service1)生成一个 Span ID
    • 将这些 ID 以及其他信息(统称为“追踪上下文”)打包成标准的 HTTP 头(如 x-b3-traceid, x-b3-spanid, x-b3-sampled 等)。
  4. 注入追踪头: front-envoy 在将请求转发给 service1 之前,会将这些新生成的追踪头注入到请求中。

至此,一个分布式追踪正式诞生。

第 2 步:上下文的传播 (Propagation)

  1. 到达 service1: 带有追踪头的请求到达 service1 容器,并被其内部的 envoy 进程接收。
  2. 透明代理: service1envoy 检查请求头,发现了追踪上下文。由于它自己的配置 (service1-envoy-jaeger.yaml) 中没有 tracing 块,它不会尝试发起新的追踪或修改追踪 ID。它的职责是传播
  3. 转发给应用: service1envoy 将请求(连同所有追踪头)原封不动地转发给在同一容器内监听的 python 应用。
  4. 应用内调用: service1python 应用处理完业务逻辑后,需要调用 service2。它将请求发往自己本地的 envoy
  5. 再次传播: service1envoy 再次接收到这个出站请求,并将追踪头继续添加到发往 service2 的请求中。这个过程对 Python 应用是完全透明的。
  6. 到达 service2: 请求到达 service2,其内部的 envoy 同样地、透明地传播追踪头给 python 应用。

第 3 步:数据的收集与聚合 (Collection & Aggregation)

  1. Span 的生成: 在整个请求 - 响应链路中,只有 front-envoy 是活跃的追踪数据生成者。它会在以下几个关键节点生成 Span:
    • 入口 Span: 接收到外部请求,准备发往 service1 时。
    • 出口 Span: 当 service1 调用 service2 的请求流经它时。
    • 以及所有响应返回时,它会记录每个环节的耗时、HTTP 状态码等信息,并完成对应的 Span。
  2. 异步上报: front-envoy 不会阻塞请求去上报追踪数据。它会将这些完成的 Span 放入一个缓冲区,然后通过一个独立的后台线程,异步地将它们发送到 jaeger 集群(由 collector_cluster: jaeger 定义)。
  3. Jaeger Collector: Jaeger 的 Collector 组件在 9411 端口接收这些符合 Zipkin 格式的 Span 数据。
  4. 聚合: Collector 会根据所有 Span 中相同的 Trace ID 将它们归属到同一次请求链路中。然后,它利用每个 Span 的 Span IDParent Span ID 字段,像拼图一样将它们组织成一个有父子关系的树状结构(Trace)。

第 4 步:可视化展示 (Visualization)

  1. 查询: 当你在浏览器中打开 Jaeger UI 并查询 front-proxy 服务时,Jaeger Query 组件会从存储中检索出完整的 Trace 数据。
  2. 渲染: Jaeger UI 将这个树状结构的 Trace 数据渲染成一个直观的火焰图(Flame Graph)或甘特图。
    • 你可以看到根 Span 是 front-envoy 的入口操作。
    • 它的子 Span 是对 service1 的调用。
    • service1 的子 Span 是对 service2 的调用。
    • 每个 Span 的长度代表了其耗时,可以轻松发现链路中的性能瓶颈。
    • 点击每个 Span,可以看到由 Envoy 附加的详细标签(Tags),如 http.method, http.status_code, 以及由 decorator.operation 定义的业务操作名 checkStock

通过这个完整的生命周期,Envoy 和 Jaeger 协作,提供了一个对应用代码几乎无侵入的、强大而清晰的分布式追踪解决方案。

8. 总结

本文通过详细解析 Envoy 官方的 Jaeger Tracing 示例,深入探讨了分布式追踪的实现原理和架构设计。我们从架构概览开始,了解了如何通过 docker-compose 启动一个包含多个容器的微服务环境,并通过 Envoy 代理实现追踪上下文的传播。接着,我们分析了各个配置文件的功能,特别是如何通过 Envoy 的路由和追踪配置来实现服务间的追踪信息传递。最后,我们详细描述了一个请求的完整生命周期,从追踪的诞生、上下文的传播,到数据的收集与聚合,直至可视化展示,全面展示了 Envoy 和 Jaeger 在分布式追踪中的强大协作能力。

这种设计不仅有助于理解分布式追踪的实现,还为深入学习如 Istio 等服务网格技术奠定了基础。通过这个示例,读者可以更好地掌握服务网格中的追踪上下文传播与数据可视化,为实际应用中的可观测性打下坚实的基础。

如果你对分布式追踪、服务网格或 Envoy 有任何疑问,欢迎在评论区留言讨论。希望本文能为你的学习和实践提供帮助!

9. 参考资料

文章导航

评论区