怎麼能快速發現java系統的問題,並快速定位解決問題
目錄
怎麼能快速發現java系統的問題,並快速定位解決問題
前言:
每個人都會生病出現各種健康問題,同樣開發人員寫的程式碼在執行後也會發生各種問題,這種問題可以分為業務邏輯問題,開發編寫程式碼沒有考慮周全引起的問題,後一種問題針對java程式來說的表象就是丟擲Exception告訴大家我身體有問題了,exception裡包含了是什麼問題及出問題的部位,而前一種業務邏輯的問題只有開發人員自己最能清楚(所以是需要開發人員自己來處理的)。
既然我們掌握了系統出現問題的輸出點,那我們只要找個合理的方法來暴露這個問題,並實時的通知相關人員,讓他們知道並排查問題就ok了,如果所有問題都解決就能保證這個系統是健康的。
當然以上過程只是第一步,我們還需要試探著解決第二步的問題,即快速定位查詢問題,那快速定位問題怎麼搞呢?如:系統出現了一個問題,但是開發運維人員還是不能根據這個問題知道答案,那他就需要在系統程式碼裡新增些程式碼,來輸出這個問題(線上問題不能遠端debug,如果你這樣做了會hold住正常請求),然後再部署這個系統,觀察你新增程式碼裡的邏輯輸出來定位這個問題,如果一次沒有找到,你可能還要新增程式碼再次釋出,你想想你做完這些都到什麼時候了,而且這個過程會影響線上正常請求。
解決思路
針對系統異常資訊的發現
我們知道所有系統的輸出都是通過引入日誌框架來實現的,不管系統所使用的框架,引用的二方包甚至三方包都是通過日誌來輸出錯誤資訊的以及應用系統自身,而統一的錯誤資訊都是error級別的。如下,當發生異常時:
try {
return HttpUtils.get(url, null, params, TIME_OUT);
} catch (Exception e) {
logger.error("call url fail!e={}", e);
return "";
}
同樣當發生業務邏輯上的錯誤時,也可以記日誌(也可以拋自定義的異常,這樣會有異常棧資訊):
if(false){
logger.error("XXX,fail");
}
所以通過日誌的方式是最方便記錄系統錯誤資訊的。日誌框架有很多,現在大多使用的是logback和log4j,所以只要收集log.error的日誌資訊就可以了。
怎麼收集error級別的日誌
現在流行的有ELK 和flume,但是我們只是對異常日誌進行收集,而且web需要定製化的東西很多,ELK和flume又太重,而且web後臺展示不符合異常日誌的展示,最重要的我要實現端控制和程式碼診斷,需要將agent端侵入到業務程式碼裡。
基於以上原因我決定自己寫程式碼實現。
收集用什麼方式實現
我不想提供api式的呼叫,類似大眾點評的Cat的方式,對業務程式碼侵入性太強,我需要零侵入,所以選擇用javaagent來實現。
javaagent功能:
- 可以在載入java檔案之前做攔截把位元組碼做修改
- 可以在執行期將已經載入的類的位元組碼做變更
- 可以獲取所有已經被載入過的類
- 將某個jar加入到bootstrapclasspath裡作為高優先順序被bootstrapClassloader載入,也可以將某個jar加入到classpath裡供AppClassloard去載入
針對收集到資訊後怎麼定位解決問題
這個異常收集系統叫啄木鳥,現在模擬下場景,啄木鳥已經收集到類資訊併發送報警郵件、簡訊或者微信給了開發運維人員,開發接到簡訊後看到報警詳細資訊(異常棧等資訊),能夠找到是哪段程式碼以及哪個方法發生了發生了問題,這個時候他需要跟蹤或者列印日誌,原來的方法是改程式碼新增相應程式碼然後釋出,前面也說了這個很耗時。
怎麼完美的解決?
通過遠端控制,實時在線上解決問題,不需要修改程式碼和多次釋出系統,不影響線上正常請求。
用什麼技術
還是用javaagent和javassist。通過javaagent用javassit進行位元組碼修改。而用netty實現使用命令對服務端程式碼進行遠端診斷。
系統介紹
接下來詳細講下這個系統,系統名為啄木鳥。
程式碼已經開源:
https://github.com/guoyang1982/woodpecker-client
系統架構圖
技術介紹
javaagent
利用javaagent進行jvm內的類轉換。
在jvm內只需要加入:
-javaagent:/letv/agent/wpclient-agent/wpclient-agent.jar=/letv/agent/wpclient-agent/wp-mini-ecommerce.properties
使用樣例:
javassist
利用javassit重寫類位元組程式碼。
log日誌的類位元組轉換:
import com.gy.woodpecker.tools.ConfigPropertyUtile;
import javassist.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
/**
* Created by guoyang on 17/10/27.
*/
@Slf4j
public class WoodpeckTransformer implements ClassFileTransformer {
private String loggerClassic;
private String methodName;
private String javassistInfo;
private String loger = "logback";
private final String logbakInfo = "if(level.levelStr.equals(\"ERROR\")){" +
"com.gy.woodpecker.agent.LoggerFactoryProx.sendToRedis(msg,params,t);}";
private final String log4jInfo = "if(level.levelStr.equals(\"ERROR\")){" +
"com.gy.woodpecker.agent.LoggerFactoryProx.sendToRedis(message.toString());}";
public boolean validLevel(String level) {
if (null == level || level.equals("")) {
return false;
}
if (level.toUpperCase().equals("ERROR")) {
return true;
}
if (level.toUpperCase().equals("INFO")) {
return true;
}
if (level.toUpperCase().equals("DEBUG")) {
return true;
}
return false;
}
public WoodpeckTransformer() {
String logerT = ConfigPropertyUtile.getProperties().getProperty("agent.log.name");
String level = ConfigPropertyUtile.getProperties().getProperty("agent.log.level");
if (null != logerT && !logerT.equals("")) {
loger = logerT;
}
if (loger.equals("logback")) {
loggerClassic = "ch.qos.logback.classic.Logger";
methodName = "buildLoggingEventAndAppend";
if (validLevel(level)) {
javassistInfo = logbakInfo.replaceFirst("ERROR", level);
} else {
javassistInfo = logbakInfo;
}
}
if (loger.equals("log4j")) {
loggerClassic = "org.apache.log4j.Category";
methodName = "forcedLog";
if (validLevel(level)) {
javassistInfo = log4jInfo.replaceFirst("ERROR", level);
} else {
javassistInfo = log4jInfo;
}
}
}
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] byteCode = classfileBuffer;
className = className.replace('/', '.');
if (isNeedLogExecuteInfo(className)) {
if (null == loader) {
loader = Thread.currentThread().getContextClassLoader();
}
byteCode = aopLog(loader, className, byteCode);
}
return byteCode;
}
private byte[] aopLog(ClassLoader loader, String className, byte[] byteCode) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = null;
//載入類的路徑 從應用的classloader搜尋類
cp.insertClassPath(new LoaderClassPath(loader));
cc = cp.get(className);
byteCode = aopLog(cc, className, byteCode);
} catch (Exception ex) {
log.info("the applog exception:{}", ex);
}
return byteCode;
}
private byte[] aopLog(CtClass cc, String className, byte[] byteCode) throws CannotCompileException, IOException {
if (null == cc) {
return byteCode;
}
if (!cc.isInterface()) {
CtMethod[] methods = cc.getDeclaredMethods();
if (null != methods && methods.length > 0) {
for (CtMethod m : methods) {
if (m.getName().equals(methodName)) {
aopLog(className, m);
}
}
byteCode = cc.toBytecode();
}
}
cc.detach();
return byteCode;
}
private void aopLog(String className, CtMethod m) throws CannotCompileException {
if (null == m || m.isEmpty()) {
return;
}
log.info("進行插樁類:" + className);
String ip = com.gy.woodpecker.tools.IPUtile.getIntranetIP();
m.insertBefore(javassistInfo);
}
private boolean isNeedLogExecuteInfo(String className) {
if (className.equals(loggerClassic)) {
return true;
}
return false;
}
}
以上程式碼會在log類裡做如下修改:
org.apache.log4j.Category
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t)
{
if(level.levelStr.equals("ERROR")){
com.gy.woodpecker.agent.LoggerFactoryProx.sendToRedis(message.toString());
}
callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}
ch.qos.logback.classic.Logger
private void buildLoggingEventAndAppend(String localFQCN, Marker marker, Level level, String msg, Object[] params, Throwable t)
{
if(level.levelStr.equals("ERROR")){
com.gy.woodpecker.agent.LoggerFactoryProx.sendToRedis(msg);
}
LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
le.setMarker(marker);
this.callAppenders(le);
}
端控制程式碼診斷部分的類轉換:
import com.gy.woodpecker.command.Command;
import com.gy.woodpecker.config.ContextConfig;
import com.gy.woodpecker.enumeration.CommandEnum;
import javassist.*;
import javassist.bytecode.MethodInfo;
import javassist.expr.ExprEditor;
import javassist.expr.Handler;
import javassist.expr.MethodCall;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.*;
import static java.io.File.separatorChar;
import static java.lang.System.getProperty;
import static org.apache.commons.io.FileUtils.writeByteArrayToFile;
/**
* Created by guoyang on 17/10/27.
*/
@Slf4j
public class SpyTransformer implements ClassFileTransformer {
// 類-位元組碼快取
private final static Map<Class<?>/*Class*/, byte[]/*bytes of Class*/> classBytesCache
= new WeakHashMap<Class<?>, byte[]>();
public final static Map<Integer, List> classNameCache
= new HashMap<Integer, List>();
private static final String WORKING_DIR = getProperty("user.home");
String methodName;
boolean beforeMethod;
// boolean throwMethod;
boolean afterMethod;
Command command;
public SpyTransformer(String methodName, boolean beforeMethod, boolean afterMethod, Command command) {
this.methodName = methodName;
this.beforeMethod = beforeMethod;
// this.throwMethod = throwMethod;
this.afterMethod = afterMethod;
this.command = command;
}
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
//每次增強從快取取 用於多人協助,如果不從快取取 每次都是從classpath拿最原始位元組碼
// byte[] byteCode = classBytesCache.get(classBeingRedefined);
//if(null == byteCode){
byte[] byteCode = classfileBuffer;
//}
className = className.replace('/', '.');
List classNames = classNameCache.get(command.getSessionId());
if (null == classNames) {
classNames = new ArrayList();
classNameCache.put(command.getSessionId(), classNames);
}
if (!classNames.contains(classBeingRedefined)) {
classNames.add(classBeingRedefined);
}
if (null == loader) {
loader = Thread.currentThread().getContextClassLoader();
}
byteCode = aopLog(loader, className, byteCode);
classBytesCache.put(classBeingRedefined, byteCode);
return byteCode;
}
private byte[] aopLog(ClassLoader loader, String className, byte[] byteCode) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = null;
cp.insertClassPath(new LoaderClassPath(loader));
cc = cp.get(className);
byteCode = aopLog(loader, cc, className, byteCode);
} catch (Exception ex) {
log.info("the applog exception:{}", ex);
this.command.setRes(false);
}
return byteCode;
}
private byte[] aopLog(ClassLoader loader, CtClass cc, String className, byte[] byteCode) throws CannotCompileException, IOException {
if (null == cc) {
return byteCode;
}
if (!cc.isInterface()) {
CtMethod[] methods = cc.getDeclaredMethods();
if (null != methods && methods.length > 0) {
for (CtMethod m : methods) {
if (m.getName().equals(methodName)) {
aopLog(loader, className, m);
}
}
byteCode = cc.toBytecode();
}
}
cc.detach();
if (ContextConfig.isdumpClass) {
dumpClassIfNecessary(WORKING_DIR + separatorChar + "woodpecker-class-dump/" + className, byteCode);
}
return byteCode;
}
/*
* dump class to file
*/
private static void dumpClassIfNecessary(String className, byte[] data) {
final File dumpClassFile = new File(className + ".class");
final File classPath = new File(dumpClassFile.getParent());
// 建立類所在的包路徑
if (!classPath.mkdirs()
&& !classPath.exists()) {
log.warn("create dump classpath:{} failed.", classPath);
return;
}
// 將類位元組碼寫入檔案
try {
writeByteArrayToFile(dumpClassFile, data);
} catch (IOException e) {
log.warn("dump class:{} to file {} failed.", className, dumpClassFile, e);
}
}
private void aopLog(ClassLoader loader, String className, CtMethod m) throws CannotCompileException {
if (null == m || m.isEmpty()) {
return;
}
System.out.println("進行插樁類:" + className);
String classLoad = className + ".class.getClassLoader()";
//先在before之前做子函式呼叫增強,以免把before增強的程式碼給增強
if (command.getCommandType().equals(CommandEnum.TRACE)) {
m.instrument(new ExprEditor() {
public void edit(MethodCall m)
throws CannotCompileException {
Integer lineNumber = m.getLineNumber();
String clazzName = m.getClassName();
String methodName = m.getMethodName();
String methodDes = "";
try {
MethodInfo methodInfo1 = m.getMethod().getMethodInfo();
methodDes = methodInfo1.getDescriptor();
} catch (NotFoundException e) {
e.printStackTrace();
}
String before = "com.gy.woodpecker.agent.Spy.methodOnInvokeBeforeTracing(" + command.getSessionId() + "," + lineNumber + ",\"" + clazzName + "\",\"" + methodName + "\",\"" + methodDes + "\");";
String after = "com.gy.woodpecker.agent.Spy.methodOnInvokeAfterTracing(" + command.getSessionId() + "," + lineNumber + ",\"" + clazzName + "\",\"" + methodName + "\",\"" + methodDes + "\");";
m.replace("{ " + before + " $_ = $proceed($$); " + after + "}");
}
});
}
//插入addcatch,這裡的不需要插入自己的間諜分析程式碼,但是要獲取異常資訊和返回資訊
/**
* addCatch() 指的是在方法中加入try catch 塊,需要注意的是,必須在插入的程式碼中,加入return 值$e代表 異常值。比如:
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.lang.Exception");
m.addCatch("{ System.out.println($e); throw $e; }", etype);
實際程式碼如下:
try {
the original method body
}
catch (java.lang.Exception e) {
System.out.println(e);
throw e;
}
*/
if (afterMethod) {
StringBuffer afterThrowsBody = new StringBuffer();
CtClass etype = null;
try {
etype = ClassPool.getDefault().get("java.lang.Exception");
} catch (NotFoundException e) {
e.printStackTrace();
}
// 判斷是否為靜態方法
if(Modifier.isStatic(m.getModifiers())){
afterThrowsBody.append("com.gy.woodpecker.agent.Spy.methodOnThrowingEnd(" + command.getSessionId() + "," + classLoad + ",\"" + className + "\",\"" + m.getName() + "\",null,null,$args,$e);");
}else{
afterThrowsBody.append("com.gy.woodpecker.agent.Spy.methodOnThrowingEnd(" + command.getSessionId() + "," + classLoad + ",\"" + className + "\",\"" + m.getName() + "\",null,this,$args,$e);");
}
m.addCatch("{"+afterThrowsBody.toString()+"; throw $e; }", etype);
}
/**
* Handler 代表的是一個try catch 宣告。
*/
if (command.getCommandType().equals(CommandEnum.TRACE)) {
m.instrument(new ExprEditor() {
public void edit(Handler h)
throws CannotCompileException {
Integer lineNumber = h.getLineNumber();
String clazzName = "";
String methodName = "";
String methodDes = "";
String throwException = "$1";
String before = "com.gy.woodpecker.agent.Spy.methodOnInvokeThrowTracing("
+ command.getSessionId() + "," + lineNumber + ",\"" + clazzName + "\",\"" + methodName + "\",\"" + methodDes + "\",$1);";
if(!h.isFinally()){
h.insertBefore(before);
}
}
});
}
if (command.getCommandType().equals(CommandEnum.PRINT)) {
String objPrintValue = command.getValue();
String printInfo = "com.gy.woodpecker.agent.Spy.printMethod(" + command.getSessionId() + "," + classLoad + ",\"" + className + "\",\"" + m.getName() + "\"," + objPrintValue + ");";
m.insertAt(Integer.parseInt(command.getLineNumber()), printInfo);
}
StringBuffer beforeBody = new StringBuffer();
if (beforeMethod) {
// 判斷是否為靜態方法
if(Modifier.isStatic(m.getModifiers())){
beforeBody.append("com.gy.woodpecker.agent.Spy.beforeMethod(" + command.getSessionId() + "," + classLoad + ",\"" + className + "\",\"" + m.getName() + "\",null,null,$args);");
}else{
beforeBody.append("com.gy.woodpecker.agent.Spy.beforeMethod(" + command.getSessionId() + "," + classLoad + ",\"" + className + "\",\"" + m.getName() + "\",null,this,$args);");
}
m.insertBefore(beforeBody.toString());
}
StringBuffer afterBody = new StringBuffer();
if (afterMethod) {
Object result = "$_";
try {
CtClass cc = m.getReturnType();
String retype = cc.getName();
if(retype.equals("boolean") || retype.equals("double") || retype.equals("int")
|| retype.equals("long") || retype.equals("float") || retype.equals("byte") || retype.equals("char")){
result = "String.valueOf($_)";
}
} catch (NotFoundException e) {
e.printStackTrace();
}
// 判斷是否為靜態方法
if(Modifier.isStatic(m.getModifiers())){
afterBody.append("com.gy.woodpecker.agent.Spy.afterMethod(" + command.getSessionId() + "," + classLoad + ",\"" + className + "\",\"" + m.getName() + "\",null,null,$args,"+result+");");
}else{
afterBody.append("com.gy.woodpecker.agent.Spy.afterMethod(" + command.getSessionId() + "," + classLoad + ",\"" + className + "\",\"" + m.getName() + "\",null,this,$args,"+result+");");
}
m.insertAfter(afterBody.toString());
}
}
}
類隔離
- 因為agent程式碼最終是要插樁到業務端,以至於agent程式碼裡引用的二方包等會汙染業務,導致包衝突等問題,所以要自定義classload做類隔離。
- 要實現類隔離就要理解JVM中類載入的機制-雙親委派:
-
雙親委派模型工作過程是:如果一個類載入器收到類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器完成。每個類載入器都是如此,只有當父載入器在自己的搜尋範圍內找不到指定的類時(即ClassNotFoundException),子載入器才會嘗試自己去載入。
- 啄木鳥的類載入設計:
-
如上圖我自己自定義類個classloader叫AgentClassLoader,而佐木鳥的核心包Woodpecker-core都是在這個載入器裡所有的二方包引用也都在這裡,這樣和應用的classloader進行隔離,而woodpeck-agent是在根載入器裡,這個包不會應用任何二方包,應用的classloader會獲取這個Woodpecker-agent的類進行程式碼級別的互動。
程式碼診斷
發現類異常問題想要線上查詢定位問題,就需要啄木鳥客戶端的程式碼診斷功能。
程式碼診斷的詳細使用方法可以到https://github.com/guoyang1982/woodpecker-client這裡檢視