本篇文章是個人「三同」好朋友歸然原創,三同是「同小區」、「前同事」、「現同事」的意思,歸然是他的花名。html
個人花名是逅弈,這是我從大學開始就沿用至今的一個暱稱,是當時和女友玩QQ炫舞,取的情侶暱稱,女友如今已經成了我老婆。java
言歸正傳,本篇文章咱們要了解的是一個線上問題排查利器:Arthas。我也用它在預發環境排查過問題,確實很是方便,當時也想着寫一篇文章總結一下,結果一拖就沒了下文。git
如今歸然寫了一篇很是詳盡的介紹 Arthas 的文章,我看過以後以爲很好,因此厚着臉皮讓他把文章在我公衆號上發佈,讓更多的同窗也可以學習到,但願對你們有所幫助。github
熟悉個人人都知道,個人公衆號內都是原創文章,也沒有作過任何的商業互吹、互推來吸引粉絲,也沒有爲了吸引關注作一些福利活動,徹底是自由發展。我相信專一於原創,用心寫好每一篇文章,能爲讀者帶來實質性的幫助,這樣就很好。算法
============如下是原文=========shell
記得前段時間遇到了一個頁面加載過長的問題,當時就想排查下在哪一步消耗的時間比較長,因爲是線上問題,第一反應就是有沒有什麼辦法能夠無侵入式的查詢調用鏈路耗時呢?apache
這時 Arthas 走進了個人眼簾,併成功幫我定位到了問題,就是這樣引發了我對 Arthas 的興趣,因而花了點時間對 Arthas 做了一個瞭解。bootstrap
摘錄一段官方 Github 上的簡介api
Arthas 是Alibaba開源的Java診斷工具,深受開發者喜好。網絡
當你遇到如下相似問題而一籌莫展時,Arthas 能夠幫助你解決:
- 這個類從哪一個 jar 包加載的?爲何會報各類類相關的 Exception?
- 我改的代碼爲何沒有執行到?難道是我沒 commit?分支搞錯了?
- 遇到問題沒法在線上 debug,難道只能經過加日誌再從新發布嗎?
- 線上遇到某個用戶的數據處理有問題,但線上一樣沒法 debug,線下沒法重現!
- 是否有一個全局視角來查看系統的運行情況?
- 有什麼辦法能夠監控到JVM的實時運行狀態?
Arthas 支持JDK 6+,支持Linux/Mac/Windows,採用命令行交互模式,同時提供豐富的 Tab 自動補全功能,進一步方便進行問題的定位和診斷。
開源地址:https://github.com/alibaba/arthas/
arthas-agent
:基於JavaAgent技術的代理bin
:一些啓動腳本arthas-boot
:Java版本的一鍵安裝啓動腳本arthas-client
:telnet client代碼arthas-common
:一些共用的工具類和枚舉類arthas-core
:核心庫,各類arthas命令的交互和實現arthas-demo
:示例代碼arthas-memorycompiler
:內存編繹器代碼,Fork from https://github.com/skalogs/SkaETL/tree/master/compilerarthas-packaging
:maven打包相關的arthas-site
:arthas站點arthas-spy
:編織到目標類中的各個切面static
:靜態資源arthas-testcase
:測試首先咱們先放出一張總體宏觀的模塊調用圖,下面咱們會按照整個 Arthas 啓動流程逐步分析,紅色部分本篇文章將不涉及,會在後續文章中單獨分析
下載 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
Arthas 支持在 Linux/Unix/Mac 等平臺上一鍵安裝,請複製如下內容,並粘貼到命令行中,敲回車執行便可:
curl -L https://alibaba.github.io/arthas/install.sh | sh
上述命令會下載啓動腳本文件 as.sh 到當前目錄,你能夠放在任何地方或將其加入到 $PATH 中。
直接在shell下面執行 ./as.sh
,就會進入交互界面。
也能夠執行 ./as.sh -h
來獲取更多參數信息。
既然官方推薦用 arthas-boot 啓動,那下面咱們就一塊兒來看下 arthas-boot 是如何啓動的。
首先咱們在 arthas-boot 的 pom 文件中找到啓動類:
<archive> <manifest> <mainClass>com.taobao.arthas.boot.Bootstrap</mainClass> </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); } /** * <pre> * 1. get local latest version * 2. get remote latest version * 3. compare two version * </pre> */ List<String> 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<String> attachArgs = new ArrayList<String>(); 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<String> telnetArgs = new ArrayList<String>(); 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(null, new Object[] { telnetArgs.toArray(new String[0]) }); }
到此,Arthas 的啓動流程就結束了,在這其中,咱們發現了兩個關鍵的 jar 包,arthas-core 和 arthas-agent,那麼這兩個jar又作了什麼事情呢,我們繼續往下走,想要了解這兩個jar包的做用,首先咱們要先普及一個知識點——Java探針。
Java探針主要涉及兩個知識點:
JavaAgent 是一種可以在不影響正常編譯的狀況下,修改字節碼的技術。java做爲一種強類型的語言,不經過編譯就不能可以進行jar包的生成。
有了 JavaAgent 技術,就能夠在字節碼這個層面對類和方法進行修改。也能夠把 JavaAgent 理解成一種代碼注入的方式,可是這種注入比起 Spring的 AOP 更加的優美。
從JDK6開始,有兩種代理方式:
經過命令行(-javaagent)的形式在應用程序啓動前處理(premain方式)
在應用程序啓動後的某個時機處理(agentmain方式)
ASM 是一個通用的 Java 字節碼操做和分析框架,它能夠用於修改現有類或直接以二進制形式動態生成類。
ASM 提供了一些常見的字節碼轉換和分析算法,能夠從中構建自定義複雜轉換和代碼分析工具。
ASM 提供與其餘Java字節碼框架相似的功能,但專一於性能。由於它的設計和實現儘量小並且快,因此它很是適合在動態系統中使用(但固然也能夠以靜態方式使用,例如在編譯器中)。
ASM 用於許多項目,包括:
明白了這兩個知識點後,咱們一塊兒來看下 Arthas 中的 JavaAgent——arthas-agent
首先咱們從Pom文件看起,找到premain和agentmain
從這裏咱們很清楚地看到了 premain 和 agentmain 的方法被放在了com.taobao.arthas.agent.AgentBootstrap
中。
那麼接下來咱們就走進 AgentBootstrap
類中,瞭解下它的實現。
在 AgentBootstrap
類中,咱們很快發現了這兩個方法
這兩個方法都同時指向當前類中的main方法,並傳遞了兩個參數,下面咱們先對着兩個參數作個解讀
這個參數是咱們在 arthas-boot.jar 中啓動 arthas-core.jar 時傳入的參數
java.lang.instrument.Instrumentation 實例,由 JVM 自動傳入,集中了幾乎全部功能方法,如:類操做、classpath 操做等
瞭解了這兩個參數之後,咱們走進 main
方法看下實現
這裏主要重點講下上面圈出來的兩點,首先咱們來看下第一段的代碼。
第一步先將咱們的 arthas-spy.jar
添加到 BootstrapClassLoader 中,在 Java Instrumention 的實現中,這行代碼應該是很常見的。爲何要這樣作呢?
在Java中,Java類加載器分爲 BootstrapClassLoader、ExtensionClassLoader和SystemClassLoader。
BootstrapClassLoader 主要加載的是JVM自身須要的類,因爲雙親委派機制的存在,越基礎的類由越上層的加載器進行加載,所以,若是須要在由 BootstrapClassLoader 加載的類的方法中調用由 SystemClassLoader 加載的arthas-spy.jar
,這違反了雙親委派機制。
而 arthas-spy.jar
添加到 BootstrapClassLoader 的 classpath 中,由 BootstrapClassLoader加載,就解決了這個問題。
initSpy
這個方法則使用 ArthasClassloader
加載 com.taobao.arthas.core.advisor.AdviceWeaver
類(在後續文章中會詳細解讀),並將其中的 methodOnBegin
、methodOnReturnEnd
、methodOnThrowingEnd
等方法取出,並賦給Spy
類。後面在經過ASM作類加強的時候,Spy
就是鏈接業務類和Arthas類的橋樑。
接着咱們看下第二段藍色框中的代碼。這裏面主要是作了一些服務端啓動的事情。
這段代碼中,主要經過反射的手段,調用了 ArthasBootstrap
類中的 bind
方法來啓動 Arthas 服務端,接下來咱們就一塊兒來看下 Arthas 服務端啓動的源碼。
廢話很少說,先上代碼。
這段代碼主要是圍繞 ShellServer 作一些配置,並調用 listen
方法啓動監聽
在listen
方法中,主要是根據以前註冊的TermServer來逐個啓動,這裏以TelnetTermServer
爲例講解,接下來看下TelnetTermServer
中的listen
方法。
咱們跟蹤下 start
代碼,發現最後調用的是 NettyTelnetBootstrap
的 start
方法。
主要是經過 netty
來啓動網絡服務。下面咱們看下對輸入的處理類 TermServerTermHandler
。
主要是經過調用shellServer
的handleTerm
方法。
這裏的 session 就是客戶端的鏈接,而readline方法就是用來處理用戶的輸入的。
至於每一個命令是如何工做的,且聽下回分解。
arthas中涉及到的知識點不少的瞭解
netty
termd
cli
asm
JavaAgent
你們若是感興趣的話,能夠花點時間研究下,相信這些框架會讓你們受益不淺。
逅弈逐碼,專一於原創分享,用通俗易懂的圖文描述源碼及原理