OpenTelemetry agent 对 Spring Boot 应用的影响:一次 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 {@Overridepublic InetAddressResolver get(Configuration configuration) {return new MyAddressResolver();}@Overridepublic String name() {return "MyAddressResolverProvider Internet Address Resolver Provider";}
}public class MyAddressResolver implements InetAddressResolver {public MyAddressResolver() {System.out.println("=====MyAddressResolver");}@Overridepublic 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}));}@Overridepublic 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异常。

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

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);elseconfigs = 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:401e2b0939d047b9f49f1b1d0108c086.png使用这个 loader 是可以通过文件路径在 jar 包中查找到我们配置的文件。

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

220e4c437c33dc2893c4290c44e3e50e.png这里我截图了同样的一份代码不同的打包方式:上面的是传统 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 实现:6319c8d8d881a05329814c9c66a45ca3.png通过这个类的实现可以直接从 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
5b135a3f83278606e8ba68d0e1567d2e.png

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

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

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

条件断点

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

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

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

社区咨询

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

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

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

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

禁用前:eae555f8d68cf3570f8a911843263e16.png

禁用后:6f34198e22dd02bcf6ee83d23c8298c0.png

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

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

参考连接:

  • https://github.com/TogetherOS/cicada

  • https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/#packaging.repackage-goal

  • https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10921

  • https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/resources/library/README.md#host

往期推荐

自动化测试在 Kubernetes Operator 开发中的应用:以 OpenTelemetry

深入剖析:如何使用Pulsar和Arthas高效排查消息队列延迟问题

日志架构演进:从集中式到分布式的Kubernetes日志策略

实战:如何编写一个 OpenTelemetry Extensions

2024年的云原生架构需要哪些技术栈

8eb3a07db7a43035fd718b4580a3cf27.gif

点分享

f53818b4b6960cb9073e8b87c9ad720e.gif

点收藏

0bbe2b75f9e8e081a2b490648048fa70.gif

点点赞

7404d157597111e8a8f7d29e575e66de.gif

点在看

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

僵尸网络的威胁值得关注

僵尸网络&#xff08;botnet&#xff09;是指一组受到恶意软件感染并遭到恶意用户控制的计算机。术语“僵尸网络”由“机器人&#xff08;bot&#xff09;”和“网络&#xff08;network&#xff09;”两个词组合而成&#xff0c;每台受感染设备被称为“机器人”。僵尸网络可用…

验证集的划分方法:确保机器学习模型泛化能力的关键

验证集的划分方法&#xff1a;确保机器学习模型泛化能力的关键 目录 一、验证集的作用 二、验证集的划分方法 三、注意事项 四、总结 在机器学习任务中&#xff0c;我们不仅要关注模型在训练数据上的表现&#xff0c;更重要的是模型在未见数据上的泛化能力。为了评估和提高…

线上虚拟展厅需要具备哪些技术特点?

虚拟展厅需要具备三维建模与渲染技术、虚拟现实技术、交互技术、多媒体展示技术、网络传输技术、大数据分析与反馈技术、跨平台兼容性等技术特点。这些技术特点共同构成了虚拟展厅的核心竞争力&#xff0c;使其能够为用户提供逼真、生动、互动的参观体验。 虚拟展厅的技术特点主…

Kotlin扩展函数和运算符重载

扩展函数 fun String.lettersCount():Int{var count 0for(i in this){if(i.isLetter())count}return count } fun main(){val str:String "12we"println(str.lettersCount()) } 相当于直接将方法写在类里面。函数体内可以直接使用this而不用传参。 运算符重载 …

c++AVL树的模拟实现

前面对map/multimap/set/multiset进行了简单的介绍&#xff0c;在其文档介绍中发现&#xff0c;这几个容器有个 共同点是&#xff1a;其底层都是按照二叉搜索树来实现的&#xff0c;但是二叉搜索树有其自身的缺陷&#xff0c;假如往树中 插入的元素有序或者接近有序&#xff0c…

meshlab: pymeshlab沿着椭圆赤道投影展开当前网格的几何图形并保存(geometric cylindrical unwrapping)

一、关于环境 请参考&#xff1a;pymeshlab遍历文件夹中模型、缩放并导出指定格式-CSDN博客 二、关于代码 本文所给出代码仅为参考&#xff0c;禁止转载和引用&#xff0c;仅供个人学习。 本文所给出的例子是https://download.csdn.net/download/weixin_42605076/89233917中的…

爬虫界的“闪电侠”:异步爬虫与分布式系统的实战秘籍

Hi&#xff0c;我是阿佑&#xff0c;前文给大家讲了&#xff0c;如何做一个合法“采蜜”的蜜蜂&#xff0c;有了这么个自保的能力后&#xff0c;阿佑今天就将和大家踏入 —— 异步爬虫 的大门&#xff01; 异步爬虫大法 1. 引言1.1 爬虫框架的价值&#xff1a;效率与复杂度管理…

贷款借钱平台 贷款源码 小额贷款系统 卡卡贷源码 小额贷款源码 贷款平台

贷款平台源码/卡卡贷源码/小贷源码/完美版 &#xff0c; 数据库替换application/database.php 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89268533 更多资源下载&#xff1a;关注我。

Vue原理学习:vdom 和 diff算法(基于snabbdom)

vdom 和 diff 背景 基于组件化&#xff0c;数据驱动视图。只需关心数据&#xff0c;无需关系 DOM &#xff0c;好事儿。 但是&#xff0c;JS 运行非常快&#xff0c;DOM 操作却非常慢&#xff0c;如何让“数据驱动视图”能快速响应&#xff1f; 引入 vdom 用 vnode 表示真实…

代购系统搭建,淘宝、1688海外代购系统建设以及部分前端源码展示

客户登录主界面&#xff0c;可以根据个人需求更换。 可支持个人定制模块化&#xff0c;也有一些模块可供选择 系统演示站测试 部分源码展示&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"> <title>会员中心 – 淘…

2024生日快乐祝福HTML源码

源码介绍 2024生日快乐祝福HTML源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c; 源码截图 源码下载 2024生日快乐祝福HTML源码

Shopline和Shopify哪个更好?Shopline和Shopify的区别

Shopline和Shopify哪个更好取决于用户面向的市场&#xff0c;面向亚洲市场就更适合有本地化支持的Shopline&#xff0c;而如果希望拓展全球业务&#xff0c;Shopify可能更好。 Shopline和Shopify都是知名的电子商务平台&#xff0c;可以很好的帮助商家搭建和管理在线商店&…

【C语言】指针(二)

目录 一、传值调用和传址调用 二、数组名的理解 三、通过指针访问数组 四、一维数组传参的本质 五、指针数组 六、指针数组模拟实现二维数组 一、传值调用和传址调用 指针可以用在哪里呢&#xff1f;我们看下面一段代码&#xff1a; #include <stdio.h>void Swap(i…

计算机毕业设计hadoop+hive+hbase学情分析 在线教育大数据 课程推荐系统 机器学习 深度学习 人工智能 大数据毕业设计 知识图谱

毕 业 设 计&#xff08;论 文&#xff09;开 题 报 告 1&#xff0e;结合毕业设计&#xff08;论文&#xff09;课题情况&#xff0c;根据所查阅的文献资料&#xff0c;每人撰写不少于1000字的文献综述&#xff1a; 一、研究背景和意义 “互联网”和大数据带来了网络教育的蓬…

Linux查看进程命令ps和top

Linux 是一种自由和开放源代码的操作系统&#xff0c;它的使用在全球范围内非常广泛。在 Linux 中&#xff0c;进程是操作系统中最重要的组成部分之一&#xff0c;它代表了正在运行的程序。了解如何查看正在运行的进程是非常重要的&#xff0c;因为它可以帮助你了解系统的运行状…

付费解锁隐藏动力和续航,订阅制又被特斯拉玩出花了

我们知道&#xff0c;「订阅制」早已成互联网领域各路大厂玩烂的操作。 上到程序订阅付费使用&#xff08;例如 Offics、Adobe&#xff09;&#xff0c;下到各类功能服务订阅&#xff08;如影视会员、网盘会员等&#xff09;。 甚至于某东、某宝等网购平台也整出了 VIP 订阅服…

网络爬虫安全:90后小伙,用软件非法搬运他人原创视频被判刑

目录 违法视频搬运软件是网络爬虫 如何发现偷盗视频的爬虫&#xff1f; 拦截违法网络爬虫 央视《今日说法》栏目近日报道了一名程序员开发非法视频搬运软件获利超700多万&#xff0c;最终获刑的案例。 国内某知名短视频平台报警称&#xff0c;有人在网络上售卖一款视频搬运…

声纹识别在无人机探测上的应用

无人机在民用和军事领域的应用越来越广泛。然而&#xff0c;随着无人机数量的增加&#xff0c;"黑飞"现象也日益严重&#xff0c;对公共安全和隐私构成了威胁。因此&#xff0c;开发有效的无人机探测与识别技术变得尤为重要。及时发现黑飞无人机的存在进而对其型号进…

专“蜀”盛会!CGT Asia 2024 第六届亚洲细胞与基因治疗创新峰会(成都站)7月火热相邀

在细胞与基因治疗领域&#xff0c;我们正站在一个科技革命的风口上。中国的CGT市场预计将持续快速增长。根据相关分析&#xff0c;预计到2025年整体市场规模将达到25.9亿美元&#xff0c;显示出276%的复合年增长率。这一增长趋势预计将持续到2030年&#xff0c;细胞与基因治疗领…

视觉SLAM14精讲——三维空间刚体运动1.2

三维空间刚体运动 欧拉角 欧拉角可以说是零理解成本的表示形式&#xff0c;由于有万向锁的问题被绝大部分项目所抛弃。欧拉角的每个轴旋转都有固定好的名称&#xff0c;这些名称十分直观&#xff1a; Z轴旋转&#xff0c;相当于左右旋转&#xff0c;叫航角&#xff0c;或偏航…