OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的案例

opentelemetry,agent,spring,boot,spi · 浏览次数 : 0

小编点评

本文主要讨论了在 JDK21 环境中使用 Spring Boot 配合 JDK18 新增的 SPI(java.net.spi.InetAddressResolverProvider)时遇到的一个不生效问题。为了解决这个问题,首先需要了解 InetAddressResolverProvider 的作用和原理,然后分析了在 JDK18 和 Spring Boot 3.x 环境下如何配置和使用这个 SPI。最后,通过调试和使用相关工具,找到了问题所在,并提出了解决方案。 1. **InetAddressResolverProvider 的作用**:InetAddressResolverProvider 是 JDK18 之后才提供的,它允许自定义实现 InetAdressResolver 接口,以便在请求某个域名时返回预期的 IP 地址。它是一个SPI(服务提供者接口),可以通过编写一个第三方包让项目的依赖加入后,在发起网络请求时按照预期处理 IP 地址。 2. **使用场景与前置条件**:在 JDK18 及更高版本的系统上,可以使用自定义的 InetAddressResolverProvider 来实现域名解析,以满足特定需求。但在 Spring Boot 3.x 环境下,需要额外添加 OpenTelemetry Java Agent 并配合使用 -javaagent 参数,以便正确加载自定义的 InetAddressResolverProvider。如果不满足这些前置条件,自定义的 SPI 无法正常工作。 3. **问题复现与排查**:作者通过实践,复现了这个不生效的问题。通过调试和日志分析,发现是因为在启动 Spring Boot 应用并加载 OpenTelemetry Java Agent 之后,ServiceLoader 加载器无法加载到正确配置的 InetAddressResolverProvider 文件。这是因为在 Spring Boot 格式的 JAR 包中,BOOT-INF 目录被单独拆分出来,而类加载器默认只能加载 Maven 编译出来的 JAR 包中的资源。因此,OpenTelemetry Java Agent 无法读取到在 springboot 格式下配置的 InetAddressResolverProvider 文件。 4. **解决方案**:在排查过程中,作者禁用了 OpenTelemetry agent 中的一个资源,解决了这个问题。禁用资源后,自定义的 InetAddressResolverProvider 可以正常被 SpEL 表达式加载。 总的来说,本文通过实践和调试,找到了在使用 Spring Boot 3.x 环境下配合 JDK18 的 InetAddressResolverProvider SPI 时遇到的不生效问题的原因,并提供了相应的解决方案。

正文

背景

前段时间公司领导让我排查一个关于在 JDK21 环境中使用 Spring Boot 配合一个 JDK18 新增的一个 SPI(java.net.spi.InetAddressResolverProvider) 不生效的问题。

但这个不生效的前置条件有点多:

  • JDK 的版本得在 18+
  • SpringBoot3.x
  • 还在额外再配合使用 -javaagent:opentelemetry-javaagent.jar 使用,也就是 OpenTelemetry 提供的 agent。

才会导致自定义的 InetAddressResolverProvider 无法正常工作。


在复现这个问题之前先简单介绍下 java.net.spi.InetAddressResolverProvider 这个 SPI;它是在 JDK18 之后才提供的,在这之前我们使用 InetAddress 的内置解析器来解析主机名和 IP 地址,但这个解析器之前是不可以自定义的。

在某些场景下会不太方便,比如我们需要请求 order.service 这个域名时希望可以请求到某一个具体 IP 地址上,我们可以自己配置 host ,或者使用服务发现机制来实现。

但现在通过 InetAddressResolverProvider 就可以定义在请求这个域名的时候返回一个我们预期的 IP 地址。

同时由于它是一个 SPI,所以我们只需要编写一个第三方包,任何项目依赖它之后在发起网络请求时都会按照我们预期的 IP 进行请求。

复现

要使用它也很简单,主要是两个类:

  • InetAddressResolverProvider:这是一个抽象类,我们可以继承它之后重写它的 get 函数返回一个 InetAddressResolver 对象
  • InetAddressResolver:一个接口,主要提供了两个函数;一个用于传入域名返回 IP 地址,另一个反之:传入 IP 地址返回域名。

public class MyAddressResolverProvider extends InetAddressResolverProvider {
    @Override
    public InetAddressResolver get(Configuration configuration) {
        return new MyAddressResolver();
    }
    @Override
    public String name() {
        return "MyAddressResolverProvider Internet Address Resolver Provider";
    }
}

public class MyAddressResolver implements InetAddressResolver {

    public MyAddressResolver() {
        System.out.println("=====MyAddressResolver");
    }

    @Override
    public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy)
            throws UnknownHostException {
        if (host.equals("fedora")) {
            return Stream.of(InetAddress.getByAddress(new byte[] {127, 127, 10, 1}));
        }
        return Stream.of(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}));
    }
    @Override
    public String lookupByAddress(byte[] addr) {
        System.out.println("++++++" + addr[0] + " " + addr[1] + " " + addr[2] + " " + addr[3]);
        return  "fedora";
    }
}

---

```java
addresses = InetAddress.getAllByName("fedora");
// output: 127 127 10 1

这里我简单实现了一个对域名 fedora 的解析,会直接返回 127.127.10.1

如果使用 IP 地址进行查询时:

InetAddress byAddress = InetAddress.getByAddress(new byte[]{127, 127, 10, 1});

System.out.println("+++++" + byAddress.getHostName());
// output: fedora

当然要要使得这个 SPI 生效的前提条件是我们需要新建一个文件:
META-INF/services/java.net.spi.InetAddressResolverProvider
里面的内容是我们自定义类的全限定名称:

com.example.demo.MyAddressResolverProvider

这样一个完整的 SPI 就实现完成了。


正常情况下我们将应用打包为一个 jar 之后运行:

java -jar target/demo-0.0.1-SNAPSHOT.jar

是可以看到输出结果是符合预期的。

一旦我们使用配合上 spring boot 打包之后,也就是加上以下的依赖:

<parent>  
  <groupId>org.springframework.boot</groupId>  
  <artifactId>spring-boot-starter-parent</artifactId>  
  <version>3.2.3</version>  
  <relativePath/> <!-- lookup parent from repository -->  
</parent>

<build>  
  <plugins>  
   <plugin>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-maven-plugin</artifactId>  
   </plugin>  
  </plugins>  
</build>

再次执行其实也没啥问题,也能按照预期输出结果。

但我们加上 OpenTelemetry 的 agent 时:

java  -javaagent:opentelemetry-javaagent.jar \
      -jar target/demo-0.0.1-SNAPSHOT.jar

就会发现在执行解析的时候抛出了 java.net.UnknownHostException异常。


从结果来看就是没有进入我们自定义的解析器。

SPI 原理

在讲排查过程之前还是要先预习下关于 Java SPI 的原理以及应用场景。

以前写过一个 http 框架 cicada,其中有一个可拔插 IOC 容器的功能:

就是可以自定义实现自己的 IOC 容器,将自己实现的 IOC 容器打包为一个第三方包加入到依赖中,cicada 框架就会自动使用自定义的 IOC 实现。

要实现这个功能本质上就是要定义一个接口,然后根据依赖的不同实现创建接口的实例对象。

public interface CicadaBeanFactory {

    /**
     * Register into bean Factory
     * @param object
     */
    void register(Object object);

    /**
     * Get bean from bean Factory
     * @param name
     * @return
     * @throws Exception
     */
    Object getBean(String name) throws Exception;

    /**
     * get bean by class type
     * @param clazz
     * @param <T>
     * @return bean
     * @throws Exception
     */
    <T> T getBean(Class<T> clazz) throws Exception;

    /**
     * release all beans
     */
    void releaseBean() ;
}

获取具体的示例代码时就只需要使用 JDK 内置的 ServiceLoader 进行加载即可:

public static CicadaBeanFactory getCicadaBeanFactory() {  
    ServiceLoader<CicadaBeanFactory> cicadaBeanFactories = ServiceLoader.load(CicadaBeanFactory.class);  
    if (cicadaBeanFactories.iterator().hasNext()){  
        return cicadaBeanFactories.iterator().next() ;  
    }  
    return new CicadaDefaultBean();  
}

代码也非常的简洁,和刚才提到的 InetAddressResolverProvider 一样我们需要新增一个 META-INF/services/top.crossoverjie.cicada.base.bean.CicadaBeanFactory 文件来配置我们的类名称。

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
        	// PREFIX = META-INF/services/
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

在 ServiceLoader 类中会会去查找 META-INF/services 的文件,然后解析其中的内容从而反射生成对应的接口对象。

这里还有一个关键是通常我们的代码都会打包为一个 JAR 包,类加载器需要加载这个 JAR 包,同时需要在这个 JAR 包里找到我们之前定义的那个 spi 文件,如果这里查不到文件那就认为没有定义 SPI。

这个是本次问题的重点,会在后文分析原因的时候用到。

排查

因为问题就出现在是否使用 opentelemetry-javaagent.jar 上,所以我需要知道在使用了 agent 之后有什么区别。

从刚才的对 SPI 的原理分析,加上 agent 出现异常,说明理论上就是没有读取到我们配置的文件: java.net.spi.InetAddressResolverProvider

于是我便开始 debug,在 ServiceLoader 加载 jar 包的时候是可以看到具体使用的是什么 classLoader

这是不配置 agent 的时候使用的 classLoader:

使用这个 loader 是可以通过文件路径在 jar 包中查找到我们配置的文件。

而配置上 agent 之后使用的 classLoader:

却是一个 JarLoader,这样是无法加载到在 springboot 格式下的配置文件的,至于为什么加载不到,那就要提一下 maven 打包后的文件目录和 spring boot 打包后的文件目录的区别了。


这里我截图了同样的一份代码不同的打包方式:
上面的是传统 maven,下图是 spring boot;其实主要的区别就是在 pom 中使用了一个构建插件:

<build>  
  <plugins>  
   <plugin>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-maven-plugin</artifactId>  
   </plugin>  
  </plugins>  
</build>

或者使用 spring-boot 命令再次打包的效果也是一样的。

会发现 spring boot 打包后会多出一层 BOOT-INF 的文件夹,然后会在 MANIFIST.MF 文件中定义 Main-ClassStart-Class.


通过上面的 debug 其实会发现 JarLoader 只能在加载 maven 打包后的文件,也就是说无法识别 BOOT-INF 这个目录。

正常情况下 spring boot 中会有一个额外的 java.nio.file.spi.FileSystemProvider 实现:

通过这个类的实现可以直接从 JAR 包中加载资源,比如我们自定义的 SPI 资源等。

初步判断使用 opentelemetry-javaagent.jar的 agent 之后,它的类加载器优先于了 spring boot ,从而导致后续的加载失败。

远程 debug

这里穿插几个 debug 小技巧,其中一个是远程 debug,因为这里我是需要调试 javaagent,正常情况下是无法直接 debug 的。

所以我们可以使用以下命令启动应用:

java -agentlib:jdwp="transport=dt_socket,server=y,suspend=y,address=5000" -javaagent:opentelemetry-javaagent.jar \
      -jar target/demo-0.0.1-SNAPSHOT.jar

然后在 idea 中配置一个 remote 启动。

注意这里的端口得和命令行中的保持一致。

当应用启动之后便可以在 idea 中启动这个 remote 了,这样便可以正常 debug 了。

条件断点

第二个是条件断点也非常有用,有时候我们需要调试一个公共函数,调用的地方非常多。

而我们只需要关心某一类行为的调用,此时就可以对这个函数中的变量进行判断,当他们满足某些条件时再进入断点,这样可以极大的提高我们的调试效率:

配置也很简单,只需要在断点上右键就可以编辑条件了。

社区咨询

虽然我根据现象初步可以猜测下原因,但依然不确定如何调整才能解决这个问题,于是便去社区提了一个 issue


最后在社区大佬的帮助下发现我们需要禁用掉 OpenTelemetry agent 中的一个 resource 就可以了。


这个 resource 是由 agent 触发的,它优先于 spring boot 之前进行 SPI 的加载。
目的是为了给 metric 和 trace 新增两个属性:


加载的核心代码在这里,只要禁用掉之后就不会再加载了。

禁用前:

禁用后:

当我们禁用掉之后就不会存在这两个属性了,不过我们目前并没有使用这两个属性,所以为了使得 SPI 生效就只有先禁用掉了,后续再看看社区还有没有其他的方案。

想要复现 debug 的可以在这里尝试:
https://github.com/crossoverJie/demo

参考连接:

与OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的案例相似的内容:

OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的案例

背景 前段时间公司领导让我排查一个关于在 JDK21 环境中使用 Spring Boot 配合一个 JDK18 新增的一个 SPI(java.net.spi.InetAddressResolverProvider) 不生效的问题。 但这个不生效的前置条件有点多: JDK 的版本得在 18+ Spri

OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的

背景 前段时间公司领导让我排查一个关于在 JDK21 环境中使用 Spring Boot 配合一个 JDK18 新增的一个 SPI(java.net.spi.InetAddressResolverProvider) 不生效的问题。 但这个不生效的前置条件有点多: JDK 的版本得在 18+ Spri

OpenTelemetry 深度定制:跨服务追踪的实战技巧

背景 在上一篇《从 Dapper 到 OpenTelemetry:分布式追踪的演进之旅》中在最后提到在做一些 Trace 的定制开发。 到现在差不多算是完成了,可以和大家分享一下。 我们的需求是这样的: 假设现在有三个服务:ServiceA、ServiceB、ServiceC ServiceA 对外

OpenTelemetry 实践指南:历史、架构与基本概念

背景 之前陆续写过一些和 OpenTelemetry 相关的文章: 实战:如何优雅的从 Skywalking 切换到 OpenTelemetry 实战:如何编写一个 OpenTelemetry Extensions 从一个 JDK21+OpenTelemetry 不兼容的问题讲起 这些内容的前提是最

.NET程序对接 OpenTelemetry logs

OpenTelemetry 简介 OpenTelemetry 是一个由 CNCF(Cloud Native Computing Foundation)托管的开源项目,旨在为观察性(Observability)提供一套全面的工具,包括度量(Metrics)、日志(Logs)和追踪(Traces)。它的

AgileConfig-1.9.4 发布,支持 OpenTelemetry

Hello 大家好,最新版的 AgileConfig 1.9.4 发布了。现在它可以通过 OpenTelemetry 对外提供 logs,traces,metrics 三个维度的数据。用户可以自由选择支持 otlp 协议的工具来进行查询与分析。比如 Seq,loki,prometheus, graf

.NET 使用 OpenTelemetry metrics 监控应用程序指标

上一次我们讲了 OpenTelemetry Logs 与 OpenTelemetry Traces。今天继续来说说 OpenTelemetry Metrics。 随着现代应用程序的复杂性不断增加,对于性能监控和故障排除的需求也日益迫切。在 .NET 生态系统中,OpenTelemetry Metri

.NET 中使用 OpenTelemetry Traces 追踪应用程序

上一次我们讲了 OpenTelemetry Logs。今天继续来说说 OpenTelemetry Traces。 在今天的微服务和云原生环境中,理解和监控系统的行为变得越来越重要。在当下我们实现一个功能可能需要调用了 N 个方法,涉及到 N 个服务。方法之间的调用如蜘蛛网一样。分布式追踪这个时候就至

使用 OpenTelemetry 构建 .NET 应用可观测性(1):什么是可观测性

[TOC] # 什么是系统的可观测性(Observability) 对软件行业来说,可观测性(Observability)是一个舶来词,出自控制论(Control Theory)。 **可观测性是系统的一个属性**,它是指系统的状态能否被观测,也就是说,系统的状态能否被监控、收集、分析、查询、可视化

使用 OpenTelemetry 构建 .NET 应用可观测性(2):OpenTelemetry 项目简介

[TOC] # 前世今生 ## OpenTracing OpenTracing 项目启动于 2016 年,旨在提供一套分布式追踪标准,以便开发人员可以更轻松地实现分布式追踪。 OpenTracing 定义了一套 Tracing 模型,以及一套 API,用于在应用程序中创建和管理这些数据模型。 下面是