专栏名称: 朱小厮的博客
著有畅销书:《深入理解Kafka》和《RabbitMQ实战指南》。公众号主要用来分享Java技术栈、Golang技术栈、消息中间件(如Kafka、RabbitMQ)、存储、大数据以及通用型技术架构等相关的技术。
目录
相关文章推荐
军武次位面  ·  每日囧图丨快把这根香肠给它舔一舔! ·  4 天前  
中国兵器工业集团  ·  北方公司携多款新产品亮相第十七届阿布扎比国际防务展 ·  5 天前  
51好读  ›  专栏  ›  朱小厮的博客

6到飞起的阿里Java诊断工具Arthas

朱小厮的博客  · 公众号  ·  · 2019-04-24 08:21

正文

点击上方“ 朱小厮的博客 ”,选择“ 设为星标

做积极的人,而不是积极废人!


记得前段时间遇到了一个页面加载过长的问题,当时就想排查下在哪一步消耗的时间比较长,由于是线上问题,第一反应就是有没有什么办法可以无侵入式的查询调用链路耗时呢?

这时 Arthas 走进了我的眼帘,并成功帮我定位到了问题,就是这样引起了我对 Arthas 的兴趣,于是花了点时间对 Arthas 作了一个了解。

什么是 Arthas

摘录一段官方 Github 上的简介

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:


  • 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?

  • 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?

  • 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?

  • 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!

  • 是否有一个全局视角来查看系统的运行状况?

  • 有什么办法可以监控到JVM的实时运行状态?


Arthas 支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

开源地址:https://github.com/alibaba/arthas/

Arthas 基于哪些工具开发而来

greys-anatomy : Arthas代码基于Greys二次开发而来,非常感谢Greys之前所有的工作,以及Greys原作者对Arthas提出的意见和建议!

termd : Arthas的命令行实现基于termd开发,是一款优秀的命令行程序开发框架,感谢termd提供了优秀的框架。

crash : Arthas的文本渲染功能基于crash中的文本渲染功能开发,可以从这里看到源码,感谢crash在这方面所做的优秀工作。

cli : Arthas的命令行界面基于vert.x提供的cli库进行开发,感谢vert.x在这方面做的优秀工作。

compiler : Arthas里的内存编绎器代码来源

Apache Commons Net : Arthas里的Telnet Client代码来源

JavaAgent :运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行 premain 方法然后再执行 main 方法

ASM :一个通用的Java字节码操作和分析框架。它可以用于修改现有的类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从它们构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但是主要关注性能。因为它被设计和实现得尽可能小和快,所以非常适合在动态系统中使用(当然也可以以静态方式使用,例如在编译器中)

工程目录

arthas-agent :基于JavaAgent技术的代理

bin :一些启动脚本

arthas-boot :Java版本的一键安装启动脚本

arthas-client :telnet client代码

arthas-common :一些共用的工具类和枚举类

arthas-core :核心库,各种arthas命令的交互和实现

arthas-demo :示例代码

arthas-memorycompiler :内存编绎器代码

arthas-packaging :maven打包相关的

arthas-site :arthas站点

arthas-spy :编织到目标类中的各个切面

static :静态资源

arthas-testcase :测试

整体流程

首先我们先放出一张整体宏观的模块调用图,下面我们会按照整个 Arthas 启动流程逐步分析,红色部分本篇文章将不涉及,会在后续文章中单独分析

启动方式介绍

使用 arthas-boot 启动(推荐)

下载 arthas-boot.jar,然后用 java -jar 的方式启动:

wget




    
 https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar

可以加 -h 参数,打印帮助信息:

java -jar arthas-boot.jar -h

如果下载速度比较慢,可以使用aliyun的镜像:

java -jar arthas-boot.jar --repo-mirror aliyun --use-http

使用 as.sh 脚本启动

Arthas 支持在 Linux/Unix/Mac 等平台上一键安装,请复制以下内容,并粘贴到命令行中,敲回车执行即可:

curl -L https://alibaba.github.io/arthas/install.sh | sh

上述命令会下载启动脚本文件 as.sh 到当前目录,你可以放在任何地方或将其加入到 $PATH 中。

直接在shell下面执行 ./as.sh ,就会进入交互界面。

也可以执行 ./as.sh -h 来获取更多参数信息。

Arthas 是如何启动的

既然官方推荐用 arthas-boot 启动,那下面我们就一起来看下 arthas-boot 是如何启动的。

首先我们在 arthas-boot 的 pom 文件中找到启动类:

<archive>
    <manifest>
        <mainClass>com.taobao.arthas.boot.BootstrapmainClass>
    manifest>
    <manifestEntries>
        <Created-By>core engine team, middleware group, alibaba inc.Created-By>
        <Specification-Title>${project.name}Specification-Title>
        <Specification-Version>${project.version}Specification-Version>
        <Implementation-Title>${project.name}Implementation-Title>
        <Implementation-Version >${project.version}Implementation-Version>
    manifestEntries>
archive>

从pom文件中,我们可以发现arthas-boot的启动类为 com.taobao.arthas.boot.Bootstrap ,下面我们就去看看 Bootstrap 是如何启动 arthas 的,有兴趣的同学也可以自行看下另外一种启动方式 as.sh

归然将整个启动的过程全部通过注释在代码中体现出来了,所以:

以下代码超级长,慎入!

以下代码超级长,慎入!

以下代码超级长,慎入!

public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException,
                    ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException,
                    IllegalArgumentException, InvocationTargetException 
{
        Package bootstrapPackage = Bootstrap.class.getPackage();
        if (bootstrapPackage != null) {
            String arthasBootVersion = bootstrapPackage.getImplementationVersion();
            if (arthasBootVersion != null) {
                AnsiLog.info("arthas-boot version: " + arthasBootVersion);
            }
        }

        String mavenMetaData = null;

        Bootstrap bootstrap = new Bootstrap();

        //解析类中所有带@Option、@Argument的方法,初始化CLI
        CLI cli = CLIConfigurator.define(Bootstrap.class);
        //解析用户的输入参数,初始化所有命令行参数
        CommandLine commandLine = cli.parse(Arrays.asList(args));

        try {
            CLIConfigurator.inject(commandLine, bootstrap);
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(usage(cli));
            System.exit(1);
        }
        //设置日志级别
        if (bootstrap.isVerbose()) {
            AnsiLog.level(Level.ALL);
        }
        //根据启动参数,判断是否是查看帮助(-h或者--help),如果是查看帮助,则打印usage
        if (bootstrap.isHelp()) {
            System.out.println(usage(cli));
            System.exit(0);
        }

        if  (bootstrap.getRepoMirror() == null || bootstrap.getRepoMirror().trim().isEmpty()) {
            bootstrap.setRepoMirror("center");
            // 如果在国内,则设置maven源为aliyun
            if (TimeUnit.MILLISECONDS.toHours(TimeZone.getDefault().getOffset(System.currentTimeMillis())) == 8) {
                bootstrap.setRepoMirror("aliyun");
            }
        }
        AnsiLog.debug("Repo mirror:" + bootstrap.getRepoMirror());
        //如果启动参数为查看版本,则显示版本
        if (bootstrap.isVersions()) {
            if (mavenMetaData == null) {
                mavenMetaData = DownloadUtils.readMavenMetaData(bootstrap.getRepoMirror(), bootstrap.isuseHttp());
            }
            System.out.println(UsageRender.render(listVersions(mavenMetaData)));
            System.exit(0);
        }
        //判断当前环境是否JDK6或者JDK7,如果是,则只支持Http方式启动
        if (JavaVersionUtils.isJava6() || JavaVersionUtils.isJava7()) {
            bootstrap.setuseHttp(true);
            AnsiLog.debug("Java version is {}, only support http, set useHttp to true.",
                            JavaVersionUtils.javaVersionStr());
        }

        //检查http和telnet端口是否被占用
        int telnetPortPid = -1;
        int httpPortPid = -1;
        if (bootstrap.getTelnetPort() > 0) {
            telnetPortPid = SocketUtils.findTcpListenProcess(bootstrap.getTelnetPort());
            if (telnetPortPid > 0) {
                AnsiLog.info("Process {} already using port {}", telnetPortPid, bootstrap.getTelnetPort());
            }
        }
        if (bootstrap.getHttpPort() > 0) {
            httpPortPid = SocketUtils.findTcpListenProcess(bootstrap.getHttpPort());
            if (httpPortPid > 0) {
                AnsiLog.info("Process {} already using port {}", httpPortPid, bootstrap.getHttpPort());
            }
        }
        //获取用户指定进程ID,如果没有指定,默认取Telnet端口占用的进程ID
        int pid = bootstrap.getPid();
        // select pid
        if (pid 0) {
            try {
                pid = ProcessUtils.select(bootstrap.isVerbose(), telnetPortPid);
            } catch (InputMismatchException e) {
                System.out.println("Please input an integer to select pid." );
                System.exit(1);
            }
            if (pid 0) {
                System.out.println("Please select an available pid.");
                System.exit(1);
            }
        }
        //校验目标进程ID与Telnet或者http端口是否一致
        if (telnetPortPid > 0 && pid != telnetPortPid) {
            AnsiLog.error("Target process {} is not the process using port {}, you will connect to an unexpected process.",
                            pid, bootstrap.getTelnetPort());
            AnsiLog.error("1. Try to restart arthas-boot, select process {}, shutdown it first.",
                            telnetPortPid);
            AnsiLog.error("2. Or try to use different telnet port, for example: java -jar arthas-boot.jar --telnet-port 9998 --http-port -1");
            System.exit(1);
        }

        if (httpPortPid > 0 && pid != httpPortPid) {
            AnsiLog.error("Target process {} is not the process using port {}, you will connect to an unexpected process.",
                            pid, bootstrap.getHttpPort());
            AnsiLog.error("1. Try to restart arthas-boot, select process {}, shutdown it first.",
                            httpPortPid);
            AnsiLog.error("2. Or try to use different http port, for example: java -jar arthas-boot.jar --telnet-port 9998 --http-port 9999", httpPortPid);
            System.exit(1);
        }

        //校验arthas目录中是否存在"arthas-core.jar", "arthas-agent.jar", "arthas-spy.jar"
        File arthasHomeDir = null;
        if (bootstrap.getArthasHome() != null) {
            verifyArthasHome(bootstrap.getArthasHome());
            arthasHomeDir = new File(bootstrap.getArthasHome());
        }
        //指定版本的处理过程
        if (arthasHomeDir == null && bootstrap.getUseVersion() != null) {
            // try to find from ~/.arthas/lib
            File specialVersionDir = new File(System.getProperty("user.home"), ".arthas" + File.separator + "lib"
                            + File.separator + bootstrap.getUseVersion() + File.separator + "arthas");
            if (!specialVersionDir.exists()) {
                // try to download arthas from remote server.
                DownloadUtils.downArthasPackaging(bootstrap.getRepoMirror(), bootstrap.isuseHttp(),
                                bootstrap.getUseVersion(), ARTHAS_LIB_DIR.getAbsolutePath());
            }
            verifyArthasHome(specialVersionDir.getAbsolutePath());
            arthasHomeDir = specialVersionDir;
        }

        //如果在上面都没有确认arthas home,则获取当前Jar包的父目录
        if  (arthasHomeDir == null) {
            CodeSource codeSource = Bootstrap.class.getProtectionDomain().getCodeSource();
            if (codeSource != null) {
                try {
                    // https://stackoverflow.com/a/17870390
                    File bootJarPath = new File(codeSource.getLocation().toURI().getSchemeSpecificPart());
                    verifyArthasHome(bootJarPath.getParent());
                    arthasHomeDir = bootJarPath.getParentFile();
                } catch (Throwable e) {
                    // ignore
                }

            }
        }


        //如果仍然没有确定arthas home,则查看ARTHAS_LIB_DIR下是否存在本地版本,并获取远程的最新版本号,对比本地和远程,如果本地版本低于远程,则尝试去下载远端最新版本
        if (arthasHomeDir == null) {
            boolean checkFile =  ARTHAS_LIB_DIR.exists() || ARTHAS_LIB_DIR.mkdirs();
            if(!checkFile){
                AnsiLog.error("cannot create directory {}: maybe permission denied", ARTHAS_LIB_DIR.getAbsolutePath());
                System.exit(1);
            }

            /**
             * 

             * 1. get local latest version
             * 2. get remote latest version
             * 3. compare two version
             * 

             */

            List versionList = listNames(ARTHAS_LIB_DIR);
            Collections.sort(versionList);

            String localLastestVersion = null;
            if (!versionList.isEmpty()) {
                localLastestVersion = versionList.get(versionList.size() - 1);
            }

            if (mavenMetaData == null) {
                mavenMetaData = DownloadUtils.readMavenMetaData(bootstrap.getRepoMirror(), bootstrap.isuseHttp());
            }

            String remoteLastestVersion = DownloadUtils.readMavenReleaseVersion(mavenMetaData);

            boolean needDownload = false;
            if (localLastestVersion == null) {
                if (remoteLastestVersion == null) {
                    // exit
                    AnsiLog.error("Can not find Arthas under local: {} and remote: {}", ARTHAS_LIB_DIR,
                                    bootstrap.getRepoMirror());
                    System.exit(1);
                } else {
                    needDownload = true ;
                }
            } else {
                if (remoteLastestVersion != null) {
                    if (localLastestVersion.compareTo(remoteLastestVersion) 0) {
                        AnsiLog.info("local lastest version: {}, remote lastest version: {}, try to download from remote.",
                                        localLastestVersion, remoteLastestVersion);
                        needDownload = true;
                    }
                }
            }
            if (needDownload) {
                // try to download arthas from remote server.
                DownloadUtils.downArthasPackaging(bootstrap.getRepoMirror(), bootstrap.isuseHttp(),
                                remoteLastestVersion, ARTHAS_LIB_DIR.getAbsolutePath());
                localLastestVersion = remoteLastestVersion;
            }

            // get the latest version
            arthasHomeDir = new File(ARTHAS_LIB_DIR, localLastestVersion + File.separator + "arthas");
        }
        //再次校验相关jar是否存在
        verifyArthasHome(arthasHomeDir.getAbsolutePath());

        AnsiLog.info("arthas home: " + arthasHomeDir);
        //启动arthas-core.jar,并指定agent
        if (telnetPortPid > 0 && pid == telnetPortPid) {
            AnsiLog.info("The target process already listen port {}, skip attach.", bootstrap.getTelnetPort());
        } else {
            // start arthas-core.jar
            List attachArgs = new ArrayList();
            attachArgs.add("-jar");
            attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
            attachArgs.add("-pid");
            attachArgs.add("" + pid);
            attachArgs.add("-target-ip");
            attachArgs.add(bootstrap.getTargetIp());
            attachArgs.add("-telnet-port");
            attachArgs.add("" + bootstrap.getTelnetPort());
            attachArgs.add("-http-port");
            attachArgs.add("" + bootstrap.getHttpPort());
            attachArgs.add("-core");
            attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
            attachArgs.add("-agent");
            attachArgs.add(new File(arthasHomeDir, "arthas-agent.jar").getAbsolutePath());
            if (bootstrap.getSessionTimeout() != null) {
                attachArgs.add("-session-timeout" );
                attachArgs.add("" + bootstrap.getSessionTimeout());
            }

            AnsiLog.info("Try to attach process " + pid);
            AnsiLog.debug("Start arthas-core.jar args: " + attachArgs);
            ProcessUtils.startArthasCore(pid, attachArgs);

            AnsiLog.info("Attach process {} success.", pid);
        }
        //判断是否只做attach操作,如果只做attach,则不连接
        if (bootstrap.isAttachOnly()) {
            System.exit(0);
        }

        // 启动telnet客户端
        URLClassLoader classLoader = new URLClassLoader(
                        new URL[] { new File(arthasHomeDir, "arthas-client.jar").toURI().toURL() });
        Class> telnetConsoleClas = classLoader.loadClass("com.taobao.arthas.client.TelnetConsole");
        Method mainMethod = telnetConsoleClas.getMethod("main", String[].class);
        List telnetArgs = new ArrayList();

        if (bootstrap.getCommand() != null) {
            telnetArgs.add("-c");
            telnetArgs.add(bootstrap.getCommand());
        }
        if (bootstrap.getBatchFile() != null) {
            telnetArgs.add("-f");
            telnetArgs.add(bootstrap.getBatchFile());
        }
        if (bootstrap.getHeight() != null) {
            telnetArgs.add("--height");
            telnetArgs.add("" + bootstrap.getHeight());
        }
        if (bootstrap.getWidth() != null) {
            telnetArgs.add("--width");
            telnetArgs.add("" + bootstrap.getWidth());
        }

        // telnet port ,ip
        telnetArgs.add(bootstrap.getTargetIp());
        telnetArgs.add("" + bootstrap.getTelnetPort());

        AnsiLog.info("arthas-client connect {} {}", bootstrap.getTargetIp(), bootstrap.getTelnetPort());
        AnsiLog.debug("Start arthas-client.jar args: " + telnetArgs);
        mainMethod.invoke(nullnew Object[] { telnetArgs.toArray(new String[0]) });
    }

到此,Arthas 的启动流程就结束了,在这其中,我们发现了两个关键的 jar 包,arthas-core 和 arthas-agent,那么这两个jar又做了什么事情呢,咱们继续往下走,想要了解这两个jar包的作用,首先我们要先普及一个知识点——Java探针。







请到「今天看啥」查看全文