目录
怎么能快速发现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 &a