arthas底层实现原理剖析

2023-10-30

前言
经常在应用的启动或者运行过程中需要动态的查看数据,或者实时的验证我们写的代码的结构与执行过程,此时需要一种工具能够动态的检测程序运行的状态,内存数据,线程情况,最好能够动态的替换代码实时生效,方便我们从日志或者其他埋点断言我们的猜测。

1. arthas 阿尔萨斯的工程结构
其实有很多工具可以达到这种效果,arthas就是其中一种。

从工程结构,其实arthas的核心功能是core,里面有arthas的attach与诊断指令的代码。 通过实际启动分析进一步看原理。

2. arthas 启动
2.1 打包
对源码去除

git-commit-id-plugin
插件,毕竟现在github已经很难连接了

执行mvn clean package,在packing module下

src下面其实有

assembly.xml
文件定义了打包的详情,每个module定义了打包的插件,毕竟诊断工具需要把所有第三方的jar class字节码打进jar,即fatjar,所以对依赖需要尽量少,观源码arthas重度依赖Telnet netty,感觉依赖有点重。

2.2 执行boot启动
boot的启动是执行java -jar,其实就是一个普通的jar应用

2.2.1  选择进程pid
pid = ProcessUtils.select(bootstrap.isVerbose(), telnetPortPid, bootstrap.getSelect());
 其实很简单,就是去找java home,找到jps命令,然后jps -l

可以看到findJps

查找本机jvm进程 

 2.2.2 启动进程attach pid
ProcessUtils.startArthasCore(pid, attachArgs);
 不明白为啥要独立启动一个进程去attach,这个进程在attach完成后自动运行结束。参数就是前面的pid core agent等,其实核心是pid agent jar,其他都是额外功能的。

    public static void startArthasCore(long targetPid, List<String> attachArgs) {
        // find java/java.exe, then try to find tools.jar
        String javaHome = findJavaHome();
 
        // find java/java.exe
        File javaPath = findJava(javaHome);
        if (javaPath == null) {
            throw new IllegalArgumentException(
                            "Can not find java/java.exe executable file under java home: " + javaHome);
        }
 
        File toolsJar = findToolsJar(javaHome);
 
        if (JavaVersionUtils.isLessThanJava9()) {
            if (toolsJar == null || !toolsJar.exists()) {
                throw new IllegalArgumentException("Can not find tools.jar under java home: " + javaHome);
            }
        }
 
        List<String> command = new ArrayList<String>();
        command.add(javaPath.getAbsolutePath());
 
        if (toolsJar != null && toolsJar.exists()) {
            command.add("-Xbootclasspath/a:" + toolsJar.getAbsolutePath());
        }
 
        command.addAll(attachArgs);
        // "${JAVA_HOME}"/bin/java \
        // ${opts} \
        // -jar "${arthas_lib_dir}/arthas-core.jar" \
        // -pid ${TARGET_PID} \
        // -target-ip ${TARGET_IP} \
        // -telnet-port ${TELNET_PORT} \
        // -http-port ${HTTP_PORT} \
        // -core "${arthas_lib_dir}/arthas-core.jar" \
        // -agent "${arthas_lib_dir}/arthas-agent.jar"
 
        ProcessBuilder pb = new ProcessBuilder(command);
        try {
            final Process proc = pb.start();
这里严重依赖tools.jar,因为使用了里面虚拟机的attach方法

 启动里面的arthas-core.jar

那么执行com.taobao.arthas.core.Arthas

源码分析,VirtualMachine即tools的能力,所以前面需要查找tools.jar

    private void attachAgent(Configure configure) throws Exception {
        VirtualMachineDescriptor virtualMachineDescriptor = null;
        //VirtualMachine.list() 相当于jps -lv的能力
        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);
            }
 
            Properties targetSystemProperties = virtualMachine.getSystemProperties();
            String targetJavaVersion = JavaVersionUtils.javaVersionStr(targetSystemProperties);
            String currentJavaVersion = JavaVersionUtils.javaVersionStr();
            if (targetJavaVersion != null && currentJavaVersion != null) {
                if (!targetJavaVersion.equals(currentJavaVersion)) {
                    AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.",
                                    currentJavaVersion, targetJavaVersion);
                    AnsiLog.warn("Target VM JAVA_HOME is {}, arthas-boot JAVA_HOME is {}, try to set the same JAVA_HOME.",
                                    targetSystemProperties.getProperty("java.home"), System.getProperty("java.home"));
                }
            }
 
            String arthasAgentPath = configure.getArthasAgent();
            //convert jar path to unicode string
            configure.setArthasAgent(encodeArg(arthasAgentPath));
            configure.setArthasCore(encodeArg(configure.getArthasCore()));
            //载入jar
            virtualMachine.loadAgent(arthasAgentPath,
                    configure.getArthasCore() + ";" + configure.toString());
        } finally {
            if (null != virtualMachine) {
                //attach 完成后需要通知结束
                virtualMachine.detach();
            }
        }
    }
2.2.3 attach后处理
attach pid后,会loadAgent,加载agent的jar

定义了

Premain-Class、Agent-Class、Can-Redefine-Classes、Can-Retransform-Classes
 Premain-Class、Agent-Class定义执行的main方法: Agent-Class是attach的方式;Premain-Class是agent随启动的执行方式

Can-Redefine-Classes、Can-Retransform-Classes 定义字节码增强的开关

获取classloader,然后bind,先看classloader

    private static ClassLoader getClassLoader(Instrumentation inst, File arthasCoreJarFile) throws Throwable {
        // 构造自定义的类加载器,尽量减少Arthas对现有工程的侵蚀
        return loadOrDefineClassLoader(arthasCoreJarFile);
    }
 
    private static ClassLoader loadOrDefineClassLoader(File arthasCoreJarFile) throws Throwable {
        if (arthasClassLoader == null) {
            arthasClassLoader = new ArthasClassloader(new URL[]{arthasCoreJarFile.toURI().toURL()});
        }
        return arthasClassLoader;
    }
其实就是自定义classloader,载入jar包 

反射创建 

ArthasBootstrap实例
    private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable {
        /**
         * <pre>
         * ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(inst);
         * </pre>
         */
        Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
        Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, String.class).invoke(null, inst, args);
        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.");
    }
其实就是new 对象的时候,干些初始化的事情

    /**
     * 单例
     *
     * @param instrumentation JVM增强
     * @return ArthasServer单例
     * @throws Throwable
     */
    public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
        if (arthasBootstrap == null) {
            arthasBootstrap = new ArthasBootstrap(instrumentation, args);
        }
        return arthasBootstrap;
    }
 进一步跟踪

    private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
        this.instrumentation = instrumentation;
 
        //新版才加入的,不明白为啥加入fastjson
        initFastjson();
 
        // 1. initSpy() 其实就是加载java.arthas.SpyAPI的class对象
        initSpy();
        // 2. ArthasEnvironment,扣的Spring环境的源码
        initArthasEnvironment(args);
 
        //输出路径
        String outputPathStr = configure.getOutputPath();
        if (outputPathStr == null) {
            outputPathStr = ArthasConstants.ARTHAS_OUTPUT;
        }
        outputPath = new File(outputPathStr);
        outputPath.mkdirs();
 
        // 3. init logger
        loggerContext = LogUtil.initLooger(arthasEnvironment);
 
        // 4. 增强ClassLoader,初始化    
        //instrumentation.addTransformer(classLoaderInstrumentTransformer, true);
        enhanceClassLoader();
        // 5. init beans  ResultViewResolver HistoryManagerImpl 结果解析器与历史记录
        initBeans();
 
        // 6. start agent server
        // 顾名思义,绑定shellServer 创建http Telnet的链接
        bind(configure);
 
        executorService = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                final Thread t = new Thread(r, "arthas-command-execute");
                t.setDaemon(true);
                return t;
            }
        });
 
        shutdown = new Thread("as-shutdown-hooker") {
 
            @Override
            public void run() {
                ArthasBootstrap.this.destroy();
            }
        };
 
        //非常关键,字节码增强使用
        transformerManager = new TransformerManager(instrumentation);
        Runtime.getRuntime().addShutdownHook(shutdown);
    }
环境信息的源码,其实是Spring的源码

 关键的字节码增强初始化

    public TransformerManager(Instrumentation instrumentation) {
        this.instrumentation = instrumentation;
 
        classFileTransformer = new ClassFileTransformer() {
 
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                    ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                for (ClassFileTransformer classFileTransformer : reTransformers) {
                    byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                            protectionDomain, classfileBuffer);
                    if (transformResult != null) {
                        classfileBuffer = transformResult;
                    }
                }
 
                for (ClassFileTransformer classFileTransformer : watchTransformers) {
                    byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                            protectionDomain, classfileBuffer);
                    if (transformResult != null) {
                        classfileBuffer = transformResult;
                    }
                }
 
                for (ClassFileTransformer classFileTransformer : traceTransformers) {
                    byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                            protectionDomain, classfileBuffer);
                    if (transformResult != null) {
                        classfileBuffer = transformResult;
                    }
                }
 
                return classfileBuffer;
            }
 
        };
        instrumentation.addTransformer(classFileTransformer, true);
    }
 instrumentation.addTransformer(classFileTransformer, true);

至此结束,其实原理很简单:attach 然后初始化Telnet与http服务,通过addTransformer来动态字节码增强,client端连接上去,然后发指令。

3. arthas的原理
基于Instrumentation的产品,除了arthas,常用的还有

pinpoint、skywalking
这些非常有名气 的产品。Instrumentation非常关键的API

arthas的关键原理是Javaagent,有2种方式

1.在 JVM 启动的时加载 JDK5开始支持

       使用javaagent VM参数 java -javaagent:xxxagent.jar xxx,这种方式在 main 方法之前执行 agent 中的 premain 方法
       public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception


 2.在 JVM 启动后 Attach JDK6开始支持

       通过 Attach API 进行加载,在进程存在的时候,动态attach,这种方式会在 agent 加载以后执行 agentmain 方法
       public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception

且必须加上maven插件参数,其他方式代码管理同理

<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>
总结
arthas其实原理很简单,使用Javaagent技术,其实debug模式也是使用这种技术。arthas增强了字节码,写了一些native方法获取jvm的堆等信息。从源码看,不知道为啥使用telnet协议,重度依赖netty termd,为啥不使用简单的HTTP协议,无状态降低依赖,而且方便前端图形化,admin端目前是telnet透传。估计设计之初就认为是敲命令吧,但是有没有联想能力,敲命令还是很费时,需要学习。

 

 

 

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

arthas底层实现原理剖析 的相关文章

随机推荐

  • python自然语言处理入门-新手上路

    新手上路 博主微信公众号 左 Python 智能大数据 AI学习交流群 右 欢迎关注和加群 大家一起学习交流 共同进步 目录 摘要 1 自然语言与编程语言 2 自然语言处理的层次 2 1 语音 图像和文本 2 2 中文分词 词性标注和命名实
  • 1400*A. World Football Cup(模拟)

    Problem 19A Codeforces 解析 模拟 记录总得分 净胜球 进球数 坑点 其中注意净胜球是进球数的差 己方进球数 对手进球数 可以为负数 排序即可 include
  • 前端大文件下载方案

    前端大文件下载方案 文章目录 前端大文件下载方案 JSZip StreamSaver js 与 JSZip 结合使用 mitm sw 配置 tags Streams API Service Worker StreamSaver js JSZ
  • 安装kubeadm

    kubeadm是一个部署K8S的一个方式 又是一个芬兰人 又是一个高中生 但是貌似不支持生产目前 但是对于尝鲜学习K8S是一个不错的方式 安装其实不麻烦 但是吧有些东西因为某些限制导致你安装不了 下载不下来 就好比原先下载安卓的SDK挂一晚
  • 点灯游戏2-15游戏解答

    快来快来学一学 点灯游戏 2 15求解 N N 解法遍历 储存 local A local B local C local N 0 local t local s 0 local function addt local t N N 灯变化记
  • Obsidian中如何创作思维导图Mind-map

    使用插件 obsidian mind map 1 直接在社区下载安装 设置快捷键或者在左侧竖形打开命令面板搜索关键字 mind 或者为了便于使用 设置快捷键 在设置 第三方插件中 选择快捷键 然后按下你想设置的快捷键就可以 我这里设置成了C
  • Permission denied: user=dr.who, access=READ_EXECUTE, inode="/tmp":root:supergroup:drwx------

    今天在做Hadoop 分布式实例的时候遇到了这个错误 Permission denied user dr who access READ EXECUTE inode tmp root supergroup drwxrwx 出错原因 tmp
  • sql企业版和标准版区别_一张图看懂OPPO Reno 3标准版和Pro版的区别

    随着发布会的临近 OPPO Reno 3系列的硬件参数被彻底曝光 虽然普通版和Pro版都支持双模5G网络 但是为了不同的定位 这2款手机在主要硬件配置上区别还是蛮大的 根据官方和工信部的消息 亓纪将两款手机的的硬件参数做了一个对比 通过一张
  • 云计算、大数据、人工智能时代,为什么不能错过Linux?

    随着这些年互联网技术的迅猛发展 在快速步入大数据 云计算 虚拟技术和人工智能时代 技术为王现象在信息科技领域越来越凸显出来 随之而来的是高端Linux运维人才出现了极度紧缺的现象 为什么要选择Linux 说起Linux 大家可能都知道好 优
  • 免费开源的箱包制造行业ERP管理系统介绍

    用Odoo免费开源ERP按需打造可持续商业模型 广东百立皮具是一家集生产 采购 定制 销售为一体的箱包及配饰贸易公司 专营各类箱包皮革制品 产品业务规模遍布全世界 百立皮具距今运营已有十余年之久 拥有千余名员工 且在多国都开设了分公司 多年
  • matplotlib中堆积图、分块图、气泡图的绘制

    本文介绍matplotlib中堆积图 分块图 气泡图的绘制 堆积图的绘制 堆积图常用于综合展示不同分类的指标趋势以及它们的总和的趋势 比如说 我们想看一下5名同学期末的总分情况 同时 我们又想看一下这5名同学的各科成绩以及它们各自的占比 这
  • uni-app——小程序实现本地图片的上传以及身份证的智能识别

    文章目录 前言 一 示例图 二 实现过程 1 完成提交图片的api地址 2 获取本地图片 3 将本地图片上传至开发者服务器 三 具体实现代码 四 身份证的智能识别 总结 前言 上传本地图片的功能很常见 那么具体该如何实现呢 一 示例图 二
  • 顶尖程序员的五种思维模式

    一 勇于研究你不懂的代码 通过研究不同的代码 从而熟悉不同的代码结构和设计模式 二 精通代码调试 几乎所有的代码都不是一遍写好 学会怎么去调试代码 三 重视能够节约时间的工具 工具是很重要的 它们能帮我们节省大量的时间 四 优化你的迭代速度
  • python调用自己写的py文件

    目录 python如何调用自己写的py文件呢 如果是不同目录怎么调用呢 如果需要调用的多个文件在多个目录呢 关于 init py的解释 关于sys path的解释 python如何调用自己写的py文件呢 同一个目录下直接写import xx
  • IP地址网段表示法

    http blog sina com cn s blog 4a1d691b010004qx html 1 IP地址 共分为四类 A B C D类 各类范围详见RFC参考 2 子网掩码 子网掩码的作用是用来表示IP地址中的多少位用来表示主机号
  • 23种设计模式之模板模式

    文章目录 概述 模版模式的优缺点 优点 缺点 模版模式的使用场景 模板模式的结构和实现 模式结构 模式实现 总结 概述 模板模式指 一个抽象类中 有一个主方法 再定义1 n个方法 可以是抽象的 也可以是实际的方法 定义一个类 继承该抽象类
  • 深入学习jquery源码之jQuery的构造函数与实例化

    深入学习jquery源码之jQuery的构造函数与实例化 创建jQuery对象的整个流程如下 1 调用 方法 2 调用jQuery prototype init 构造函数 3 根据选择器不同返回不同的jQuery对象 4 不同jQuery对
  • java web中servlet详解_javaWeb之Servlet详解

    Servlet详解 1 servlet简单介绍 servlet是javaweb三大组件之一 他与filter listener 共同组成了javaweb的三大组件 Servlet Server Applet 是Java Servlet的简称
  • 成员变量和局部变量

    成员变量和局部变量的区别 1 成员变量是独立于方法外的变量 局部变量是类的方法中的变量 成员变量 包括实例变量和类变量 用static修饰的是类变量 不用static修饰的是实例变量 所有类的成员变量可以通过this来引用 2 局部变量 包
  • arthas底层实现原理剖析

    前言 经常在应用的启动或者运行过程中需要动态的查看数据 或者实时的验证我们写的代码的结构与执行过程 此时需要一种工具能够动态的检测程序运行的状态 内存数据 线程情况 最好能够动态的替换代码实时生效 方便我们从日志或者其他埋点断言我们的猜测