
一、概述
使用 docker 容器部署項目已經(jīng)成為開發(fā)者必須掌握的技能,當使用 docker 容器部署項目后,如何在容器中對 Java 應(yīng)用進行實時診斷,這篇文章主要介紹在 docker 容器中如何使用 Java 診斷工具 —— Arthas 。在容器中使用 Arthas 和在服務(wù)器上面使用是沒有太大區(qū)別的,通常情況下一個容器中只會運行我們的應(yīng)用服務(wù)這一個 Java 進程,所以在容器中使用 Arthas 只會看到一個 Java 進程。關(guān)于 Arthas 的詳細說明可以查看下面的官方文檔,這里只會對自己在 docker 容器中使用過的 Arthas 命令通過案例進行介紹。
Arthas 中文文檔:http://arthas.gitee.io/
二、在 docker 容器中安裝 Arthas
1、構(gòu)建 docker 容器
這里使用一個 spring boot 的 demo 進行實踐,其中只包括一個 controller, 內(nèi)容如下:
@Slf4j
@RestController
public class TestController {
@GetMapping("hello/{content}")
public String hello(@PathVariable(value = "content") String content) {
log.debug("----------log debug----------");
log.info("----------log info----------");
log.warn("----------log warn----------");
log.error("----------log error----------");
return "返回結(jié)果:" + content;
}
}
構(gòu)建鏡像的 Dockerfile 內(nèi)容如下:
FROM openjdk:8u232-jdk
WORKDIR /app
LABEL maintainer="peterwd" app="devops-demo"
COPY target/devops-demo.jar devops-demo.jar
EXPOSE 8080
CMD java -jar devops-demo.jar
使用如下命令構(gòu)建鏡像:
docker build -t devops-demo .
使用下面的命令啟動容器:
docker run --name devop-demo -d -p 8080:8080 devops-demo
構(gòu)建好鏡像之后使用如下命令進入 docker 容器:
docker exec -it devops-demo bash
2、安裝 Arthas
進入 docker 容器之后,使用如下命令安裝 Arthas:
wget https://arthas.aliyun.com/arthas-boot.jar
使用如下命令啟動 Arthas:
java -jar arthas-boot.jar
啟動 Arthas 的過程中會選擇對應(yīng)的 Java 進程,在 docker 容器中通常只有一個 Java 進程,所以直接 1 即可,如果有多個 Java 進程輸入前面的編號。
如下圖所示:
三、Arthas 命令介紹
請注意,這些命令,都通過字節(jié)碼增強技術(shù)來實現(xiàn)的,會在指定類的方法中插入一些切面來實現(xiàn)數(shù)據(jù)統(tǒng)計和觀測,因此在線上、預(yù)發(fā)使用時,請盡量明確需要觀測的類、方法以及條件,診斷結(jié)束要執(zhí)行 stop 或?qū)⒃鰪娺^的類執(zhí)行 reset 命令。
1、基礎(chǔ)命令
help——查看命令幫助信息
cat——打印文件內(nèi)容,和linux里的cat命令類似
echo–打印參數(shù),和linux里的echo命令類似
grep——匹配查找,和linux里的grep命令類似
base64——base64編碼轉(zhuǎn)換,和linux里的base64命令類似
tee——復(fù)制標準輸入到標準輸出和指定的文件,和linux里的tee命令類似
pwd——返回當前的工作目錄,和linux命令類似
cls——清空當前屏幕區(qū)域
session——查看當前會話的信息
reset——重置增強類,將被 Arthas 增強過的類全部還原,Arthas 服務(wù)端關(guān)閉時會重置所有增強過的類
version——輸出當前目標 Java 進程所加載的 Arthas 版本號
history——打印命令歷史
quit——退出當前 Arthas 客戶端,其他 Arthas 客戶端不受影響
stop——關(guān)閉 Arthas 服務(wù)端,所有 Arthas 客戶端全部退出
keymap——Arthas快捷鍵列表及自定義快捷鍵
2、jvm 相關(guān)命令
dashboard——當前系統(tǒng)的實時數(shù)據(jù)面板
thread——查看當前 JVM 的線程堆棧信息
jvm——查看當前 JVM 的信息
sysprop——查看和修改JVM的系統(tǒng)屬性
sysenv——查看JVM的環(huán)境變量
vmoption——查看和修改JVM里診斷相關(guān)的option
perfcounter——查看當前 JVM 的Perf Counter信息
logger——查看和修改logger
getstatic——查看類的靜態(tài)屬性
ognl——執(zhí)行ognl表達式
mbean——查看 Mbean 的信息
heapdump——dump java heap, 類似jmap命令的heap dump功能
vmtool——從jvm里查詢對象,執(zhí)行forceGc
3、class/classloader相關(guān)命令
sc——查看JVM已加載的類信息
sm——查看已加載類的方法信息
jad——反編譯指定已加載類的源碼
mc——內(nèi)存編譯器,內(nèi)存編譯.java文件為.class文件
retransform——加載外部的.class文件,retransform到JVM里
redefine——加載外部的.class文件,redefine到JVM里
dump——dump 已加載類的 byte code 到特定目錄
classloader——查看classloader的繼承樹,urls,類加載信息,使用classloader去getResource
4、monitor/watch/trace相關(guān)命令
monitor——方法執(zhí)行監(jiān)控
watch——方法執(zhí)行數(shù)據(jù)觀測
trace——方法內(nèi)部調(diào)用路徑,并輸出方法路徑上的每個節(jié)點上耗時
stack——輸出當前方法被調(diào)用的調(diào)用路徑
tt——方法執(zhí)行數(shù)據(jù)的時空隧道,記錄下指定方法每次調(diào)用的入?yún)⒑头祷匦畔?,并能對這些不同的時間下調(diào)用進行觀測
三、使用 Arthas 的 logger 實時修改類的日志級別
前面簡單介紹了 Arthas 的命令,這里主要介紹使用 Arthas 的 logger 實時修改類的日志級別,這里的使用的 demo 中定義了四種日志級別,分別是 debug、info、warn、error,通過動態(tài)修改不同日志級別來控制日志的顯示。
log4j 定義了8個級別的log(除去 OF F和 ALL,可以說分為6個級別),優(yōu)先級從高到低依次為:
OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。如果將log level設(shè)置在某一個級別上,那么比此級別優(yōu)先級高的log都能打印出來。例如,如果設(shè)置優(yōu)先級為WARN,那么OFF、FATAL、ERROR、WARN 4個級別的log能正常輸出,而INFO、DEBUG、TRACE、 ALL級別的log則會被忽略。Log4j建議只使用四個級別,優(yōu)先級從高到低分別是
ERROR、WARN、INFO、DEBUG。
1、使用 sc 命令查看 JVM 加載的類信息
命令:sc -d [查找類的全路徑 或者 *類名]
sc 命令支持通過類名模糊查找類信息,-d 顯示詳細信息,獲取到類的全路徑名和 classLoaderHash 如下所示:
[arthas@7]$ sc -d *TestController
class-info devops.demo.controller.TestController
code-source file:/app/devops-demo.jar!/BOOT-INF/classes!/
name devops.demo.controller.TestController
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name TestController
modifier public
annotation org.springframework.web.bind.annotation.RestController
interfaces
super-class +-java.lang.Object
class-loader +-org.springframework.boot.loader.LaunchedURLClassLoader@7daf6ecc
+-sun.misc.Launcher$AppClassLoader@70dea4e
+-sun.misc.Launcher$ExtClassLoader@1a04f701
classLoaderHash 7daf6ecc
2、使用 logger 命令查看指定類的日志級別
命令:logger --name [查找類的全路徑]
logger 用來查看和修改logger信息,--name 指定全路徑類名,如下所示:
[arthas@7]$ logger --name devops.demo.controller.TestController
name devops.demo.controller.TestController
class ch.qos.logback.classic.Logger
classLoader org.springframework.boot.loader.LaunchedURLClassLoader@7daf6ecc
classLoaderHash 7daf6ecc
level null
effectiveLevel INFO
additivity true
codeSource jar:file:/app/devops-demo.jar!/BOOT-INF/lib/logback-classic-1.2.3.jar!/
3、使用 logger 命令修改指定類的日志級別
命令:logger -c [classLoaderHash的值] --name [查找類的全路徑] --level [待更新的日志level]
-c 指定 classLoaderHash 的值 --level 指定要更新的日志級別,如下所示:
[arthas@7]$ logger -c 7daf6ecc --name devops.demo.controller.TestController --level debug
Update logger level success.
4、驗證修改日志級別的結(jié)果
默認情況下類的日志級別是 info,這里訪問 demo 輸出日志沒有 debug 的信息,如下圖所示:
使用如下命令修改 logger 的日志級別為 debug:
[arthas@7]$ logger -c 7daf6ecc --name devops.demo.controller.TestController --level debug
Update logger level success.
再次訪問,輸出日志有 debug 信息,如下圖所示:
使用如下命令修改 logger 的日志級別為 error:
[arthas@7]$ logger -c 7daf6ecc --name devops.demo.controller.TestController --level error
Update logger level success.
再次訪問,輸出日志只有 error 信息,如下圖所示:
四、使用 Arthas 的 watch 查看方法輸入輸出參數(shù)
命令: watch 全路徑類名 方法名 [表達式]
watch 命令的使用說明如下:
watch 用來查看指定方法調(diào)用的輸入輸出參數(shù),返回值以及拋出的異常信息,watch 可以使用的表達式如下:
target : the object
clazz : the object's class
method : the constructor or method
params : the parameters array of method
params[0..n] : the element of parameters array
returnObj : the returned object of method
throwExp : the throw exception of method
isReturn : the method ended by return
isThrow : the method ended by throwing exception
#cost : the execution time in ms of method invocation
關(guān)于 watch 命令的詳細說明可以使用 watch --help 查看,這里只介紹示例方法的使用。
使用如下命令查看方法的調(diào)用參數(shù):
[arthas@7]$ watch devops.demo.controller.TestController hello params
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 26 ms, listenerId: 3
method=devops.demo.controller.TestController.hello location=AtExit
ts=2021-05-26 11:36:58; [cost=0.627099ms] result=@Object[][
@String[測試方法調(diào)用參數(shù)],
]
使用如下命令查看方法的返回參數(shù):
[arthas@7]$ watch devops.demo.controller.TestController hello returnObj
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 24 ms, listenerId: 4
method=devops.demo.controller.TestController.hello location=AtExit
ts=2021-05-26 11:39:18; [cost=0.525488ms] result=@String[返回結(jié)果:測試方法返回參數(shù)]
五、使用 Arthas 實現(xiàn)在線代碼熱更新
使用 Arthas 提供的 sc jad mc redefine 這四個命令就可以實現(xiàn)在線代碼熱更新,這個功能非常強大,但是也非常危險,在容器中使用要控制進入容器的權(quán)限,在服務(wù)器使用也要控制服務(wù)器的使用權(quán)限,下面以提供的 demo 為例詳細說明如何使用這幾個命令實現(xiàn)代碼熱更新。
1、使用 sc 命令查找 JVM 加載的類信息
命令:sc -d [查找類的全路徑 或者 *類名]
sc 命令支持通過類名模糊查找類信息,-d 顯示詳細信息,獲取到類的全路徑名和 classLoaderHash 如下所示:
[arthas@7]$ sc -d *TestController
class-info devops.demo.controller.TestController
code-source file:/app/devops-demo.jar!/BOOT-INF/classes!/
name devops.demo.controller.TestController
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name TestController
modifier public
annotation org.springframework.web.bind.annotation.RestController
interfaces
super-class +-java.lang.Object
class-loader +-org.springframework.boot.loader.LaunchedURLClassLoader@7daf6ecc
+-sun.misc.Launcher$AppClassLoader@70dea4e
+-sun.misc.Launcher$ExtClassLoader@1a04f701
classLoaderHash 7daf6ecc
2、使用 jad 命令反編譯已加載類的源碼
命令:jad --source-only 類的全路徑 > 類名.java
jad 命令反編譯已加載類的源碼, --source-only 指定只輸出源碼,> 類名.java 將輸出結(jié)果保存到當前目錄的 類名.java 文件
jad --source-only devops.demo.controller.TestController > TestController.java
查看反編譯的內(nèi)容如下:
[arthas@7]$ cat TestController.java
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* org.slf4j.Logger
* org.slf4j.LoggerFactory
* org.springframework.web.bind.annotation.GetMapping
* org.springframework.web.bind.annotation.PathVariable
* org.springframework.web.bind.annotation.RestController
*/
package devops.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
private static final Logger log = LoggerFactory.getLogger(TestController.class);
@GetMapping(value={"hello/{content}"})
public String hello(@PathVariable(value="content") String content) {
/*14*/ log.debug("----------log debug----------");
/*15*/ log.info("----------log info----------");
/*16*/ log.warn("----------log warn----------");
/*17*/ log.error("----------log error----------");
return "返回結(jié)果:" + content;
}
}
在容器中沒有 vim 編輯器,不方便修改,可以將反編譯出來的源碼復(fù)制出來,修改完成之后,通過 docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH 命令將修改后的內(nèi)容復(fù)制到容器中。
docker cp TestController.java devop-demo:/app
修改上面反編譯出來的源代碼,修改如下:
[arthas@7]$ cat TestController.java
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* org.slf4j.Logger
* org.slf4j.LoggerFactory
* org.springframework.web.bind.annotation.GetMapping
* org.springframework.web.bind.annotation.PathVariable
* org.springframework.web.bind.annotation.RestController
*/
package devops.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
private static final Logger log = LoggerFactory.getLogger(TestController.class);
@GetMapping(value={"hello/{content}"})
public String hello(@PathVariable(value="content") String content) {
/*14*/ log.debug("----------log debug----------");
/*15*/ log.info("----------log info----------");
/*16*/ log.warn("----------log warn----------");
/*17*/ log.error("----------log error----------");
return "返回結(jié)果:測試熱更新代碼 " + content;
}
}
3、使用 mc 內(nèi)存編譯.java文件為.class文件
命令 mc -c 類加載器hash java源碼路徑 -d /tmp
mc 內(nèi)存編譯.java文件為.class文件,-c 類加載器hash 指定前面通過 sc -d 命令查找到的 classLoaderHash , -d /tmp 指定編譯輸出的 class 文件的目錄為 /tmp, 不指定則輸出到當前目錄。
[arthas@7]$ mc -c 7daf6ecc TestController.java -d /tmp
Memory compiler output:
/tmp/devops/demo/controller/TestController.class
Affect(row-cnt:1) cost in 993 ms.
4、使用 redefine 加載外部的.class文件
命令:redefine class文件路徑
這里的 class 文件路徑填寫上面反編譯輸出的路徑,如下所示:
[arthas@7]$ redefine /tmp/devops/demo/controller/TestController.class
redefine success, size: 1, classes:
devops.demo.controller.TestController
5、驗證熱更新結(jié)果
root@devops-demo-7bdf65859c-mtjqm:/app# curl localhost:8080/hello/test
返回結(jié)果:測試熱更新代碼 test
六、總結(jié)
這篇文章簡單介紹了 Arthas 在 docker 容器的使用,主要介紹了 logger 和 watch 命令以及如何實現(xiàn)在線代碼熱更新,后續(xù)有使用到其他命令再來補充,詳細信息可以查閱官方文檔。