使用OpenTelemetry、Spring Cloud Sleuth、Kafka和Jaeger实现分布式跟踪


分布式跟踪可让您深入了解特定服务在分布式软件系统中作为整体的一部分是如何执行的。它跟踪和记录从起点到目的地的请求以及它们经过的系统。
在本文中,我们将使用 OpenTelemetry、Spring Cloud Sleuth、Kafka 和 Jaeger 在三个 Spring Boot 微服务中实现分布式跟踪。
我们先来看看分布式追踪中的一些基本术语。
跨度:表示系统内的单个工作单元。跨度可以相互嵌套以模拟工作的分解。例如,一个跨度可能正在调用一个 REST 端点,然后另一个子跨度可能是该端点调用另一个,等等在不同的服务中。
Trace:所有共享相同根跨度的跨度集合,或者更简单地说,将所有跨度创建为原始请求的直接结果。跨度的层次结构(每个跨度在根跨度旁边都有自己的父跨度)可用于形成有向无环图,显示请求在通过各种组件时的路径。

OpenTelemetry
OpenTelemetry,也简称为 OTel,是一个供应商中立的开源 Observability 框架,用于检测、生成、收集和导出遥测数据,例如跟踪指标日志。作为云原生计算基金会 (CNCF) 的孵化项目,OTel 旨在提供与供应商无关的统一库和 API 集——主要用于收集数据并将其传输到某处。OTel 正在成为生成和管理遥测数据的世界标准,并被广泛采用。

Spring Cloud Sleuth
Sleuth是一个由 Spring Cloud 团队管理和维护的项目,旨在将分布式跟踪功能集成到 Spring Boot 应用程序中。它作为一个典型Spring Starter的 . 以下是一些开箱即用的 Sleuth 工具:

  • 在 Spring MVC 控制器(REST 端点)收到的请求
  • 通过 Kafka 或 MQ 等消息传递技术的请求
  • 用RestTemplate,WebClient等发出的请求

Sleuth 添加了一个拦截器,以确保在请求中传递所有跟踪信息。每次调用时,都会创建一个新的 Span。它在收到响应后关闭。
Sleuth 能够跟踪您的请求和消息,以便您可以将该通信与相应的日志条目相关联。您还可以将跟踪信息导出到外部系统以可视化延迟。

Jaeger
Jaeger 最初由 Uber 的团队构建,然后于 2015 年开源。它于 2017 年被接受为云原生孵化项目,并于 2019 年毕业。作为 CNCF 的一部分,Jaeger 是云原生架构中公认的项目。它的源代码主要是用 Go 编写的。Jaeger 的架构包括:

  • Instrumentation Libraries
  • Collectors
  • 查询服务和网页界面
  • 数据库存储

与 Jaeger 类似,Zipkin 在其架构中也提供了相同的组件集。尽管 Zipkin 是一个较老的项目,但 Jaeger 具有更现代和可扩展的设计。对于此示例,我们选择 Jaeger 作为后端。

追踪系统设计
让我们设计三个 Spring Boot 微服务:

  • customer-service-bff:使用backend for frontend模式,此服务位于 UI 和后端之间。它由 UI Web 应用程序调用,而后者又通过 REST API 调用调用后端客户服务。
  • customer-service: 一个简单的客户 CRUD 服务。除了在 CRUD 操作时将数据持久化到其数据库之外,它还在创建、更新或删除客户记录时将事件发布到 Kafka。
  • order-service:监听 Kafka 主题,使用客户创建/更新/删除的事件。


这三个微服务旨在:
  • 通过 REST API (customer-service-bff和customer-service)进行通信
  • 通过 Kafka(customer-service和order-service)通过事件驱动的 pub/sub 进行通信

这是为了观察 OpenTelemetry 如何结合 Spring Cloud Sleuth 处理代码的自动检测以及生成和传输跟踪数据。上面的虚线捕获了微服务导出的跟踪数据的路径,通过OTLP(OpenTelemetry Protocol)传输到OpenTelemetry Collector,收集器依次处理并将跟踪数据导出到后端Jaeger进行存储和查询。

使用 monorepo,我们的项目结构如下:

第 1 步:添加 POM 依赖项
这是使用 OTel 和 Spring Cloud Sleuth 实现分布式跟踪的关键。我们的目标是不必手动检测我们的代码,因此我们依靠这些依赖项来完成它们设计的工作——自动检测我们的代码,除了跟踪实现、将遥测数据导出到 OTel 收集器等。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-otel-dependencies</artifactId>
            <version>${spring-cloud-sleuth-otel.version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-sleuth-brave</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-sleuth-otel-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp-trace</artifactId>
    </dependency>
</dependencies>

  • spring-cloud-dependencies: Spring Cloud 依赖
  • spring-cloud-sleuth-otel-dependencies: Spring Cloud Sleuth OpenTelemetry 依赖项
  • spring-cloud-starter-sleuth:Sleuth 通过模块中可用的桥与 OpenZipkin Brave 跟踪器集成spring-cloud-sleuth-brave。由于我们将在示例中使用 OpenTelemetry 跟踪器,因此我们spring-cloud-sleuth-brave从spring-cloud-starter-sleuth依赖项中排除,而是添加spring-cloud-sleuth-otel-autoconfigure依赖项。这将基于Brave的默认跟踪实现替换为基于 OpenTelemetry 的实现。
  • opentelemetry-exporter-otlp-trace:这是 Spring Cloud Sleuth OTel 中向 OpenTelemetry Collector 发送跟踪的组件。

第 2 步:OpenTelemetry 配置
OpenTelemetry 收集器端点
对于每个微服务,我们需要在其中添加以下配置application.yml(请参阅下面部分中的示例片段)。spring.sleuth.otel.exporter.otlp.endpoint主要是配置OTel Collector端点。它告诉导出器,在我们的例子中是 Sleuth,通过 OTLP 将跟踪数据发送到指定的收集器端点http://otel-collector:4317。注意otel-collector端点 URL 来自otel-collector图像的 docker-compose 服务。

跟踪数据概率抽样
spring.sleuth.otel.config.trace-id-ratio-based属性定义了跟踪数据的采样概率。它根据提供给采样器的分数对一部分迹线进行采样。概率抽样允许 OpenTelemetry 跟踪用户通过使用随机抽样技术降低跨度收集成本。如果该比率小于 1.0,则某些迹线将不会被导出。对于此示例,我们将采样配置为 1.0、100%。

有关其他 OTel Spring Cloud Sleuth 属性,请参阅常见应用程序属性。

spring:
  application:
    name: customer-service
  sleuth:
    otel:
      config:
        trace-id-ratio-based: 1.0
      exporter:
        otlp:
          endpoint: http://otel-collector:4317


OpenTelemetry 配置文件
我们需要项目根目录下的 OTel 配置文件otel-config.yaml。内容如下。此配置文件定义了 OTel 接收器、处理器和导出器的行为。正如我们所看到的,我们定义了我们的接收器来监听 gRPC 和 HTTP,处理器使用批处理和导出器作为 jaeger 和日志记录。

extensions:
  memory_ballast:
    size_mib: 512
  zpages:
    endpoint: 0.0.0.0:55679

receivers:
  otlp:
    protocols:
      grpc:
      http:

processors:
  batch:

exporters:
  logging:
    logLevel: debug
  jaeger:
    endpoint: jaeger-all-in-one:14250
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [ otlp ]
      processors: [ batch ]
      exporters: [ logging, jaeger ]
  extensions: [ memory_ballast, zpages ]

第 3 步:docker-compose 将所有内容串在一起
让我们看看我们需要启动哪些 docker 容器来运行这三个微服务并观察它们的分布式跟踪,前三个微服务在上面的部分中进行了解释。

  • customer-service-bff
  • customer-service
  • order-service
  • postgres-customer: 数据库customer-service
  • postgres-order: 数据库order-service
  • jaeger-all-in-one:运行所有 Jaeger 后端组件和 UI 的单个图像
  • otel-collector:OpenTelemetry 追踪引擎,接收、处理、导出追踪数据到后端
  • zookeeper:跟踪Kafka集群中节点的状态,维护Kafka主题和消息列表
  • kafka:发布/订阅事件流处理平台

services:

  customer-service-bff:
    image: customer-service-bff:0.0.1-SNAPSHOT
    ports:
      - "8080:8080"
    depends_on:
      - zookeeper
      - kafka

  customer-service:
    image: customer-service:0.0.1-SNAPSHOT
    ports:
      -
"8081:8081"
    depends_on:
      - zookeeper
      - kafka
      - postgres-customer
    environment:
      - SPRING_DATASOURCE_JDBC-URL=jdbc:postgresql:
//postgres-customer:5432/customerservice
      - SPRING_DATASOURCE_USERNAME=postgres
      - SPRING_DATASOURCE_PASSWORD=postgres
      - SPRING_JPA_HIBERNATE_DDL_AUTO=update

  order-service:
    image: order-service:0.0.1-SNAPSHOT
    ports:
      -
"8082:8082"
    depends_on:
      - zookeeper
      - kafka
      - postgres-order
    environment:
      - SPRING_DATASOURCE_JDBC-URL=jdbc:postgresql:
//postgres-order:5432/orderservice
      - SPRING_DATASOURCE_USERNAME=postgres
      - SPRING_DATASOURCE_PASSWORD=postgres
      - SPRING_JPA_HIBERNATE_DDL_AUTO=update

  postgres-customer:
    image: postgres
    ports:
      -
"5432:5432"
    environment:
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_DB=customerservice

  postgres-order:
    image: postgres
    ports:
      -
"5431:5431"
    environment:
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_DB=orderservice

  jaeger-all-in-one:
    image: jaegertracing/all-in-one:latest
    ports:
      -
"16686:16686"
      -
"14268"
      -
"14250"

  otel-collector:
    image: otel/opentelemetry-collector:0.47.0
    command: [
"--config=/etc/otel-collector-config.yaml" ]
    volumes:
      - ./otel-config.yaml:/etc/otel-collector-config.yaml
    ports:
      -
"1888:1888"   # pprof extension
      -
"13133:13133" # health_check extension
      -
"4317"        # OTLP gRPC receiver
      -
"55670:55679" # zpages extension
    depends_on:
      - jaeger-all-in-one

  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - 22181:2181

  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper
    ports:
      - 29092:29092
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT:
//kafka:9092,PLAINTEXT_HOST://localhost:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1


运行docker-compose up -d以调出所有九个容器:

第 4 步:追踪数据在行动
快乐之路
现在,让我们启动customer-service-bff流程的入口点,以创建新客户。

启动 Jaeger UI,http://localhost:16686/[url=https://link.zhihu.com/?target=http%3A//localhost%3A16686/%2C]按[/url]服务搜索customer-service-bff,单击Find Traces按钮,这是我们看到的创建客户跟踪:它跨越三个服务,总共跨越六个,持续时间 82.35 毫秒。

除了 Trace Timeline 视图(上面的屏幕截图),Jaeger 还提供了一个图形视图(Trace Graph在右上角的下拉菜单中选择):

三个微服务在 docker 中的日志输出显示相同的跟踪 id,以红色突出显示,并根据其应用程序名称显示不同的跨度 id(应用程序名称及其对应的跨度 id 以匹配的颜色突出显示)。在 的情况下customer-service,相同的 span id 从 REST API 请求传递到 Kafka 发布者请求。


错误场景
customer-service让我们在 docker 中暂停我们的PostgreSQL 数据库,然后重复从customer-service-bff. 500 internal server error正如预期的那样,我们得到了。检查 Jaeger,我们看到以下跟踪,异常堆栈跟踪抱怨SocketTimeoutException,再次如预期的那样。

识别长期运行的跨度
Jaeger UI 允许我们搜索超过指定最大持续时间的跟踪。例如,我们可以搜索所有耗时超过 1000 毫秒的跟踪。然后,我们可以深入研究长期运行的跟踪以调查其根本原因。

总结
在这个故事中,我们从 OpenTelemetry、Spring Cloud Sleuth 和 Jaeger 的角度解压了分布式跟踪,验证了 REST API 调用和 Kafka pub/sub 中分布式跟踪的自动检测。我希望这个故事能让你更好地理解这些跟踪框架和工具,尤其是 OpenTelemetry,以及它如何从根本上改变我们在分布式系统中进行可观察性的方式。
这个故事的源代码可以在我的 GitHub 存储库中找到。