从利用Arthas排查线上Fastjson问题到Java动态字节码技术(中)

2023-10-27

上一篇文章 中通过对一次线上事故的复盘,引出了福报厂的Arthas,一个建立在Java动态字节码技术之上的Java诊断工具;关于Arthas的使用方式就不赘述了,查看官方文档可以很快上手,玩法也特别多;上一篇中也仅仅只介绍了一种使用场景,即”debug线上JVM内部class信息、在线watch方法执行并查看方法输入输出、在线反编译class、重新编辑Java后直接热部署“的组合拳(手动狗头)…

上手一门技术最基本要做到 what-how-why,在知道了Arthas是什么(what),以及如何使用(how)之后,自然需要去了解它内如如何工作的,以及底层原理(why);而了解底层原理最直接的方式便是阅读源码,毕竟代码不会骗人的,但任何技术文章或书籍都会有”噪音“,那么接下来就从Arthas源码入手,聊聊Java Agent、Instrument、动态字节码技术;

一直都回避写源码方向的文章,源码读起来容易,但写解读就需要粘贴大量代码,会占据大量篇幅,并且很容易写成流水账,但像Arthas或者Dubbo这类优秀的国产项目还是值得的;
同时由于时间与精力有限,本篇着重介绍Java Agent,Instrument等动态字节码技术放到下一篇…


Arthas源码

记得当初第一次拿到Arthas源码的时候,印象最深的就是那可爱的TODO.md,哈哈,像是某位福娃老哥在做交接…

* 代码还是很乱,需要继续重构
* 依赖需要清理,几个问题:
    * 所有 apache 的 common 库应当不需要
    * json 库有好几份
    * `jopt-simple` 看下能不能用 `cli` 取代
    * `cli`, `termd` 的 artifactId, version 需要想下。是不是应该直接拿进来。他们的依赖也需要仔细看一下
* termd 依赖 netty,感觉有点重,而且第一次 attach 比较慢,不确定是 netty 的问题还是 attach 的问题
* 目前 web console 依赖 termd 中自带的 term.js 和 css,需要美化,需要想下如何集成到研发门户上
* 因为现在没有 Java 客户端了,所以 batch mode 也就没有了
* `com.taobao.arthas.core.shell.session.Session` 的能力需要和以前的 session 的实现对标。其中:
    * 真的需要 textmode 吗?我觉得这个应该是 option 的事情
    * 真的需要 encoding 吗?我觉得仍然应该在 option 中定义,就算是真的需要,因为我觉得就应该是 UTF-8
    * duration 是应当展示的,session 的列表也许也应当展示
    * 需要仔细看下 session 过期是否符合预期
    * 多人协作的时候 session 原来是在多人之间共享的吗?
* 所有的命令现在实现的是 AnnotatedCommand,需要继续增强的是:
    * Help 中的格式化输出被删除。需要为 `@Description` 定义一套统一的格式
    * 命令的输入以及输出的日志 (record logger) 被删除,需要重新实现,因为现在是用 `CommandProcess` 来输出,所以,需要在 `CommandProcess` 的实现里打日志
* `com.taobao.arthas.core.GlobalOptions` 看上去好奇怪,感觉是 OptionCommand 应当做的事情
* `com.taobao.arthas.core.config.Configure` 需要清理,尤其是和 http 相关的
* 需要合并 develop 分支上后续的修复
* 代码中的 TODO/FIXME

回归主题,首先直奔 arthas-core 模块,因为这是整个arthas的入口,即执行java -jar arthas-core.jar;主函数内最重要的任务就是通过java进程的pid来 attach JVM 与 load agent;

private void attachAgent(Configure configure) throws Exception {
    VirtualMachineDescriptor virtualMachineDescriptor = null;
    for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
        String pid = descriptor.id();
        if (pid.equals(Long.toString(configure.getJavaPid()))) {
            virtualMachineDescriptor = descriptor;
            break;
        }
    }
    VirtualMachine virtualMachine = null;
    try {
        if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
            virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
        } else {
            virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
        }
        //略
        virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString());
    } finally {
        if (null != virtualMachine) {
            virtualMachine.detach();
        }
    }
}

这里就用到了Java Agent技术

Java Agent

Java Agent可以说是JVM的一个后门,平时我们使用的 动态编译、热部署、APM监控工具、trace链路分析等,都是通过Java Agent,或者说都是建立在Java Instrument之上的;那么以Arthas为例看下Arthas是如何做的;

打开arthas-agent模块,在pom中可以发现些端倪,以下四行是关键

<manifestEntries>
    <Premain-Class>com.taobao.arthas.agent334.AgentBootstrap</Premain-Class>
    <Agent-Class>com.taobao.arthas.agent334.AgentBootstrap</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>

可以看到Manifest中都使用到了这个AgentBootstrap,那么打开AgentBootstrap可以看到它有一个private修饰的主函数,而真正调用它的是两个静态方法

public static void premain(String args, Instrumentation inst) {
    main(args, inst);
}
public static void agentmain(String args, Instrumentation inst) {
    main(args, inst);
}
private static synchronized void main(String args, final Instrumentation inst) {
	//略
}

这两个方法,一个pre-main,一个agent-main,整好对应着Manifest中定义的两个入口;

而这两个,也正是Java Agent的两种实现,静态Agent与动态Agent;

可以看到,参数中除了args之外,还有一个Instrumentation,这就是Java的字节码增强功能,Agent提供的任何功能底层都是通过对原字节码进行增强而动态织入的;这里先不深入Instrument,放到下一篇中去介绍;

继续看主函数,主函数中最重要的就是执行 bind 动作;

private static synchronized void main(String args, final Instrumentation inst) {
	//略
    final ClassLoader agentLoader = getClassLoader(inst, arthasCoreJarFile);
    Thread bindingThread = new Thread() {
        @Override
        public void run() {
            try {
                bind(inst, agentLoader, agentArgs);
            } catch (Throwable throwable) {
                throwable.printStackTrace(ps);
            }
        }
    };
    //略
}
private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) {
	//ARTHAS_BOOTSTRAP => com.taobao.arthas.core.server.ArthasBootstrap
    Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
    //GET_INSTANCE => getInstance()
    Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, String.class).invoke(null, inst, args);
    //IS_BIND => boolean isBind()
    boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
    if (!isBind) {
        String errorMsg = "Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.";
        ps.println(errorMsg);
        throw new RuntimeException(errorMsg);
    }
    ps.println("Arthas server already bind.");
}

在这个 bind 中,通过自定的ClassLoader加载了 arthas-core 模块下的 server 包中的 ArthasBootstrap 这个关键类(代码结构确实比较乱…),同时通过反射,执行了两个方法,一个是 getInstance 另外一个是检测是否bind成功;

查看 ArthasBootstrap.getInstance 方法,可以看到这是个懒加载的单例模式,

public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, String args){
    if (arthasBootstrap != null) {
        return arthasBootstrap;
    }
    Map<String, String> argsMap = FeatureCodec.DEFAULT_COMMANDLINE_CODEC.toMap(args);
	//略
    return getInstance(instrumentation, mapWithPrefix);
}
public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
    if (arthasBootstrap == null) {
        arthasBootstrap = new ArthasBootstrap(instrumentation, args);
    }
    return arthasBootstrap;
}

其中第5行是通过自定义的分隔符来解析配置参数;

终于来到了重头戏ArthasBootstrap

private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args){
    initFastjson();    //略
    
    initSpy();	// 1. initSpy()
    
    initArthasEnvironment(args);    //略

	transformerManager = new TransformerManager(instrumentation); // 2. 增强
    enhanceClassLoader();	// 2. 增强
    
    initBeans();	//略

    bind(configure);	// 3. 启动server

    shutdown = new Thread("as-shutdown-hooker") {
        @Override
        public void run() {
            ArthasBootstrap.this.destroy();
        }
    };

    Runtime.getRuntime().addShutdownHook(shutdown);
}

第一个值得说说的是 arthas.SpyAPI, Arthas中的Spy就类似于AOP,可以在各个切入点进行方法织入,举个例子:

public class SpyAPI {
    public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
        spyInstance.atEnter(clazz, methodInfo, target, args);
    }
}
public class SpyImpl extends AbstractSpy {
    @Override
    public void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
        ClassLoader classLoader = clazz.getClassLoader();

        String[] info = splitMethodInfo(methodInfo);
        String methodName = info[0];
        String methodDesc = info[1];

        List<AdviceListener> listeners = AdviceListenerManager.queryAdviceListeners(classLoader, clazz.getName(),
                methodName, methodDesc);
        if (listeners != null) {
            for (AdviceListener adviceListener : listeners) {
                try {
                    if (skipAdviceListener(adviceListener)) {
                        continue;
                    }
                    adviceListener.before(clazz, methodName, methodDesc, target, args);
                } catch (Throwable e) {
                    logger.error("class: {}, methodInfo: {}", clazz.getName(), methodInfo, e);
                }
            }
        }
    }

可以看出来这里也用到了设计模式观察者模式,首先获取到所有切面,然后对各个切面织入before方法!

接下来是 enhanceClassLoader() 这里是动态字节码技术中的 transformer,也暂且不表,现在只需要知道它就是真正需要动态织入的逻辑即可;

最后是 bind 方法,底层是利用 Netty 搭建的server,最后的 isBind() 则是检测 server是否启动成功;server一旦启动成功,就可以通过客户端 CLI 命令行来发送指令了;

以上,不论是将arthas.jar 打包到项目中,还是先部署的项目后启动的arthas,都可以通过 pre-main 与 agent-main 将 arthas server 提供的功能织入到 原项目中;

那么,下一篇就深入介绍arthas是如何通过 instrument 来实现的字节码级别的功能织入…

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从利用Arthas排查线上Fastjson问题到Java动态字节码技术(中) 的相关文章

随机推荐

  • HTTP1、HTTP2、HTTPS详解

    HTTP1 HTTP 协议老的标准是HTTP 1 0 为了提高系统的效率 HTTP 1 0规定浏览器与服务器只保持短暂的连接 浏览器的每次请求都需要与服务器建立一个TCP连接 服务器完成请求处理后立即断开TCP连接 服务器不跟踪每个客户也不
  • 软件外包接单经验谈-需求篇

    上一篇谈了如何寻找客户 这一期就谈谈在和客户接洽时 如何与客户沟通需求 在这里我不去套用类似PMP里面那些完善的高大上的需求管理的方法论 因为我第一篇文章就说了 我写的这一系列文章都是针对小公司或者个人承接的外包项目 也就是都是一些中小项目
  • Android dumpsys使用

    目录 一 dumpsys命令介绍 1 命令介绍 2 服务查询和介绍 二 核心服务信息查询 1 package包信息查询 2 activity信息查询 3 window信息查询 三 实现自定义服务dumpsys信息查询 一 dumpsys命令
  • NodeJS - 回调函数

    什么是回调函数 回调函数是一个异步等价的函数 在给定任务完成时调用回调函数 NodeJS大量使用回调 NodeJS的所有API都是以支持回调的方式编写的 例如 读取文件的函数可能会开始读取文件后并立即将控件返回到执行环境 以便可以执行下一条
  • 区块链-区块链特点

    目录 https blog csdn net qq 40452317 article details 89646633 区块链 Blockchain 是一系列现有成熟技术的有机组合 它对账本进行分布式的有效记录 并且提供完善的脚本以支持不同
  • Qt 2D图形平面绘制

    Qt 2D图形平面绘制 一 图形视图框架 Graphics View Framework 二 实战 1 步骤 2 代码 三 参考 四 总结 一 图形视图框架 Graphics View Framework QGraphicsScene 场景
  • RocketMQ解析

    文章目录 1 单机版消息中心 2 分布式消息中心 2 1 问题与解决 2 1 1 消息丢失的问题 2 1 2 同步落盘怎么才能快 2 1 3 消息堆积的问题 2 1 4 定时消息的实现 2 1 5 顺序消息的实现 2 1 6 分布式消息的实
  • 软件测试进阶篇

    测试专栏 软件测试的基本概念 关于软件测试 作为一个测试人员 这些基础知识必不可少 关于测试用例 目录 一 按照测试对象来划分 1 界面 2 可靠性的测试 3 容错性 4 文档测试 5 兼容性测试 6 易用性测试 7 安装卸载测试 8 安全
  • Java string的基本用法

    Java string的基本用法 一 定义字符串与子串 定义 String e 空字符串 String E Hello 提取子串使用Substring方法 String E Hello String s E substring 0 4 s等
  • 《数字化转型》——企业持续有效增长的新引擎

    中国国民经济和社会发展第十四个五年规划和2035念远景目标纲要 明确指出 迎接数字时代 激活数据要素潜能 推动网络强国建设 加快建设数字经济 数字社会 数字政府 以数字化转型整体驱动生产方式 生活方式和治理方式变革 那么企业如何做 如何选型
  • Nginx通过/etc/init.d/nginx方式启动或停止服务

    Linux Nginx启动 停止 重启脚本 Nginx 启动 重启 停止脚本 第一步 先运行命令关闭nginx sudo kill cat usr local nginx logs nginx pid lt
  • Python时间序列预测大气二氧化碳浓度

    二氧化碳 CO2 和甲烷 CH4 等温室气体 GHG 会在大气中捕获热量 从而使我们的星球保持温暖 对生物物种友好 无论如何 燃烧化石燃料等人类活动会导致大量温室气体排放 从而过度提高地球的全球平均温度 因此 向可持续的全球经济转型势在必行
  • 极光笔记

    01 营销人 你是否饱受困扰 作为营销人的你 从996到007 每天从早忙到晚 但还是没办法把访客转化成客户 作为营销人的你 想通过APP通知 短信 邮件 公众号消息等方式 把所有能想到的营销方式 万箭齐发 结果却收效甚微 作为营销人的你
  • unity 设置animation不循环

    Unity中动画创建后 将会生成一个后缀名为 anim的文件 里面包含着动画内容 里面有一个属性 叫Loop Time 创建时它默认是勾选的 如果想去掉 可先找到你生成动画时创建的 anim文件 点击它 在右边Inspector栏里面找到L
  • MediaCodec、OpenGL、OpenSL/AudioTrack 实现一款简单的视频播放器

    概述 功能很简单 大致流程为 1 MediaCodec 解码视频文件得到 YUV PCM 数据 2 OpenGL 将 YUV 转为 RGB 并渲染到 Surface 上 3 OpenSL AudoTrack 获取 PCM 数据并播放 需要的
  • IDEA插件ASM Bytecode Outline

    IDEA插件ASM Bytecode Outline
  • Python之map()函数详解

    文章目录 一 map 函数简介 1 1 map 函数基本语法 1 2 map 函数 lambda表达式 1 3 map 函数输入多个可迭代对象iterable 1 4 查看返回的迭代器内容 二 map 函数示例 示例一 使用 map 函数操
  • CentOS 7.6镜像下载

    目前在国内使用最多的两个Linux发行版本一个是CentOS 另外一个是Ubuntu CentOS是一个可以重新分发的开源操作系统 也是企业Linux发行版的领头羊 官方目前发布的最新CentOS版本为CentOS 9 那么如何到下载旧版本
  • 一个数组有 N 个元素,求连续子数组的最大和。 例如:[-1,2,1],和最大的连续子数组为[2,1],其和为 3

    1 题目描述 一个数组有 N 个元素 求连续子数组的最大和 例如 1 2 1 和最大的连续子数组为 2 1 其和为 3 输入描述 输入为两行 第一行一个整数n 1 lt n lt 100000 表示一共有n个元素 第二行为n个数 即每个元素
  • 从利用Arthas排查线上Fastjson问题到Java动态字节码技术(中)

    上一篇文章 中通过对一次线上事故的复盘 引出了福报厂的Arthas 一个建立在Java动态字节码技术之上的Java诊断工具 关于Arthas的使用方式就不赘述了 查看官方文档可以很快上手 玩法也特别多 上一篇中也仅仅只介绍了一种使用场景 即