arthas启动-attach流程

5 篇文章 2 订阅
1 篇文章 0 订阅

arthas启动-attach流程

上一篇文档写到arthas启动的两种方式, 无论通过执行java -jar arthas-boot.jar 或者执行./as.sh 最终的效果都是会执行java -jar arthas-core.jar的逻辑, 本文我们着重看看启动arthas-core的逻辑实现。 本文的理解需要对java agent技术有一定的了解,有需要的请自行google查询一下。

大家应该都知道,启动java -jar xx.jar 不指定mainclass的时候取的是jar包中的META-INF/MANIFEST.MF文件。我们解压arthas-core.jar可以看到main-class是com.taobao.arthas.core.Arthas, 我们启动arthas-core.jar 是启动com.taobao.arthas.core.Arthas的main函数

image.png

该类功能主要做的就是使用agent技术,将arthas-agent attach到对应jvm进程中, 具体实现如下:

Arthas#parse方法

上篇文章我们看到,最后启动的命令行是 java -jar arthas-core.jar -core arthas-core.jar -agent arthas-agent.jar -pid pid,自然而然第一个阶段需要将传入的参数列表解析成Arthas类所需要的,此处的实现比较通用的实现方法,具体实现如下:

private Configure parse(String[] args) {
                // 处理参数信息
        Option pid = new TypedOption<Long>().setType(Long.class).setShortName("pid").setRequired(true);
        Option core = new TypedOption<String>().setType(String.class).setShortName("core").setRequired(true);
        Option agent = new TypedOption<String>().setType(String.class).setShortName("agent").setRequired(true);
        Option target = new TypedOption<String>().setType(String.class).setShortName("target-ip");
        Option telnetPort = new TypedOption<Integer>().setType(Integer.class).setShortName("telnet-port");
        Option httpPort = new TypedOption<Integer>().setType(Integer.class).setShortName("http-port");
        Option sessionTimeout = new TypedOption<Integer>().setType(Integer.class).setShortName("session-timeout");
        Option username = new TypedOption<String>().setType(String.class).setShortName("username");
        Option password = new TypedOption<String>().setType(String.class).setShortName("password");
        Option tunnelServer = new TypedOption<String>().setType(String.class).setShortName("tunnel-server");
        Option agentId = new TypedOption<String>().setType(String.class).setShortName("agent-id");
        Option appName = new TypedOption<String>().setType(String.class).setShortName(ArthasConstants.APP_NAME);
        Option statUrl = new TypedOption<String>().setType(String.class).setShortName("stat-url");
        //构造cli解析器
        CLI cli = CLIs.create("arthas").addOption(pid).addOption(core).addOption(agent).addOption(target).addOption(telnetPort).addOption(httpPort).addOption(sessionTimeout)
                .addOption(username).addOption(password)
                .addOption(tunnelServer).addOption(agentId).addOption(appName).addOption(statUrl);
        CommandLine commandLine = cli.parse(Arrays.asList(args));
        // 组装config对象
        Configure configure = new Configure();
        configure.setJavaPid((Long) commandLine.getOptionValue("pid"));
        configure.setArthasAgent((String) commandLine.getOptionValue("agent"));
        configure.setArthasCore((String) commandLine.getOptionValue("core"));
        if (commandLine.getOptionValue("session-timeout") != null) {
            configure.setSessionTimeout((Integer) commandLine.getOptionValue("session-timeout"));
        }
        if (commandLine.getOptionValue("target-ip") != null) {
            configure.setIp((String) commandLine.getOptionValue("target-ip"));
        }
        if (commandLine.getOptionValue("telnet-port") != null) {
            configure.setTelnetPort((Integer) commandLine.getOptionValue("telnet-port"));
        }
        if (commandLine.getOptionValue("http-port") != null) {
            configure.setHttpPort((Integer) commandLine.getOptionValue("http-port"));
        }
        configure.setUsername((String) commandLine.getOptionValue("username"));
        configure.setPassword((String) commandLine.getOptionValue("password"));
        configure.setTunnelServer((String) commandLine.getOptionValue("tunnel-server"));
        configure.setAgentId((String) commandLine.getOptionValue("agent-id"));
        configure.setStatUrl((String) commandLine.getOptionValue("stat-url"));
        configure.setAppName((String) commandLine.getOptionValue(ArthasConstants.APP_NAME));
        return configure;
    }

整体这个地方的逻辑比较简单,就是将启动的arthas-core的参数列表转换为configure的对象。其实无论arthas-boot的启动或者 arthas-core的启动逻辑中都需要解析参数的,大体实现逻辑一致

Arthas#attachAgent方法

参数解析后,就需要进去attach的关键步骤,这几个步骤是我们平时实现jvm启动后attach的通用步骤。

1、通过传入的进程ID找到对应的找到虚拟的机的描述信息

2、调用VirtualMachineDescriptor#attach方法找到对应的虚拟机信息

3、调用VirtualMachine#loadAgent加载arthas实现的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);
            }

            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()));
            virtualMachine.loadAgent(arthasAgentPath,
                    configure.getArthasCore() + ";" + configure.toString());
        } finally {
            if (null != virtualMachine) {
                virtualMachine.detach();
            }
        }
    }

上面加载的agent的目录,对应的arthas-agent.jar的目录,为什么需要传入arthas-agent.jar, 以及arthas-agent.jar 有什么要求。想理解这个就需要去搜索了解java agent技术。 本文描述下agent所需要的配置项,我们看arthas项目的agent子module的pom.xml 可以看到manifest配置项:

1、Premain-Class ,定义jvm启动过程中加载的agent类, 通常可以使用java -javaagent:agentpath=args ..... 调用的方式,在程序启动过程中触发Premain-Class对应的class中的premain方法

2、Agent-Class,   如果jvm已经启动的情况下,可以使用动态attach的agent的方式,配置的class文件中重写agentmain方法,使用VirtualMachine.loadAgent(agent, args)的方式

3、Can-Redefine-Classes, 加载agent后是否可以redefine class, redefine的实现见java.lang.instrument.Instrumentation的定义

4、Can-Retransform-Classes, 加载的agent是否可以Retransform class, retransform的定义见java.lang.instrument.Instrumentation的定义

 <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中结合启动方式可以看到,执行完VirtualMachine#loadAgent之后相当于,对应的进程中会去加载agent,同时调用com.taobao.arthas.agent334.AgentBootstrap#premain函数。

AgentBootstrap#agentmain方法

我们看AgentBootstrap的代码可以看到,无论是agentmain/premain函数实现都是通过调用其中的实现的main函数实现的, 其中的基本流程如下:

1、检测arthas是否已经加载过了, 通过Class.forName("java.arthas.SpyAPI"), 抛出ClassNotFoundException的话代表未初始化过arthas

2、解析参数,从上一步我们看到。传入的参数其实是 arthas-core.jar的路径+";"+configure.toString的信息,此处需要获取到arthas-core.jar的路径

  • 1: 如果arthas-core.jar在args中可以获取到,并且文件存在的话就使用获取的文件
  • 2: 如果参数中不包含arthas-core.jar的信息时,尝试从arthas-agent.jar的目录中查找是否存在arthas-core.jar的文件

3、执行bind函数, bind函数的实现比较简单,就是反射创建com.taobao.arthas.core.server.ArthasBootstrap的对象(构造函数中会进行一些列初始话操作),通过调用其isBind方法判断是否初始初始化成功

 public static void agentmain(String args, Instrumentation inst) {
        main(args, inst);
 }
 private static synchronized void main(String args, final Instrumentation inst) {
    try {
        Class.forName("java.arthas.SpyAPI"); // 加载不到会抛异常
        if (SpyAPI.isInited()) {
            ps.println("Arthas server already stared, skip attach.");
            ps.flush();
            return;
        }
    } catch (Throwable e) {
    }
    try {
        ps.println("Arthas server agent start...");
        // 传递的args参数分两个部分:arthasCoreJar路径和agentArgs, 分别是Agent的JAR包路径和期望传递到服务端的参数
        if (args == null) {
            args = "";
        }
        args = decodeArg(args);
        String arthasCoreJar;
        final String agentArgs;
        int index = args.indexOf(';');
        if (index != -1) {
            arthasCoreJar = args.substring(0, index);
            agentArgs = args.substring(index);
        } else {
            arthasCoreJar = "";
            agentArgs = args;
        }
        File arthasCoreJarFile = new File(arthasCoreJar);
        if (!arthasCoreJarFile.exists()) {
            //... 从arthas-agent.jar的目录中查找arthas-code.jar
        }
        if (!arthasCoreJarFile.exists()) {
            return;
        }
        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);
                }
            }
        };
        bindingThread.setName("arthas-binding-thread");
        bindingThread.start();
        bindingThread.join();
    } catch (Throwable t) {
        t.printStackTrace(ps);
        try {
            if (ps != System.err) {
                ps.close();
            }
        } catch (Throwable tt) {
        }
        throw new RuntimeException(t);
    }
}

疑问

在arthas其中的attach流程整体代码相对简单不复杂,不知道大家有没有考虑过Arthas#attachAgent调用过程中获取的VirtualMachineDescriptor, VirtualMachine。 在java中如何可以通过一个jvm进程获取到另外一个jvm进程的信息,在VirtualMachine#loadAgent的时候启动arthas-core.jar的jvm进程应该会和要目标jvm之间进行通信的。 通信是如何实现的?

大家可以先考虑考虑,下章节我们简单分析下其实现,同时明确下为何上节说的两种问题:

1、启动arthas时一定要求启动arthas的用户和jvm启动的用户一致

2、要求/tmp/hsperfdata_{user}目录下的进程描述文件存在, 以及/tmp/.java{pid}文件的相关内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值