arthas源码刨析:arthas 命令粗谈 dashboard watch retransform (3)

文章目录

前面介绍了 arthas 启动相关的代码并聊了聊怎么到一个 shellserver 的建立。
本篇我们来探讨一下几个使用频次非常高的命令是如何实现的。
在开始之前,我们先概要地了解一下 arthas 命令的几个思路。

  1. 自定义命令,普通信息查询。如 cat、 help、 echo 等等,在 process 中写逻辑,并更新到 XXXModel 类中,process.appendResult(new CatModel(file, fileToString)); 在控制台中打印。
  2. MXBean 如 JvmCommand,MemoryCommond,HeapDumpCommand 等。核心是调用 JVM 提供的 MXBean 接口。
  3. 执行 字节码增强,调用自定义 ClassFileTransformer 的 transform 方法,增强字节码功能或者实现 aop 的 trace。如 retransform,jad 等。

整个命令体系如果拆开了看,也会发现还算是比较简单的:信息获取、功能增强或者 trace。难的还是他的架构如何构建,怎么做到功能解耦,还是很好的学习素材。

dashboard

想看这个命令的主要原因是编程这些年来从来没有开发过 terminal 的这种比较花哨的界面,和 top 命令一样。

java 复制代码
  public void process(final CommandProcess process) {

        Session session = process.session();
        timer = new Timer("Timer-for-arthas-dashboard-" + session.getSessionId(), true);

        // ctrl-C support
        process.interruptHandler(new DashboardInterruptHandler(process, timer));

        /*
         * 通过handle回调,在suspend和end时停止timer,resume时重启timer
         */
        Handler<Void> stopHandler = new Handler<Void>() {
            @Override
            public void handle(Void event) {
                stop();
            }
        };

        Handler<Void> restartHandler = new Handler<Void>() {
            @Override
            public void handle(Void event) {
                restart(process);
            }
        };
        process.suspendHandler(stopHandler);
        process.resumeHandler(restartHandler);
        process.endHandler(stopHandler);

        // q exit support
        process.stdinHandler(new QExitHandler(process));

        // start the timer
        timer.scheduleAtFixedRate(new DashboardTimerTask(process), 0, getInterval());
    }
  1. 获取会话

    java 复制代码
    Session session = process.session();
    • 调用process对象的session方法获取当前会话对象,并赋值给session变量。process
  2. 创建定时器

    java 复制代码
    timer = new Timer("Timer-for-arthas-dashboard-" + session.getSessionId(), true);
    • 创建一个定时器对象,命名为"Timer-for-arthas-dashboard-" + session.getSessionId(),其中session.getSessionId()获取会话ID。 定时器的主要作用是进行界面刷新。
    • 第二个参数true表示这个定时器是守护线程,当所有非守护线程结束时,程序会自动退出。
  3. 处理中断

    java 复制代码
    process.interruptHandler(new DashboardInterruptHandler(process, timer));
    • 设置一个中断处理器DashboardInterruptHandler,用于处理中断信号(如Ctrl+C)。
  4. 处理挂起、恢复和结束事件

    java 复制代码
    Handler<Void> stopHandler = new Handler<Void>() {
        @Override
        public void handle(Void event) {
            stop();
        }
    };
    
    Handler<Void> restartHandler = new Handler<Void>() {
        @Override
        public void handle(Void event) {
            restart(process);
        }
    };
    process.suspendHandler(stopHandler);
    process.resumeHandler(restartHandler);
    process.endHandler(stopHandler);
    • 定义了两个处理器stopHandlerrestartHandler,分别用于处理挂起、恢复和结束事件。
    • stopHandler调用stop方法停止定时器。
    • restartHandler调用restart(process)方法重新启动定时器。
    • 将这些处理器分别注册到process对象的suspendHandlerresumeHandlerendHandler中。
  5. 处理退出命令

    java 复制代码
    process.stdinHandler(new QExitHandler(process));
    • 设置一个标准输入处理器QExitHandler,用于处理退出命令(如输入q)。
  6. 启动定时器

    java 复制代码
    timer.scheduleAtFixedRate(new DashboardTimerTask(process), 0, getInterval());
    • 使用timer对象定期执行DashboardTimerTask任务。
    • 周期性执行 DashboardTimerTask ,统计信息从 jvm 的 MXBean 中获取并组装 dashboardModel 再后面跟踪,就能发现界面绘制是属于 ProcessImpl 对命令结果的处理,发现没有,arthas 是一个很完整的解耦设计,职责分明。如何绘制界面在 view 文件夹,比如该命令

再往后看,界面就是做的以行为单元的字符串,输出到标准输出中,stdout。

watch

下面的 watch 命令在分析性能问题是出场率最高的命令,极大的释放了开发的背锅压力。

WatchCommand 继承了 EnhancerCommand ,下面的代码就是增强的核心代码。一般来说我们都知道使用 javaagent 可以在加载到jvm之前做增强,也就是 premain 方法,但 jvm也提供了 attach 的增强,即 agentmain,这也是在上一篇中提到过的

java 复制代码
protected void enhance(CommandProcess process) {
        Session session = process.session();
        if (!session.tryLock()) {
            String msg = "someone else is enhancing classes, pls. wait.";
            process.appendResult(new EnhancerModel(null, false, msg));
            process.end(-1, msg);
            return;
        }
        EnhancerAffect effect = null;
        int lock = session.getLock();
        try {
            Instrumentation inst = session.getInstrumentation();
            AdviceListener listener = getAdviceListenerWithId(process);
            if (listener == null) {
                logger.error("advice listener is null");
                String msg = "advice listener is null, check arthas log";
                process.appendResult(new EnhancerModel(effect, false, msg));
                process.end(-1, msg);
                return;
            }
            boolean skipJDKTrace = false;
            if(listener instanceof AbstractTraceAdviceListener) {
                skipJDKTrace = ((AbstractTraceAdviceListener) listener).getCommand().isSkipJDKTrace();
            }

            Enhancer enhancer = new Enhancer(listener, listener instanceof InvokeTraceable, skipJDKTrace, getClassNameMatcher(), getClassNameExcludeMatcher(), getMethodNameMatcher());
            // 注册通知监听器
            process.register(listener, enhancer);
            effect = enhancer.enhance(inst, this.maxNumOfMatchedClass);

            if (effect.getThrowable() != null) {
                String msg = "error happens when enhancing class: "+effect.getThrowable().getMessage();
                process.appendResult(new EnhancerModel(effect, false, msg));
                process.end(1, msg + ", check arthas log: " + LogUtil.loggingFile());
                return;
            }

            if (effect.cCnt() == 0 || effect.mCnt() == 0) {
                // no class effected
                if (!StringUtils.isEmpty(effect.getOverLimitMsg())) {
                    process.appendResult(new EnhancerModel(effect, false));
                    process.end(-1);
                    return;
                }
                // might be method code too large
                process.appendResult(new EnhancerModel(effect, false, "No class or method is affected"));

                String smCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("sm CLASS_NAME METHOD_NAME").reset().toString();
                String optionsCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("options unsafe true").reset().toString();
                String javaPackage = Ansi.ansi().fg(Ansi.Color.GREEN).a("java.*").reset().toString();
                String resetCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("reset CLASS_NAME").reset().toString();
                String logStr = Ansi.ansi().fg(Ansi.Color.GREEN).a(LogUtil.loggingFile()).reset().toString();
                String issueStr = Ansi.ansi().fg(Ansi.Color.GREEN).a("https://github.com/alibaba/arthas/issues/47").reset().toString();
                String msg = "No class or method is affected, try:\n"
                        + "1. Execute `" + smCommand + "` to make sure the method you are tracing actually exists (it might be in your parent class).\n"
                        + "2. Execute `" + optionsCommand + "`, if you want to enhance the classes under the `" + javaPackage + "` package.\n"
                        + "3. Execute `" + resetCommand + "` and try again, your method body might be too large.\n"
                        + "4. Match the constructor, use `<init>`, for example: `watch demo.MathGame <init>`\n"
                        + "5. Check arthas log: " + logStr + "\n"
                        + "6. Visit " + issueStr + " for more details.";
                process.end(-1, msg);
                return;
            }

            // 这里做个补偿,如果在enhance期间,unLock被调用了,则补偿性放弃
            if (session.getLock() == lock) {
                if (process.isForeground()) {
                    process.echoTips(Constants.Q_OR_CTRL_C_ABORT_MSG + "\n");
                }
            }

            process.appendResult(new EnhancerModel(effect, true));

            //异步执行,在AdviceListener中结束
        } catch (Throwable e) {
            String msg = "error happens when enhancing class: "+e.getMessage();
            logger.error(msg, e);
            process.appendResult(new EnhancerModel(effect, false, msg));
            process.end(-1, msg);
        } finally {
            if (session.getLock() == lock) {
                // enhance结束后解锁
                process.session().unLock();
            }
        }
    }

这个方法的核心就在这几行代码中:

java 复制代码
			Enhancer enhancer = new Enhancer(listener, listener instanceof InvokeTraceable, skipJDKTrace, getClassNameMatcher(), getClassNameExcludeMatcher(), getMethodNameMatcher());
            // 注册通知监听器
            process.register(listener, enhancer);
            effect = enhancer.enhance(inst, this.maxNumOfMatchedClass);
java 复制代码
/**
     * 对象增强
     *
     * @param inst              inst
     * @param maxNumOfMatchedClass 匹配的class最大数量
     * @return 增强影响范围
     * @throws UnmodifiableClassException 增强失败
     */
    public synchronized EnhancerAffect enhance(final Instrumentation inst, int maxNumOfMatchedClass) throws UnmodifiableClassException {
        // 获取需要增强的类集合
        this.matchingClasses = GlobalOptions.isDisableSubClass
                ? SearchUtils.searchClass(inst, classNameMatcher)
                : SearchUtils.searchSubClass(inst, SearchUtils.searchClass(inst, classNameMatcher));

        if (matchingClasses.size() > maxNumOfMatchedClass) {
            affect.setOverLimitMsg("The number of matched classes is " +matchingClasses.size()+ ", greater than the limit value " + maxNumOfMatchedClass + ". Try to change the limit with option '-m <arg>'.");
            return affect;
        }
        // 过滤掉无法被增强的类
        List<Pair<Class<?>, String>> filtedList = filter(matchingClasses);
        if (!filtedList.isEmpty()) {
            for (Pair<Class<?>, String> filted : filtedList) {
                logger.info("ignore class: {}, reason: {}", filted.getFirst().getName(), filted.getSecond());
            }
        }

        logger.info("enhance matched classes: {}", matchingClasses);

        affect.setTransformer(this);

        try {
            ArthasBootstrap.getInstance().getTransformerManager().addTransformer(this, isTracing);

            // 批量增强
            if (GlobalOptions.isBatchReTransform) {
                final int size = matchingClasses.size();
                final Class<?>[] classArray = new Class<?>[size];
                arraycopy(matchingClasses.toArray(), 0, classArray, 0, size);
                if (classArray.length > 0) {
                    inst.retransformClasses(classArray);
                    logger.info("Success to batch transform classes: " + Arrays.toString(classArray));
                }
            } else {
                // for each 增强
                for (Class<?> clazz : matchingClasses) {
                    try {
                        inst.retransformClasses(clazz);
                        logger.info("Success to transform class: " + clazz);
                    } catch (Throwable t) {
                        logger.warn("retransform {} failed.", clazz, t);
                        if (t instanceof UnmodifiableClassException) {
                            throw (UnmodifiableClassException) t;
                        } else if (t instanceof RuntimeException) {
                            throw (RuntimeException) t;
                        } else {
                            throw new RuntimeException(t);
                        }
                    }
                }
            }
        } catch (Throwable e) {
            logger.error("Enhancer error, matchingClasses: {}", matchingClasses, e);
            affect.setThrowable(e);
        }

        return affect;
    }

增强的关于字节码增强部分在 WatchAdviceListener 中,相当于做了一个 AOP。而这些 AdviceListenerAdapter 的调用链: SpyImpl ---- SpyAPI --- SpyInterceptors --- Enhancer.transform

java 复制代码
class WatchAdviceListener extends AdviceListenerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(WatchAdviceListener.class);
    private final ThreadLocalWatch threadLocalWatch = new ThreadLocalWatch();
    private WatchCommand command;
    private CommandProcess process;

    public WatchAdviceListener(WatchCommand command, CommandProcess process, boolean verbose) {
        this.command = command;
        this.process = process;
        super.setVerbose(verbose);
    }

    private boolean isFinish() {
        return command.isFinish() || !command.isBefore() && !command.isException() && !command.isSuccess();
    }

    @Override
    public void before(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args)
            throws Throwable {
        // 开始计算本次方法调用耗时
        threadLocalWatch.start();
        if (command.isBefore()) {
            watching(Advice.newForBefore(loader, clazz, method, target, args));
        }
    }

    @Override
    public void afterReturning(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args,
                               Object returnObject) throws Throwable {
        Advice advice = Advice.newForAfterReturning(loader, clazz, method, target, args, returnObject);
        if (command.isSuccess()) {
            watching(advice);
        }

        finishing(advice);
    }

    @Override
    public void afterThrowing(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args,
                              Throwable throwable) {
        Advice advice = Advice.newForAfterThrowing(loader, clazz, method, target, args, throwable);
        if (command.isException()) {
            watching(advice);
        }

        finishing(advice);
    }

    private void finishing(Advice advice) {
        if (isFinish()) {
            watching(advice);
        }
    }


    private void watching(Advice advice) {
        try {
            // 本次调用的耗时
            double cost = threadLocalWatch.costInMillis();
            boolean conditionResult = isConditionMet(command.getConditionExpress(), advice, cost);
            if (this.isVerbose()) {
                process.write("Condition express: " + command.getConditionExpress() + " , result: " + conditionResult + "\n");
            }
            if (conditionResult) {
                // TODO: concurrency issues for process.write

                Object value = getExpressionResult(command.getExpress(), advice, cost);

                WatchModel model = new WatchModel();
                model.setTs(new Date());
                model.setCost(cost);
                model.setValue(new ObjectVO(value, command.getExpand()));
                model.setSizeLimit(command.getSizeLimit());
                model.setClassName(advice.getClazz().getName());
                model.setMethodName(advice.getMethod().getName());
                if (advice.isBefore()) {
                    model.setAccessPoint(AccessPoint.ACCESS_BEFORE.getKey());
                } else if (advice.isAfterReturning()) {
                    model.setAccessPoint(AccessPoint.ACCESS_AFTER_RETUNING.getKey());
                } else if (advice.isAfterThrowing()) {
                    model.setAccessPoint(AccessPoint.ACCESS_AFTER_THROWING.getKey());
                }

                process.appendResult(model);
                process.times().incrementAndGet();
                if (isLimitExceeded(command.getNumberOfLimit(), process.times().get())) {
                    abortProcess(process, command.getNumberOfLimit());
                }
            }
        } catch (Throwable e) {
            logger.warn("watch failed.", e);
            process.end(-1, "watch failed, condition is: " + command.getConditionExpress() + ", express is: "
                    + command.getExpress() + ", " + e.getMessage() + ", visit " + LogUtil.loggingFile()
                    + " for more details.");
        }
    }
}

Enhance 是个自定义的 ClassTransformer ,在 Instrumentation 进行 addTransformer 时,将 enhance 增强纳进管理,在 redefined 或者 retransformed 的时候调用 transform 方法。。

Instrumentation 的方法:

retransform

retransform 这个命令的牛逼之处是可以进行灵活的热加载,用过 jrebel 的同学应该比较清除,在开发时能使用热加载,compile 一下修改的功能就能生效在开发时能节省大量的时间。

在arthas 中 jad--mc--retransform 工具链实现热加载也是一个在线上快速修复的一个方法。

retransform 使用时要注意的一点就是 JVM判断是否是同一个类的时候要判断是不是用一个 classloader 加载。不同 classloader 加载的同一个 class 被认为是不同的 2 个类。

相关推荐
Gabriel_wei2 分钟前
腾讯云Ubuntu系统安装宝塔,配置Java环境,运行spring boot项目
java·spring boot·ubuntu·腾讯云·宝塔
积跬步DEV10 分钟前
Claude Prompt 汉语新解
开发语言·前端·javascript
虚无火星车22 分钟前
说说停止线程池的执行流程?
java·开发语言
乔以亦23 分钟前
python 装饰器
linux·开发语言·python
新手小袁_J24 分钟前
SpringBoot整合JDBCTemplate(day34)
java·spring boot·后端·spring·spring cloud·maven
计算机学姐29 分钟前
基于python+django+vue的社区爱心养老管理系统
开发语言·vue.js·后端·python·mysql·django·web3.py
V_fanglue370530 分钟前
qmt量化交易策略小白学习笔记第60期【qmt编程之期权数据--基于BS模型计算欧式期权隐含波动率--内置Python】
开发语言·windows·笔记·python·学习·区块链
Flying_Fish_roe33 分钟前
Reactive 编程-Loom 项目(虚拟线程)
java
汽车电子助手1 小时前
如何制作Vector Vflash中加载的DLL文件--自动解锁刷写过程中27服务
开发语言·单片机·嵌入式硬件
码农不惑1 小时前
如何在C++中使用mupdf操作pdf文件(一)
开发语言·c++·pdf