SpringBoot3 WebFlux 可观测最佳实践

前言

链路追踪是可观测性软件系统的一个非常好的工具。它使开发人员能够了解应用程序中和应用程序之间不同交互发生的时间、地点和方式。同时让观测复杂的软件系统变得更加容易。

Spring Boot 3开始,Spring Boot 中用于链路追踪的旧 Spring Cloud Sleuth 解决方案将替换为新的 Micrometer Tracing 库。

您可能已经了解 Micrometer,因为它以前被用作公开独立于平台的指标和监控基于 JVM 的微服务(例如 Prometheus )的默认解决方案。最新产品通过独立于平台的链路追踪解决方案扩展了 Micrometer 生态系统。这使得开发人员能够使用一个通用 API 来检测其应用程序,并以不同的格式将其导出到 Jaeger、Zipkin 或 OpenTelemetry 等链路追踪收集器。

本文将介绍在响应式编程 Kotlin 中,如何在 Spring Boot 3 WebFlux 利用 Micrometer 进行链路追踪。

1. 微服务设置

接下来,我们将创建一个简单的 Spring Boot 微服务,它提供一个响应式 REST 端点,该端点在内部查询另一个第三方服务以获取一些信息。目标是导出两个操作的 trace。

我们将从以下 Spring Boot Initializr 项目开始,您可以在此处找到该项目。它包括带有Kotlin Gradle DSLSpring Boot 3.0.1Spring Web Reactive (WebFlux)和带有PrometheusSpring Actuator。以下代码主要使用Kotlin,但如果使用 Java 也是可以的,大多数方法都是相同的。

Spring 初始化模板 带有 Webflux、Spring Actuator 和 Prometheus 的 Spring Boot 3 Kotlin 模板

定义 endpoint

我们将首先添加一个带有测试 endpoint 的简单 REST 控制器类,该测试 endpoint 使用 Spring WebClient 调用外部 API 。我们正在使用 suspend 关键字来使用Kotlin的协程。这使我们能够在利用 Spring WebFlux 的响应式流的同时编写命令式代码。

在以下示例中,我们使用 Spring WebClient 调用外部 TODO-API,该 API 以 JSON 字符串形式返回 TODO 项。我们还将创建一条日志消息,其中稍后应包含一些链路追踪信息。

@RestController
class Controller {
  val log = LoggerFactory.getLogger(javaClass)
  
  val webClient = WebClient.builder()
    .baseUrl("https://jsonplaceholder.typicode.com")
    .build()
  
  @GetMapping("/test")
  suspend fun test(): String {
    // simulate some complex calculation  
    delay(1.seconds)
    
    log.info("test log with tracing info")
    
    // make web client call to external API
    val externalTodos = webClient.get()
      .uri("/todos/1")
      .retrieve()
      .bodyToMono(String::class.java)
      .awaitSingle()
    
    return externalTodos
  }
}

新增 Micrometer tracing

在下一步中,我们将把 Micrometer tracing 依赖项添加到我们的build.gradle.kts文件中。由于 Micrometer 支持不同的链路追踪格式和供应商,因此依赖项被分开,我们只导入我们需要的内容。为了保持所有依赖项同步,我们使用 Micrometer Tracing BOM(bom 清单)。此外,我们添加了核心依赖项和桥接器,以将 Micrometer Tracing 转换为 OpenTelemetry 格式(其他格式也可用)。

implementation(platform("io.micrometer:micrometer-tracing-bom:1.0.0"))
implementation("io.micrometer:micrometer-tracing")
implementation("io.micrometer:micrometer-tracing-bridge-otel")

我们还需要添加导出器依赖项来导出创建的 trace。在此示例中,我们将使用由 OpenTelemetry 维护并由 Micrometer Tracing 支持的 Zipkin 导出器。

implementation("io.opentelemetry:opentelemetry-exporter-zipkin")

配置

配置是设置链路追踪必不可少的一步,配置文件application.yaml位于src/main/resources目录下。

  • 首先,我们必须在管理设置中启用链路追踪。我们还将链路追踪采样率设置为 1(默认值为 0.1),以便为服务收到的每个调用创建链路追踪。在具有大量请求的生产系统中,您可能只想追踪一些调用链路。
  • 此外,我们可以定义希望 Zipkin 导出器发送链路追踪的端点 URL。
  • 最后,我们必须更新默认日志记录模式以包含链路追踪和 spanId。
management:
  tracing:
    enabled: true
    sampling.probability: 1.0

  zipkin.tracing.endpoint: http://localhost:9411/api/v2/spans

logging.pattern.level: "trace_id=%mdc{traceId} span_id=%mdc{spanId} trace_flags=%mdc{traceFlags} %p"

2. 测试

现在我们已经完成了服务设置,我们可以运行它了。如果启动应用程序,默认情况下,服务器应在端口下启动 8080。然后,可以通过打开浏览器来调用我们创建的端点http://localhost:8080/test。以下是请求响应内容:

{  "userId" :  1 ,  "id" :  1 ,  "title" :  "delectus aut autem" ,  "已完成" :  false  }

要查看调用端点时创建的实际链路追踪,我们需要收集并查看它们。在本教程中,我们将使用zipkin导出器将数据导出到 观测云。当然也可以使用其他系统,例如 Zipkin、Grafana Loki 或 Datadog。

现在您可以再次调用我们的 Spring Boot 服务的端点。之后,当您在 观测云 中搜索任何 tracing 时,您应该能够找到端点请求的链路追踪信息。

3. 问题

乍一看,一切似乎都运行良好。然而,我们有两个问题。

解决了部分 issue 问题,这些问题可以在 Micrometer Tracing 文档中找到。

日志缺少数据

如果我们查看应用程序日志,可以发现调用端点时发出的日志消息。

trace_id= span_id= trace_flags= INFO 43636 --- [DefaultExecutor] com.example.tracing.Controller           : test log with tracing info

正如你所看到的,trace_id 和 span_id没有设置。这是因为Micrometer Tracing还无法轻松处理响应式流中的链路追踪上下文。此外,响应式流的Kotlin协程包装器隐藏了链路追踪上下文。因此,我们必须推迟当前响应式流的上下文来获取链路追踪信息。实际上,这看起来如下所示:

 Mono.deferContextual { contextView ->
   ContextSnapshot.setThreadLocalsFrom(
     contextView,
     ObservationThreadLocalAccessor.KEY
   ).use {
     log.info("test log with tracing info")
     Mono.empty<String>()
   }
}.awaitSingleOrNull()

为了更符合应用性,我们可以将示例代码提取到一个单独的函数中。

@GetMapping("/test")
suspend fun test(): String {
  // ...
  observeCtx { log.info("test log with tracing info") }
  // ...
}

suspend inline fun observeCtx(crossinline f: () -> Unit) {
  Mono.deferContextual { contextView ->
    ContextSnapshot.setThreadLocalsFrom(
      contextView,
      ObservationThreadLocalAccessor.KEY
    ).use {
      f()
      Mono.empty<Unit>()
    }
  }.awaitSingleOrNull()
}

如果我们现在启动应用程序并调用我们的端点,我们应该能够trace_id在日志中看到。

trace_id=6c0053eba01199f194f5f76ff8d61917 span_id=967d591266756905 trace_flags= INFO 45139 --- [DefaultExecutor] com.example.tracing.Controller           : test log with tracing info

WebClient 调用没有产生追踪信息

第二个问题可以通过查看观测云中的 trace 来发现。它仅显示端点的父链路追踪,但不显示调用的子范围 WebClient。理论上,Spring WebClient 以及 RestTemplate 都是由 Micrometer 自动检测的。但是如果我们查看代码,就会发现我们正在使用静态构建器方法 WebClient。为了从 WebClient 获取自动链路追踪,我们需要使用 Spring 框架提供的构建器 bean。它可以通过我们类的构造函数注入Controller。

@RestController
class Controller(
  webClientBuilder: WebClient.Builder
) {

 val webClient = webClientBuilder // use injected builder
  .baseUrl("https://jsonplaceholder.typicode.com")
  .build()

 // ...

}

通过上面的代码调整后重新调用 endpoint,我们在观测云中可以看到WebClient的跨度。Micrometer Tracing 还将自动为包含trace_id. 例如,如果我们调用另一个带有链路追踪功能的微服务,它可以获取 ID 并向观测云发送附加信息。

4. 观测指南

Micrometer Tracing 在 Spring 中自动为我们做了很多事情。但是,有时我们可能希望向链路追踪范围添加特定信息或观察应用程序中非传入或传出调用的特定部分。

添加跨度标签

我们可以定义自定义标签并将其添加到当前观察中以增强链路追踪数据。要检索当前链路追踪,我们可以使用ObservationRegistry类的 bean 。与日志记录问题类似,我们必须使用包装函数来获取正确的上下文。

@GetMapping("/test")
suspend fun test(): String {

  observeCtx {
    val currentObservation = observationRegistry.currentObservation
    currentObservation?.highCardinalityKeyValue("test_key", "test sample value")
  }

  // ...
}

添加此代码后,我们可以在观测云中看到我们的自定义标签及其值。

自定义可观测

使用 Micrometer API 创建自定义可观测(跨度)通常很容易。但是,在使用响应式流和协程时,我们需要帮助上下文链路追踪。如果我们在端点处理程序中创建一个新的观测,它将被视为一个单独的链路追踪。为了使代码可重用,我们可以编写一个简单的包装函数来创建新的观测点。它的工作原理与我们之前创建的用于使用 trace_id 。

suspend fun runObserved(
  name: String, 
  observationRegistry: ObservationRegistry,
  f: suspend () -> Unit
) {
  Mono.deferContextual { contextView ->
    ContextSnapshot.setThreadLocalsFrom(
      contextView,
      ObservationThreadLocalAccessor.KEY
    ).use {
      val observation = Observation.start(name, observationRegistry)
      Mono.just(observation).flatMap {
        mono { f() }
      }.doOnError {
        observation.error(it)
        observation.stop()
      }.doOnSuccess {
        observation.stop()
      }
    }
  }.awaitSingleOrNull()
}

该函数可以将任何挂起函数包装在新的观察周围。一旦执行了给定的函数,它将自动停止观测。此外,我们将追踪可能发生的任何错误并将其附加到链路追踪中。

我们现在可以应用这个函数来观察任何代码,例如函数的执行delay

@GetMapping("/test")
suspend fun test(): String {

  runObserved("delay", observationRegistry) {
    delay(1.seconds)
  }

  // ....
}

将此代码添加到端点处理程序后,观测云将向我们显示该操作的自定义范围。

5. 数据库链路追踪

典型的 Spring Boot 应用程序通常会连接到实际应用程序中的数据库。要利用响应式技术栈,建议使用 R2DBC API 而不是 JDBC 。

由于Micrometer Tracing是一项相当新的技术,目前还没有可用的自动追踪。然而,Spring 团队正在研究创建自动配置。实验存储库可以在这里找到。

当前项目,需将添加以下依赖项到build.gradle.kts. 为了方便测试,我们不会使用真实的数据库,而是使用 H2 内存数据库。

 implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
 runtimeOnly("com.h2database:h2")
 runtimeOnly("io.r2dbc:r2dbc-h2")

 // R2DBC micrometer auto tracing
 implementation("org.springframework.experimental:r2dbc-micrometer-spring-boot:1.0.2")

Kotlin代码中,添加了一个带有协程支持的简单 CRUD 存储库。如下所示:

@Table("todo")
data class ToDo(
  @Id
  val id: Long = 0,
  val title: String,
)

interface ToDoRepository : CoroutineCrudRepository<ToDo, Long>


@RestController
class Controller(
  val todoRepo: ToDoRepository,
  // ...
) {

  @GetMapping("/test")
  suspend fun test(): String {
    // ...
    // save
    val entry = ToDo(0,"Springboot3 + WebFlux + Kotlin ")
    todoRepo.save(entry)
    // Sample traced DB call
    val dbtodos = todoRepo.findAll().toList()
    
    // ...

    return "${dbtodos.size} $externalTodos"
  }
}

调用我们的 endpoint 将会再添加一个跨度。新的跨度名为query,包含多个标签,包括Spring Data R2DBC 执行的 SQL 查询。

结论

Micrometer 和新的链路追踪扩展统一了Spring Boot 3及以上版本的可观测性技术栈。为不同公司及其技术栈使用的不同链路追踪解决方案提供了很好的抽象。因此,它简化了我们开发人员的工作。

在 Spring WebFlux 的响应式编程方面,仍然有一些改进的潜力,尤其是 Kotlin。Micrometer 团队正在与Project Reactor (Spring WebFlux 使用的响应式库)背后的团队进行积极会谈,以简化响应式技术栈的 Micrometer Tracing 的使用。

参考资源

kotlin-spring-boot-tracing-example

micrometer-metrics

micrometer tracing

r2dbc-micrometer-spring-boot

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/309399.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

08、Kafka ------ 消息存储相关的配置-->消息过期时间设置、查看主题下的消息存活时间等配置

目录 消息存储相关的配置★ 消息的存储介绍★ 消息过期时间及处理方式演示&#xff1a;log.cleanup.policy 属性配置 ★ 修改指定主题的消息保存时间演示&#xff1a;将 test2 主题下的消息的保存时间设为10个小时1、先查看test2主题下的配置2、然后设置消息的保存时间3、然后再…

JavaScript基础课程

JavaScript 基础 - 第1天 了解变量、数据类型、运算符等基础概念&#xff0c;能够实现数据类型的转换&#xff0c;结合四则运算体会如何编程。 体会现实世界中的事物与计算机的关系 理解什么是数据并知道数据的分类 理解变量存储数据的“容器” 掌握常见运算符的使用&#x…

SpringSecurity集成JWT实现后端认证授权保姆级教程-授权配置篇

&#x1f341; 作者&#xff1a;知识浅谈&#xff0c;CSDN签约讲师&#xff0c;CSDN博客专家&#xff0c;华为云云享专家&#xff0c;阿里云专家博主 &#x1f4cc; 擅长领域&#xff1a;全栈工程师、爬虫、ACM算法 &#x1f492; 公众号&#xff1a;知识浅谈 &#x1f525;网站…

【Spring Boot】SpringMVC入门

1.什么是springMVC MVC就是把一个项目分成了三部分&#xff1a; MVC是一种思想。Spring进行了实现,称为Spring MVC。SpringBoot是创建SpringMVC项目的一种方式而已。springMVC对于MVC做出了一些改变&#xff1a; 当前阶段,MVC的概念又发生了一些变化,后端开发人员不涉及前端页…

【JaveWeb教程】(18) MySQL数据库开发之 MySQL数据库设计-DDL 如何查询、创建、使用、删除数据库数据表 详细代码示例讲解

目录 2. 数据库设计-DDL2.1 项目开发流程2.2 数据库操作2.2.1 查询数据库2.2.2 创建数据库2.2.3 使用数据库2.2.4 删除数据库 2.3 图形化工具2.3.1 介绍2.3.2 安装2.3.3 使用2.2.3.1 连接数据库2.2.3.2 操作数据库 2.3 表操作2.3.1 创建2.3.1.1 语法2.3.1.2 约束2.3.1.3 数据类…

从0到1实现html文件转换为markdown文档(进度0.1)

Spider-Man 前言准备环境1、node.js2、git 执行指令顺序报错及其解决方案一、npm 错误&#xff01;可以在以下位置找到此运行的完整日志解决方案 二、没有修改权限解决方案&#xff1a; 注意事项总结 前言 当我们处理文档时&#xff0c;常常会遇到将HTML文档转换为Markdown文档…

微信小程序:发送小程序订阅消息

文档&#xff1a;小程序订阅消息&#xff08;用户通过弹窗订阅&#xff09;开发指南 目录 步骤一&#xff1a;获取模板 ID步骤二&#xff1a;小程序端获取参数2.1、获取消息下发权限2.2、获取登录凭证&#xff08;code&#xff09; 步骤三&#xff1a;后端调用接口下发订阅消息…

从生活入手学编程(1):Edge浏览器设置自动刷新专业教程

一、前言 我们都知道&#xff0c;Edge浏览器运行时的速度卡的实在是感人…… 于是今天&#xff0c;我就突发奇想&#xff0c;来看一看怎么刷新并且还能保留页面内容。 二、探索 首先&#xff0c;我在此提醒您&#xff0c;在使用这种方法时要非常小心。因为更改网页源代…

一级倒立摆控制 - 非线性 MPC 控制及 MATLAB 实现

系列文章目录 前言 本示例使用非线性模型预测控制器对象和块实现对小车上倒立摆的摆动和平衡控制。 本示例需要 Optimization Toolbox™ 软件为非线性 MPC 提供默认的非线性编程求解器&#xff0c;以计算每个控制间隔的最优控制动作。 一、摆锤/小车装配 本例中的被控对象是…

[笔记]学习做微信小程序

学习视频&#xff1a;前端微信小程序开发教程 本篇文章 只对关键内容笔记&#xff0c;用于自用。 这里写目录标题 注册、下载、安装我的小程序ID&#xff1a;wxe1fbd6939d8797d8我的小游戏ID&#xff1a;wx8b2c3e47ac9127b7开发者工具外观代理设置 创建第一个小程序主界面5个组…

Redis:原理速成+项目实战——Redis实战8(基于Redis的分布式锁及优化)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理速成项目实战——Redis实战7&#xff08;优惠券秒杀细节解决超卖、一人一单问题&#xff09; &#x1f4d…

C语言数组基础知识

目录 一维数组&#xff1a; 一维数组的创建&#xff1a; 一维数组的访问&#xff1a; 一维数组在内存中的存储&#xff1a; 二维数组&#xff1a; 二维数组的创建&#xff1a; 二维数组的初始化&#xff1a; 二维数组的使用&#xff1a; 二维数组在内存中的存储&#x…

UE5 将类修改目录

有个需求&#xff0c;需要修改ue里面类的位置&#xff0c;默认在Public类下面&#xff0c;我想创建一个二级目录&#xff0c;将所有的类分好位置&#xff0c;方便查看。 上图为创建一个类所在的默认位置。 接下来&#xff0c;将其移动到一个新的目录中。 首先在资源管理器中找…

kubeSphere DevOps自定义容器环境JDK11

kubeSphere DevOps自定义容器环境JDK11 &#x1f342;前言&#x1f342;增加JDK11容器环境&#x1f341;检查是否成功 &#x1f342;不生效的原因排查&#x1f341;按步骤执行如下命令 &#x1f342;前言 kubeSphere 版本v3.1.1 遇到问题:kubeSphere默认支持容器只有JDK8,目前…

[足式机器人]Part3 机构运动学与动力学分析与建模 Ch00-3(1) 刚体的位形 Configuration of Rigid Body

本文仅供学习使用&#xff0c;总结很多本现有讲述运动学或动力学书籍后的总结&#xff0c;从矢量的角度进行分析&#xff0c;方法比较传统&#xff0c;但更易理解&#xff0c;并且现有的看似抽象方法&#xff0c;两者本质上并无不同。 2024年底本人学位论文发表后方可摘抄 若有…

接口测试管理续集

今天应大家需要&#xff0c;接着谈app端数据返回层面的用例设计方法。第二部分给大家安利一个“接口管理平台”&#xff0c;以帮助大家解决接口文档维护、接口测试数据Mock、接口自动化测试等问题。希望对小伙伴们有用。 言归正传&#xff0c;进入今天的话题。 一、用例设计 …

【大数据OLAP引擎】StartRocks存算分离

存算分离的原因 降低存储成本&#xff1a;同样的存储大小对象存储价格只有SSD的1/10&#xff0c;所以号称存储成本降低80%不是吹的。 存算一体到存算分离 存算一体 作为 MPP 数据库的典型代表&#xff0c;StarRocks 3.0 版本之前使用存算一体 (shared-nothing) 架构&#xf…

Flink任务实战优化

前言&#xff1a;一个好产品&#xff0c;功能应该尽量包装在服务内部&#xff1b;对于Flink而言&#xff0c;无疑是做到了这一点。但是用户在使用Flink的时候&#xff0c;依然可以从版本的选择、代码逻辑、资源参数、业务的数据情况等方面做任务级的定制化优化&#xff1b;用最…

OpenMv颜色识别

本文旨在分享OpenMv实现数字识别并通过串口打印出来的工程源码。如果大家想将识别的结果传给单片机&#xff0c;即OpenMv与单片机之间的通信&#xff0c;可以参考以下文章&#xff1a; OpenMV与STM32之间的通信&#xff08;附源码&#xff09;_openmv与stm32串口-CSDN博客 ​​…

webpack学习笔记

为什么要使用Webpack webpack是一个用于现代JavaScript应用程序的静态模块打包工具。在webpack里一切文件皆模块&#xff0c;通过loader转换文件&#xff0c;通过plugin注入钩子&#xff0c;最后输出由多个模块组合成的文件&#xff0c;webpack专注构建模块化项目。 webPack可以…