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

上一篇文章 中通过对一次线上事故的复盘,引出了福报厂的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.AgentBootstrapPremain-Class>
    <Agent-Class>com.taobao.arthas.agent334.AgentBootstrapAgent-Class>
    <Can-Redefine-Classes>trueCan-Redefine-Classes>
    <Can-Retransform-Classes>trueCan-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 来实现的字节码级别的功能织入…

你可能感兴趣的:(架构,编码,设计模式,源码,java,agent,instrument,动态字节码,源码,arthas)