当前位置: 首页 > article >正文

PDF书籍《手写调用链监控APM系统-Java版》第9章 插件与链路的结合:Mysql插件实现

本人阅读了 Skywalking 的大部分核心代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 “调用链监控APM” 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。

作者已经将过程写成一部书籍,奈何没有钱发表,如果您知道渠道可以联系本人。一定重谢。

本书涉及到的核心技术与思想

JavaAgent , ByteBuddy,SPI服务,类加载器的命名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。

适用人群

自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;

版权

本书是作者呕心沥血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。

原版PDF+源码请见:

本章涉及到的工具类也在这里面:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

第9章 插件与链路的结合:Mysql插件实现

9.1 Mysql插件的流程分析

数据库归根结底就是JDBC的操作,在学习时期,我们肯定会学习基本的jdbc查询数据库的写法:

// 1. 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取连接 对象
ConnectionImpl connection = (ConnectionImpl) DriverManager.getConnection("", "", "");
// 3. 准备 statement
PreparedStatement statement = connection.prepareStatement("select * from t_user");
// 4. 执行sql
statement.executeQuery();

无论用什么框架,数据库操作都避免不了上面原始步骤。我们分析下上面的流程:

1. 加载驱动,我们不关心。

2. 通过DriverManager的静态方法getConnection获取到ConnectionImpl 连接对象。

3. 通ConnectionImpl的prepareStatement方法,去配合sql准备一个PreparedStatement 。

4. 最后通过PreparedStatement的executeQuery方法去查询sql语句并返回结果。

数据库插件要想采集到调用的sql信息,就必须要拦截ConnectionImpl类的prepareStatement方法。 如果还要采集sql调用的返回信息,还需要拦截 PreparedStatement 类的executeQuery方法。但是我们还需要数据库服务器的地址信息,这个还必须要拦截 DriverManager.getConnection 。

拦截的三个类我们梳理出来了,但是这里有个很严重的问题,当我们拦截DriverManager.getConnection获取到数据库地址后,没办法向后传递到ConnectionImpl类的prepareStatement中。

我们架设一个猜想:

拦截DriverManager.getConnection返回的是ConnectionImpl。如果能在这个阶段将数据库的地址信息设置到返回的ConnectionImpl对象中,后面拦截ConnectionImpl的prepareStatement方法时,方法切面那里是不是有个参数能获取到当前对象,也就能拿到DriverManager.getConnection拦截时的数据库信息了。

这也就需要在增强类中添加Object字段,用于参数传递的思想。

我们目前的字节码增强代码时无法实现上述思想的,需要进行改造。接下来我们来讲解下如何实现。

9.2 插桩类改造,新增Object字段和实现EnhancedInstance接口

由于篇幅过长,请到第一章查看原版PDF和源码:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客


9.3 Mysql插件真正实现

根据前面的分析,我们依次需要拦截

类名: java.sql.DriverManager

方法:getConnection

JDK类库

然后拦截:

类名: com.mysql.jdbc.ConnectionImpl

方法:prepareStatement

非JDK类库

最后拦截:

类名: com.mysql.jdbc.PreparedStatement

方法:executeQuery

非JDK类库

在插件模块下新增apm-mysql-plugin项目,POM内容:

<dependency>
    <groupId>com.hadluo.apm</groupId>
    <artifactId>apm-commons</artifactId>
    <version>1.0</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.44</version>
    <scope>provided</scope>
</dependency>

hadluo-apm-plugin.def 插件定义文件:

mysql5-DriverConnect=com.hadluo.apm.plugin.mysql5.DriverConnectInstrumentation

mysql5-PrepareStatement=com.hadluo.apm.plugin.mysql5.PrepareStatementInstrumentation

mysql5-PrepareStatementExecute=com.hadluo.apm.plugin.mysql5.PrepareStatementExecuteInstrumentation

三个类我就不建了,值得注意的是,DriverManager#getConnection方法最终调用下面方法:

DriverConnectInstrumentation配置拦截类名时,还需要指定参数签名:

还有就是isBootstrapInstrumentation 一定要返回true, 因为它是rt.jar的JDK类。

三个方法环绕执行器分别是:

DriverConnectInterceptor

PrepareStatementInterceptor

PrepareStatementExecuteInterceptor

DriverConnectInterceptor 代码实现:

由于篇幅过长,请到第一章查看原版PDF和源码:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

打包测试,kafka数据json如下:

{
    "msgTypeClass": "com.hadluo.apm.commons.kafka.Segment",
    "sampleTime": 1734056030540,
    "serviceName": null,
    "serviceInstance": "1a91d6d937ea4d6b8c2cb34dc75bf240@192.168.2.125",
    "traceId": "c133c183325b48fdbd3c94eca8bf341e.44.17340560303730001",
    "traceSegmentId": "c133c183325b48fdbd3c94eca8bf341e.44.17340560303710000",
    "spans": [
        {
            "spanId": 1,
            "parentSpanId": 0,
            "startTime": 1734056030528,
            "endTime": 1734056030529,
            "refs": [

            ],
            "operationName": "jdbc:mysql://127.0.0.1:3306/test/select * from t_user/executeQuery",
            "peer": null,
            "spanType": "Exit",
            "spanLayer": "DB",
            "component": "MySQL",
            "tags": {
                "remotePeer": "jdbc:mysql://127.0.0.1:3306/test",
                "extra": "{password=, user=root}",
                "sql": "select * from t_user"
            },
            "logs": {

            }
        },
        {
            "spanId": 0,
            "parentSpanId": -1,
            "startTime": 1734056030373,
            "endTime": 1734056030538,
            "refs": [

            ],
            "operationName": "/order",
            "peer": null,
            "spanType": "Entry",
            "spanLayer": "HTTP",
            "component": "Tomcat",
            "tags": {
                "http.method": "GET",
                "url": "/order"
            },
            "logs": {

            }
        }
    ]
}

上述json我想应该很熟悉了,不用我过多分析,只是还有一个问题,就是serviceName为空。 熟悉SkyWalking的读者都应该知道,应用名称是在启动参数上通过 agent name 来配置的。

作者不这样做,我们可以拦截SpringBoot的启动流程,在解析到Enviroment后,将 spring.application.name 的值设置到Config,

当然,这仅限制于springboot服务生效。下节我们就来分析如何拦截springboot的Enviroment。


http://www.kler.cn/a/454119.html

相关文章:

  • 基于 Python 大数据的拼团购物数据分析系统的设计与实现
  • 路由器的原理
  • Axure10
  • 【无标题】学生信息管理系统界面
  • GitLab安装及使用
  • C++打造局域网聊天室第十三课: 任务栏托盘功能的实现
  • [2003].第2-01节:关系型数据库表及SQL简介
  • [bug]java导出csv用Microsoft Office Excel打开乱码解决
  • 【K8S问题系列 | 20 】K8S如何删除异常对象(Pod、Namespace、PV、PVC)
  • 各种网站(学习资源、常用工具及其他,持续更新中~)
  • Python - 获取当前函数中的所有参数信息(名称和值)
  • 【Rust自学】7.3. 路径(Path)Pt.2:访问父级模块、pub关键字在结构体和枚举类型上的使用
  • Redis基础知识分享(含5种数据类型介绍+增删改查操作)
  • c#自定义事件
  • 2023年软件产业报告|2023年我国软件业经济运行情况及未来发展趋势预期
  • 【论文阅读笔记】Scalable, Detailed and Mask-Free Universal Photometric Stereo
  • 数据库系统概论
  • ctfshow-web入门-文件包含(web82-web86)条件竞争实现session会话文件包含
  • Llama3.370B超越GPT-4o和Claude3.5 Sonnet
  • 从随机生成到深度学习:使用DCGAN和CycleGAN生成图像的实战教程