arthas源碼分析

arthas簡介

arthas 是Alibaba開源的Java診斷工具,基于jvm Agent方式,使用Instrumentation方式修改字節碼方式以及使用java.lang.management包提供的管理接口的方式進行java應用診斷。詳細的介紹可以參考官方文檔。
官方文檔地址:https://alibaba.github.io/arthas/
GitHub地址:https://github.com/alibaba/arthas/
本文主要分析arthas源碼,主要分成下面幾個部分:

  1. arthas組成模塊
  2. arthas服務端代碼分析
  3. arthas客戶端代碼分析

arthas組成模塊

arthas有多個模塊組成,如下圖所示:


arthas模塊圖.png
  1. arthas-boot.jar和as.sh模塊功能類似,分別使用java和shell腳本,下載對應的jar包,并生成服務端和客戶端的啟動命令,然后啟動客戶端和服務端。服務端最終生成的啟動命令如下:
${JAVA_HOME}"/bin/java \
     ${opts}  \
     -jar "${arthas_lib_dir}/arthas-core.jar" \
         -pid ${TARGET_PID} \             要注入的進程id
         -target-ip ${TARGET_IP} \       服務器ip地址
         -telnet-port ${TELNET_PORT} \  服務器telnet服務端口號
         -http-port ${HTTP_PORT} \      websocket服務端口號
         -core "${arthas_lib_dir}/arthas-core.jar" \      arthas-core目錄
         -agent "${arthas_lib_dir}/arthas-agent.jar"    arthas-agent目錄
  1. arthas-core.jar是服務端程序的啟動入口類,會調用virtualMachine#attach到目標進程,并加載arthas-agent.jar作為agent jar包。
  2. arthas-agent.jar既可以使用premain方式(在目標進程啟動之前,通過-agent參數靜態指定),也可以通過agentmain方式(在進程啟動之后attach上去)。arthas-agent會使用自定義的classloader(ArthasClassLoader)加載arthas-core.jar里面的com.taobao.arthas.core.config.Configure類以及com.taobao.arthas.core.server.ArthasBootstrap。 同時程序運行的時候會使用arthas-spy.jar。
  3. arthas-spy.jar里面只包含Spy類,目的是為了將Spy類使用BootstrapClassLoader來加載,從而使目標進程的java應用可以訪問Spy類。通過ASM修改字節碼,可以將Spy類的方法ON_BEFORE_METHODON_RETURN_METHOD等編織到目標類里面。Spy類你可以簡單理解為類似spring aop的Advice,有前置方法,后置方法等。
  4. arthas-client.jar是客戶端程序,用來連接arthas-core.jar啟動的服務端代碼,使用telnet方式。一般由arthas-boot.jar和as.sh來負責啟動。

arthas服務端代碼分析

前置準備

看服務端啟動命令可以知道 從 arthas-core.jar開始啟動,arthas-core的pom.xml文件里面指定了mainClass為com.taobao.arthas.core.Arthas,使得程序啟動的時候從該類的main方法開始運行。Arthas源碼如下:

public class Arthas {

    private Arthas(String[] args) throws Exception {
        attachAgent(parse(args));
    }

    private Configure parse(String[] args) {
        // 省略非關鍵代碼,解析啟動參數作為配置,并填充到configure對象里面
        return configure;
    }

    private void attachAgent(Configure configure) throws Exception {
           // 省略非關鍵代碼,attach到目標進程
          virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
          virtualMachine.loadAgent(configure.getArthasAgent(),
                            configure.getArthasCore() + ";" + configure.toString());
    }


    public static void main(String[] args) {
            new Arthas(args);
    }
}
  1. Arthas首先解析入參,生成com.taobao.arthas.core.config.Configure類,包含了相關配置信息
  2. 使用jdk-tools里面的VirtualMachine.loadAgent,其中第一個參數為agent路徑, 第二個參數向jar包中的agentmain()方法傳遞參數(此處為agent-core.jar包路徑和config序列化之后的字符串),加載arthas-agent.jar包,并運行
  3. arthas-agent.jar包,指定了Agent-Class為com.taobao.arthas.agent.AgentBootstrap,同時可以使用Premain的方式和目標進程同時啟動
<manifestEntries>
    <Premain-Class>com.taobao.arthas.agent.AgentBootstrap</Premain-Class>
    <Agent-Class>com.taobao.arthas.agent.AgentBootstrap</Agent-Class>
</manifestEntries>

其中Premain-ClasspremainAgent-Classagentmain都調用main方法。
main方法主要做4件事情:

  1. 找到arthas-spy.jar路徑,并調用Instrumentation#appendToBootstrapClassLoaderSearch方法,使用bootstrapClassLoader來加載arthas-spy.jar里的Spy類。
  2. arthas-agent路徑傳遞給自定義的classloader(ArthasClassloader),用來隔離arthas本身的類和目標進程的類。
  3. 使用 ArthasClassloader#loadClass方法,加載com.taobao.arthas.core.advisor.AdviceWeaver類,并將里面的methodOnBeginmethodOnReturnEndmethodOnThrowingEnd等方法取出賦值給Spy類對應的方法。同時Spy類里面的方法又會通過ASM字節碼增強的方式,編織到目標代碼的方法里面。使得Spy 間諜類可以關聯由AppClassLoader加載的目標進程的業務類和ArthasClassloader加載的arthas類,因此Spy類可以看做兩者之間的橋梁。根據classloader雙親委派特性,子classloader可以訪問父classloader加載的類。源碼如下:
    private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File agentJarFile) throws Throwable {
        // 將Spy添加到BootstrapClassLoader
        inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));

        // 構造自定義的類加載器ArthasClassloader,盡量減少Arthas對現有工程的侵蝕
        return loadOrDefineClassLoader(agentJarFile);
    }

    private static void initSpy(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException {
        // 該classLoader為ArthasClassloader
        Class<?> adviceWeaverClass = classLoader.loadClass(ADVICEWEAVER);
        Method onBefore = adviceWeaverClass.getMethod(ON_BEFORE, int.class, ClassLoader.class, String.class,
                String.class, String.class, Object.class, Object[].class);
        Method onReturn = adviceWeaverClass.getMethod(ON_RETURN, Object.class);
        Method onThrows = adviceWeaverClass.getMethod(ON_THROWS, Throwable.class);
        Method beforeInvoke = adviceWeaverClass.getMethod(BEFORE_INVOKE, int.class, String.class, String.class, String.class);
        Method afterInvoke = adviceWeaverClass.getMethod(AFTER_INVOKE, int.class, String.class, String.class, String.class);
        Method throwInvoke = adviceWeaverClass.getMethod(THROW_INVOKE, int.class, String.class, String.class, String.class);
        Method reset = AgentBootstrap.class.getMethod(RESET);
        Spy.initForAgentLauncher(classLoader, onBefore, onReturn, onThrows, beforeInvoke, afterInvoke, throwInvoke, reset);
    }


classloader關系如下:

+-BootstrapClassLoader                                                                                                                                                                               
+-sun.misc.Launcher$ExtClassLoader@7bf2dede                                                                                                                                                          
  +-com.taobao.arthas.agent.ArthasClassloader@51a10fc8                                                                                                                                               
  +-sun.misc.Launcher$AppClassLoader@18b4aac2
  1. 異步調用bind方法,該方法最終啟動server監聽線程,監聽客戶端的連接,包括telnet和websocket兩種通信方式。源碼如下:
    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) throws Throwable {
            /**
            * <pre>
            * Configure configure = Configure.toConfigure(args);
            * int javaPid = configure.getJavaPid();
            * ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(javaPid, inst);
            * </pre>
            */
            Class<?> classOfConfigure = agentLoader.loadClass(ARTHAS_CONFIGURE);
            Object configure = classOfConfigure.getMethod(TO_CONFIGURE, String.class).invoke(null, args);
            int javaPid = (Integer) classOfConfigure.getMethod(GET_JAVA_PID).invoke(configure);
            Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
            Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, int.class, Instrumentation.class).invoke(null, javaPid, inst);
            boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
            if (!isBind) {
                try {
                    ps.println("Arthas start to bind...");
                    bootstrapClass.getMethod(BIND, classOfConfigure).invoke(bootstrap, configure);
                    ps.println("Arthas server bind success.");
                    return;
                } catch (Exception e) {
                    ps.println("Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.");
                    throw e;
                }
            }
            ps.println("Arthas server already bind.");
        }

主要做兩件事情:

  • 使用ArthasClassloader加載com.taobao.arthas.core.config.Configure類(位于arthas-core.jar),并將傳遞過來的序列化之后的config,反序列化成對應的Configure對象。
  • 使用ArthasClassloader加載com.taobao.arthas.core.server.ArthasBootstrap類(位于arthas-core.jar),并調用bind方法。

啟動服務器,并監聽客戶端請求

下面重點看下com.taobao.arthas.core.server.ArthasBootstrap#bind方法

    /**
     * Bootstrap arthas server
     *
     * @param configure 配置信息
     * @throws IOException 服務器啟動失敗
     */
    public void bind(Configure configure) throws Throwable {

        long start = System.currentTimeMillis();

        if (!isBindRef.compareAndSet(false, true)) {
            throw new IllegalStateException("already bind");
        }

        try {
            ShellServerOptions options = new ShellServerOptions()
                            .setInstrumentation(instrumentation)
                            .setPid(pid)
                            .setSessionTimeout(configure.getSessionTimeout() * 1000);
            shellServer = new ShellServerImpl(options, this);
            BuiltinCommandPack builtinCommands = new BuiltinCommandPack();
            List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
            resolvers.add(builtinCommands);
            // TODO: discover user provided command resolver
            if (configure.getTelnetPort() > 0) {
                // telnet方式的server
                shellServer.registerTermServer(new TelnetTermServer(configure.getIp(), configure.getTelnetPort(),
                                options.getConnectionTimeout()));
            } else {
                logger.info("telnet port is {}, skip bind telnet server.", configure.getTelnetPort());
            }
            if (configure.getHttpPort() > 0) {
                // websocket方式的server
                shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
                                options.getConnectionTimeout()));
            } else {
                logger.info("http port is {}, skip bind http server.", configure.getHttpPort());
            }

            for (CommandResolver resolver : resolvers) {
                shellServer.registerCommandResolver(resolver);
            }

            shellServer.listen(new BindHandler(isBindRef));

            logger.info("as-server listening on network={};telnet={};http={};timeout={};", configure.getIp(),
                    configure.getTelnetPort(), configure.getHttpPort(), options.getConnectionTimeout());
            // 異步回報啟動次數
            UserStatUtil.arthasStart();

            logger.info("as-server started in {} ms", System.currentTimeMillis() - start );
        } catch (Throwable e) {
            logger.error(null, "Error during bind to port " + configure.getTelnetPort(), e);
            if (shellServer != null) {
                shellServer.close();
            }
            throw e;
        }
    }

可以看到有兩種類型的server,TelnetTermServerHttpTermServer。同時會在BuiltinCommandPack里添加所有的命令Command,添加命令的源碼如下:

public class BuiltinCommandPack implements CommandResolver {

    private static List<Command> commands = new ArrayList<Command>();

    static {
        initCommands();
    }

    @Override
    public List<Command> commands() {
        return commands;
    }

    private static void initCommands() {
        commands.add(Command.create(HelpCommand.class));
        commands.add(Command.create(KeymapCommand.class));
        commands.add(Command.create(SearchClassCommand.class));
        commands.add(Command.create(SearchMethodCommand.class));
        commands.add(Command.create(ClassLoaderCommand.class));
        commands.add(Command.create(JadCommand.class));
        commands.add(Command.create(GetStaticCommand.class));
        commands.add(Command.create(MonitorCommand.class));
        commands.add(Command.create(StackCommand.class));
        commands.add(Command.create(ThreadCommand.class));
        commands.add(Command.create(TraceCommand.class));
        commands.add(Command.create(WatchCommand.class));
        commands.add(Command.create(TimeTunnelCommand.class));
        commands.add(Command.create(JvmCommand.class));
        // commands.add(Command.create(GroovyScriptCommand.class));
        commands.add(Command.create(OgnlCommand.class));
        commands.add(Command.create(DashboardCommand.class));
        commands.add(Command.create(DumpClassCommand.class));
        commands.add(Command.create(JulyCommand.class));
        commands.add(Command.create(ThanksCommand.class));
        commands.add(Command.create(OptionsCommand.class));
        commands.add(Command.create(ClsCommand.class));
        commands.add(Command.create(ResetCommand.class));
        commands.add(Command.create(VersionCommand.class));
        commands.add(Command.create(ShutdownCommand.class));
        commands.add(Command.create(SessionCommand.class));
        commands.add(Command.create(SystemPropertyCommand.class));
        commands.add(Command.create(SystemEnvCommand.class));
        commands.add(Command.create(RedefineCommand.class));
        commands.add(Command.create(HistoryCommand.class));
    }
}

調用shellServer.registerTermServershellServer.registerTermServershellServer.registerCommandResolve 注冊到ShellServer里,ShellServer是整個服務端的門面類,調用listen方法啟動ShellServer
ShellServer會使用一系列的類,細節比較復雜,可以見下面的類圖。

Arthas-服務端類圖.png

ShellServer#listen會調用所有注冊的TermServer的listen方法,比如TelnetTermServer。然后TelnetTermServerlisten方法會注冊一個回調類,該回調類在有新的客戶端連接時會調用TermServerTermHandlerhandle方法處理。

        bootstrap = new NettyTelnetTtyBootstrap().setHost(hostIp).setPort(port);
        try {
            bootstrap.start(new Consumer<TtyConnection>() {
                @Override
                public void accept(final TtyConnection conn) {
                    termHandler.handle(new TermImpl(Helper.loadKeymap(), conn));
                }
            }).get(connectionTimeout, TimeUnit.MILLISECONDS);
            listenHandler.handle(Future.<TermServer>succeededFuture());

該方法會接著調用ShellServerImplhandleTerm方法進行處理,ShellServerImplhandleTerm方法會調用ShellImplreadline方法。該方法會注冊ShellLineHandler作為回調類,服務端接收到客戶端發送的請求行之后,會回調ShellLineHandlerhandle方法處理請求。readline方法源碼如下:

    public void readline(String prompt, Handler<String> lineHandler, Handler<Completion> completionHandler) {
        if (conn.getStdinHandler() != echoHandler) {
            throw new IllegalStateException();
        }
        if (inReadline) {
            throw new IllegalStateException();
        }
        inReadline = true;
        // 注冊回調類RequestHandler,該類包裝了ShellLineHandler,處理邏輯還是在ShellLineHandler類里面
        readline.readline(conn, prompt, new RequestHandler(this, lineHandler), new CompletionHandler(completionHandler, session));
    }

處理客戶端請求

ShellLineHandlerhandle方法會根據不同的請求命令執行不同的邏輯:

  1. 如果是exit,logout,quit, jobs,fg,bg,kill等直接執行。
  2. 如果是其他的命令,則創建Job,并運行。創建Job的類圖如下:


    服務端-創建job類圖.png

    步驟比較多,就不一一細講,總之:

  3. 創建Job時,會根據具體客戶端傳遞的命令,找到對應的Command,并包裝成Process, Process再被包裝成Job。
  4. 運行Job時,反向先調用Process,再找到對應的Command,最終調用Commandprocess處理請求。

Command處理流程

Command主要分為兩類:

  1. 不需要使用字節碼增強的命令
    其中JVM相關的使用 java.lang.management 提供的管理接口,來查看具體的運行時數據。比較簡單,就不介紹了。
  2. 需要使用字節碼增強的命令
    字節碼增強的命令,可以參考下圖:


    arthas-command相關類圖.png

字節碼增加的命令統一繼承EnhancerCommand類,process方法里面調用enhance方法進行增強。調用Enhancerenhance方法,該方法內部調用inst.addTransformer方法添加自定義的ClassFileTransformer,這邊是Enhancer類。

Enhancer類使用AdviceWeaver(繼承ClassVisitor),用來修改類的字節碼。重寫了visitMethod方法,在該方法里面修改類指定的方法。visitMethod方法里面使用了AdviceAdapter(繼承了MethodVisitor類),在onMethodEnter方法, onMethodExit方法中,把Spy類對應的方法(ON_BEFORE_METHODON_RETURN_METHODON_THROWS_METHOD等)編織到目標類的方法對應的位置。

在前面Spy初始化的時候可以看到,這幾個方法其實指向的是AdviceWeaver類的methodOnBeginmethodOnReturnEnd等。在這些方法里面都會根據adviceId查找對應的AdviceListener,并調用AdviceListener的對應的方法,比如before,afterReturning, afterThrowing

通過這種方式,可以實現不同的Command使用不同的AdviceListener,從而實現不同的處理邏輯。下面找幾個常用的AdviceListener介紹下:

  1. StackAdviceListener
    在方法執行前,記錄堆棧和方法的耗時。
  2. WatchAdviceListener
    滿足條件時打印打印參數或者結果,條件表達式使用Ognl語法。
  3. TraceAdviceListener
    在每個方法前后都記錄,并維護一個調用樹結構。

arthas客戶端代碼分析

客戶端代碼在arthas-client模塊里面,入口類是com.taobao.arthas.client.TelnetConsole。主要使用apache commons-net jar進行telnet連接,關鍵的代碼有下面幾步:

  1. 構造TelnetClient對象,并初始化
  2. 構造ConsoleReader對象,并初始化
  3. 調用IOUtil.readWrite(telnet.getInputStream(), telnet.getOutputStream(), System.in, consoleReader.getOutput())處理各個流,一共有四個流:
  • telnet.getInputStream()
  • telnet.getOutputStream()
  • System.in
  • consoleReader.getOutput()

請求時:從本地System.in讀取,發送到 telnet.getOutputStream(),即發送給遠程服務端。
響應時:從telnet.getInputStream()讀取遠程服務端發送過來的響應,并傳遞給 consoleReader.getOutput(),即在本地控制臺輸出。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容