2018-10-10 12:55 閱讀:1282次 作者: 來源: 公眾賬號(hào)
上個(gè)月屁置,阿里開源了 **監(jiān)控與診斷 **工具 「 **Arthas **」做裙,一款可用于線上問題分析的利器,短期之內(nèi)收獲了大量關(guān)注棍苹,在 Twitter 上連 Java 官方的 Twitter 也轉(zhuǎn)發(fā)了涧衙,真的很贊哺窄。
GitHub 上是這樣自述的:
Arthas 是一款線上監(jiān)控診斷產(chǎn)品,通過全局視角實(shí)時(shí)查看應(yīng)用 load到腥、內(nèi)存朵逝、gc、線程的狀態(tài)信息乡范,并能在不修改應(yīng)用代碼的情況下配名,對(duì)業(yè)務(wù)問題進(jìn)行診斷,包括查看方法調(diào)用的出入?yún)⒔尽惓G觯O(jiān)測(cè)方法執(zhí)行耗時(shí),類加載信息等栈拖,大大提升線上問題排查效率连舍。
我一般看到感興趣的開源工具,會(huì)找?guī)讉€(gè)最感興趣的功能點(diǎn)切入涩哟,從源碼了解設(shè)計(jì)與實(shí)現(xiàn)原理索赏。對(duì)于一些自己了解的實(shí)現(xiàn)思路,再?gòu)脑创a中驗(yàn)證一下是否是采用相同的實(shí)現(xiàn)思路贴彼。如果實(shí)現(xiàn)和自己想的一樣潜腻,可能你會(huì)想,啊哈器仗,想到一塊了融涣。如果源碼中是另一種實(shí)現(xiàn)童番,你就會(huì)想 Cool, 還可以這樣玩。 **仿佛如同在和源碼的作者對(duì)話一樣 **威鹿。
這次趁著國(guó)慶假期看了一些「 ##Arthas」的源碼剃斧,大致總結(jié)下。
從源碼的包結(jié)構(gòu)上忽你,可以看到分為幾個(gè)大的 模塊:
Agent -- VM 加載的自定義 Agent
Client -- Telnet 客戶端實(shí)現(xiàn)
Core -- Arthas 核心實(shí)現(xiàn)幼东,包含連接 VM, 解析各類命令等
Site -- Arthas 的幫助手冊(cè)站點(diǎn)內(nèi)容
我主要看了以下幾個(gè)功能:
連接進(jìn)程
反編譯class,獲取源碼
查詢指定加載的 class
連接進(jìn)程
連接到指定的進(jìn)程科雳,是后續(xù)監(jiān)控與診斷的 **基礎(chǔ) **根蟹。只有先 attach 到進(jìn)程之上,才能獲取 VM 對(duì)應(yīng)的信息糟秘,查詢 ClassLoader 加載的類等等简逮。
怎樣連接到進(jìn)程呢?
用于類似診斷工具的讀者可能都有印象尿赚,像 JProfile散庶、 VisualVM 等工具,都會(huì)讓你選擇一個(gè)要連接到的進(jìn)程凌净。然后再在指定的 VM 上進(jìn)行操作督赤。比如查看對(duì)應(yīng)的內(nèi)存分區(qū)信息,內(nèi)存垃圾收集信息泻蚊,執(zhí)行 BTrace腳本等等躲舌。
咱們先來想想,這些可供連接的進(jìn)程列表性雄,是怎么列出來的呢没卸?
一般可能會(huì)是類似 ps aux | grep java
這種,或者是使用 Java 提供的工具 jps -lv
都可以列出包含進(jìn)程id的內(nèi)容秒旋。我在很早之前的文章里寫過一點(diǎn) jps 的內(nèi)容( 你可能不知道的幾個(gè)java小工具 )约计,其背后實(shí)現(xiàn),是會(huì)將本地啟動(dòng)的所有 Java 進(jìn)程迁筛,以 pid
做為文件名存放在Java 的臨時(shí)目錄中煤蚌。這個(gè)列表,遍歷這些文件即可得出來细卧。
Arthas 是怎么做的呢尉桩?
在啟動(dòng)腳本 as.sh
中,有關(guān)于進(jìn)程列表的代碼如下贪庙,實(shí)現(xiàn)也是通過 jps
然后把Jps自己排除掉:
# check pid
if [ -z ${TARGET_PID} ] && [ ${BATCH_MODE} = false ]; then
local IFS_backup=$IFS
IFS=/pre>\n'
CANDIDATES=($(${JAVA_HOME}/bin/jps -l | grep -v sun.tools.jps.Jps | awk '{print $0}'))
if [ ${#CANDIDATES[@]} -eq 0 ]; then
echo "Error: no available java process to attach."
# recover IFS
IFS=$IFS_backup
return 1
fi
echo "Found existing java process, please choose one and hit RETURN."
index=0
suggest=1
# auto select tomcat/pandora-boot process
for process in "${CANDIDATES[@]}"; do
index=$(($index+1))
if [ $(echo ${process} | grep -c org.apache.catalina.startup.Bootstrap) -eq 1 ] \
|| [ $(echo ${process} | grep -c com.taobao.pandora.boot.loader.SarLauncher) -eq 1 ]
then
suggest=${index}
break
fi
done
選擇好進(jìn)程之后蜘犁,就是連接到指定進(jìn)程了。連接部分在 attach
這里
# attach arthas to target jvm
# $1 : arthas_local_version
attach_jvm()
{
local arthas_version=$1
local arthas_lib_dir=${ARTHAS_LIB_DIR}/${arthas_version}/arthas
echo "Attaching to ${TARGET_PID} using version ${1}..."
if [ ${TARGET_IP} = ${DEFAULT_TARGET_IP} ]; then
${JAVA_HOME}/bin/java \
${ARTHAS_OPTS} ${BOOT_CLASSPATH} ${JVM_OPTS} \
-jar ${arthas_lib_dir}/arthas-core.jar \
-pid ${TARGET_PID} \
-target-ip ${TARGET_IP} \
-telnet-port ${TELNET_PORT} \
-http-port ${HTTP_PORT} \
-core "${arthas_lib_dir}/arthas-core.jar" \
-agent "${arthas_lib_dir}/arthas-agent.jar"
fi}
對(duì)于 JVM 內(nèi)部的 attach 實(shí)現(xiàn)止邮,
是通過 tools.jar
這個(gè)包中的 com.sun.tools.attach.VirtualMachine
以及 VirtualMachine.attach(pid)
這種方式來實(shí)現(xiàn)的这橙。
底層則是通過 JVMTI
奏窑。之前的文章簡(jiǎn)單分析過 JVMTI
這種技術(shù)( 當(dāng)我們談Debug時(shí),我們?cè)谡勈裁?Debug實(shí)現(xiàn)原理) )屈扎,在運(yùn)行前或者運(yùn)行時(shí)埃唯,將自定義的 Agent加載并和 VM 進(jìn)行 **通信 **。
上面具體執(zhí)行的內(nèi)容在 arthas-core.jar
的主類中鹰晨,我們來看具體的內(nèi)容:
private void attachAgent(Configure configure) throws Exception {
VirtualMachineDescriptor virtualMachineDescriptor = null;
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
String pid = descriptor.id();
if (pid.equals(Integer.toString(configure.getJavaPid()))) {
virtualMachineDescriptor = descriptor;
}
}
VirtualMachine virtualMachine = null;
try {
if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 這種方式
virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
} else {
virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
}
Properties targetSystemProperties = virtualMachine.getSystemProperties();
String targetJavaVersion = targetSystemProperties.getProperty("java.specification.version");
String currentJavaVersion = System.getProperty("java.specification.version");
if (targetJavaVersion != null && currentJavaVersion != null) {
if (!targetJavaVersion.equals(currentJavaVersion)) {
AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.",
currentJavaVersion, targetJavaVersion);
AnsiLog.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.",
targetSystemProperties.getProperty("java.home"));
}
}
virtualMachine.loadAgent(configure.getArthasAgent(),
configure.getArthasCore() + ";" + configure.toString());
} finally {
if (null != virtualMachine) {
virtualMachine.detach();
}
}
}
通過 VirtualMachine
, 可以attach到當(dāng)前指定的pid上筑凫,或者是通過 VirtualMachineDescriptor
實(shí)現(xiàn)指定進(jìn)程的attach,最核心的就是這一句:
virtualMachine.loadAgent(configure.getArthasAgent(),
configure.getArthasCore() + ";" + configure.toString());
這樣,就和指定進(jìn)程的 VM建立了連接并村,此時(shí)就可以進(jìn)行通信啦。
類的反編譯實(shí)現(xiàn)
我們?cè)趩栴}診斷中滓技,有些時(shí)候需要了解當(dāng)前加載的 class 對(duì)應(yīng)的內(nèi)容哩牍,方便確認(rèn)加載的類是否正確等,一般通過 javap
只能顯示類似摘要的內(nèi)容令漂,并不直觀膝昆。 在桌面端我們可以通過 jd-gui
之類的工具,在命令行里一般可選的不多叠必。
Arthas 則集成了這一功能荚孵。
大致的步驟如下:
通過指定class名稱的內(nèi)容,先進(jìn)行類的查找
根據(jù)選項(xiàng)纬朝,判斷是否進(jìn)行Inner Class之類的查找
進(jìn)行反編譯
我們來看 Arthas 的實(shí)現(xiàn)收叶。
對(duì)于 VM 中指定名稱的 class 的查找,我們看下面這幾行代碼:
public void process(CommandProcess process) {
RowAffect affect = new RowAffect();
Instrumentation inst = process.session().getInstrumentation();
Set<Class> matchedClasses = SearchUtils.searchClassOnly(inst, classPattern, isRegEx, code);
try {
if (matchedClasses == null || matchedClasses.isEmpty()) {
processNoMatch(process);
} else if (matchedClasses.size() > 1) {
processMatches(process, matchedClasses);
} else {
Set<Class> withInnerClasses = SearchUtils.searchClassOnly(inst, classPattern + "(?!.*\\$\\$Lambda\\$).*", true, code);
processExactMatch(process, affect, inst, matchedClasses, withInnerClasses);
}
關(guān)鍵的查找內(nèi)容共苛,做了封裝判没,在 SearchUtils
里,這里有一個(gè)核心的參數(shù): Instrumentation
隅茎,都是這個(gè)哥們給實(shí)現(xiàn)的澄峰。
/**
* 根據(jù)類名匹配,搜已經(jīng)被JVM加載的類
*
* @param inst inst
* @param classNameMatcher 類名匹配
* @return 匹配的類集合
*/
public static Set<> searchClass(Instrumentation inst, Matcher classNameMatcher, int limit) {
for (Class clazz : inst.getAllLoadedClasses()) {
if (classNameMatcher.matching(clazz.getName())) {
matches.add(clazz);
}
}
return matches;
}
inst.getAllLoadedClasses()
辟犀,它才是背后的大玩家俏竞。
查找到了 Class 之后,怎么反編譯的呢堂竟?
private String decompileWithCFR(String classPath, Class clazz, String methodName) {
List<String> options = new ArrayList<String>();
options.add(classPath);
// options.add(clazz.getName());
if (methodName != null) {
options.add(methodName);
}
options.add(OUTPUTOPTION);
options.add(DecompilePath);
options.add(COMMENTS);
options.add("false");
String args[] = new String[options.size()];
options.toArray(args);
Main.main(args);
String outputFilePath = DecompilePath + File.separator + Type.getInternalName(clazz) + ".java";
File outputFile = new File(outputFilePath);
if (outputFile.exists()) {
try {
return FileUtils.readFileToString(outputFile, Charset.defaultCharset());
} catch (IOException e) {
logger.error(null, "error read decompile result in: " + outputFilePath, e);
}
}
return null;
}
通過這樣一個(gè)方法: decompileWithCFR
魂毁,所以我們大概了解到反編譯是通過第三方工具「 **CFR **」來實(shí)現(xiàn)的。上面的代碼也是拼 Option然后傳給 CFR的 Main方法實(shí)現(xiàn)出嘹,再保存下來漱牵。感興趣的朋友可以查詢 benf cfr
了解具體用法。
查詢加載類的實(shí)現(xiàn)
看過上面反編譯 class 的內(nèi)容之后疚漆,我們知道封裝了一個(gè) SearchUtil
的類酣胀,后面許多地方都會(huì)用到刁赦,而且上面反編譯也是在查詢到類的之后再進(jìn)行的。查詢的過程闻镶,也是在Instrument的基礎(chǔ)之上甚脉,再加上各種匹配規(guī)則過濾,所以更多的具體內(nèi)容不再贅述铆农。
我們發(fā)現(xiàn)上面幾個(gè)功能的實(shí)現(xiàn)中牺氨,有兩個(gè)關(guān)鍵的東西:
VirtualMachine
Instrumentation
Arthas 的整體邏輯也是在 Java 的 Instrumentation基礎(chǔ)上來實(shí)現(xiàn),所有在加載的類會(huì)通過Agent的加載墩剖, 通過addTransformer之后猴凹,進(jìn)行增強(qiáng),然后將對(duì)應(yīng)的Advice織入進(jìn)去岭皂,對(duì)于類的查找郊霎,方法的查找,都是通過SearchUtil來進(jìn)行的爷绘,通過Instrument的loadAllClass方法將所有的JVM加載的class按名字進(jìn)行匹配书劝,一致的會(huì)進(jìn)行返回。
Instrumentation 是個(gè)好同志土至! :)