一、来自深夜的电话!
咋滴,你那上线的系统是裸奔呢?
![](https://img-blog.csdnimg.cn/img_convert/bafba68d7cf9646b0bd8f4000abd0283.png)
周末熟睡的深夜,突然接到老板电话☎的催促。“赶紧看微信、看微信,咋系统出问题了,我们都不知道,还得用户反馈才知道的!!!”深夜爬起来,打开电脑连上 VPN ,打着哈欠、睁开朦胧的眼睛,查查系统日志,原来是系统挂了,赶紧重启恢复!
虽然重启恢复了系统,也重置了老板扭曲的表情。但系统是怎么挂的呢,因为没有一个监控系统,也不知道是流量太大导致,还是因为程序问题引起,通过一片片的日志,也仅能粗略估计出一些打着好像的标签给老板汇报。不过老板也不傻,聊来聊去,让把所有的系统运行状况都监控出来。
双手拖着困倦的脑袋,一时半会也想不出什么好方法,难道在每个方法上都硬编码上 执行耗时计算。之后把信息再统一收集起来,展示到一个监控页面呢,监控页面使用阿帕奇的 echarts,别说要是这样显示了,还真能挺好看还好用。
![](https://img-blog.csdnimg.cn/img_convert/a325cb2d08bc091a15680767db1a854b.png)
- 但这么硬编码也不叫玩意呀,这不把我们部门搬砖的码农累岔气呀!再说了,这么干他们肯定瞧不起我。啥架构师,要监控系统,还得硬编码,傻了不是!!!
- 这么一想整的没法睡觉,得找找资料,明天给老板汇报!
其实一套线上系统是否稳定运行,取决于它的运行健康度,而这包括;调用量、可用率、影响时长以及服务器性能等各项指标的一个综合值。并且在系统出现异常问题时,可以抓取整个业务方法执行链路并输出;当时的入参、出参、异常信息等等。当然还包括一些JVM、Redis、Mysql的各项性能指标,以用于快速定位并解决问题。
那么要做到这样的事情有什么处理方案呢,其实做法还是比较多的,比如;
- 最简单粗暴的就是硬编码在方法中,收取执行耗时以及出入参和异常信息。但这样的编码成本实在太大,而且硬编码完还需要大量回归测试,可能给系统带来一定的风险。万一谁手抖给复制粘贴错了呢!
- 可以选择切面方式做一套统一监控的组件,相对来说还是好一些的。但也需要硬编码,比如写入注解,同时维护成本也不低。
- 其实市面上对于这样的监控其实是有整套的非入侵监控方案的,比如;Google Dapper、Zipkin等都可以实现监控系统需求,他们都是基于探针技术非入侵的采用字节码增强的方式采集系统运行信息进行分析和监控运行状态。
好,那么本文就来带着大家来尝试下几种不同方式,监控系统运行状态的实现思路。
二、准备工作
本文会基于 AOP、字节码框架(ASM、Javassist、Byte-Buddy),分别实现不同的监控实现代码。整个工程结构如下:
MonitorDesign
├── cn-bugstack-middleware-aop
├── cn-bugstack-middleware-asm
├── cn-bugstack-middleware-bytebuddy
├── cn-bugstack-middleware-javassist
├── cn-bugstack-middleware-test
└── pom.xml
复制代码
- 源码地址:github.com/fuzhengwei/…
- 简单介绍:aop、asm、bytebuddy、javassist,分别是四种不同的实现方案。test 是一个基于 SpringBoot 的简单测试工程。
- 技术使用:SpringBoot、asm、byte-buddy、javassist
cn-bugstack-middleware-test
@RestController
public class UserController {
private Logger logger = LoggerFactory.getLogger(UserController.class);
/**
* 测试:http://localhost:8081/api/queryUserInfo?userId=aaa
*/
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
logger.info("查询用户信息,userId:{}", userId);
return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑14-0000");
}
}
复制代码
- 接下来的各类监控代码实现,都会以监控 UserController#queryUserInfo 的方法执行信息为主,看看各类技术都是怎么操作的。
三、使用 AOP 做个切面监控
1. 工程结构
cn-bugstack-middleware-aop
└── src
├── main
│ └── java
│ ├── cn.bugstack.middleware.monitor
│ │ ├── annotation
│ │ │ └── DoMonitor.java
│ │ ├── config
│ │ │ └── MonitorAutoConfigure.java
│ │ └── DoJoinPoint.java
│ └── resources
│ └── META-INF
│ └── spring.factories
└── test
└── java
└── cn.bugstack.middleware.monitor.test
└── ApiTest.java
复制代码
基于 AOP 实现的监控系统,核心逻辑的以上工程并不复杂,其核心点在于对切面的理解和运用,以及一些配置项需要按照 SpringBoot 中的实现方式进行开发。
- DoMonitor,是一个自定义注解。它作用就是在需要使用到的方法监控接口上,添加此注解并配置必要的信息。
- MonitorAutoConfigure,配置下是可以对 SpringBoot yml 文件的使用,可以处理一些 Bean 的初始化操作。
- DoJoinPoint,是整个中间件的核心部分,它负责对所有添加自定义注解的方法进行拦截和逻辑处理。
2. 定义监控注解
cn.bugstack.middleware.monitor.annotation.DoMonitor
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMonitor {
String key() default "";
String desc() default "";
}
复制代码
- @Retention(RetentionPolicy.RUNTIME),Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
- @Retention 是注解的注解,也称作元注解。这个注解里面有一个入参信息 RetentionPolicy.RUNTIME 在它的注释中有这样一段描述:Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively. 其实说的就是加了这个注解,它的信息会被带到JVM运行时,当你在调用方法时可以通过反射拿到注解信息。除此之外,RetentionPolicy 还有两个属性 SOURCE、CLASS,其实这三个枚举正式对应了Java代码的加载和运行顺序,Java源码文件 -> .class文件 -> 内存字节码。并且后者范围大于前者,所以一般情况下只需要使用 RetentionPolicy.RUNTIME 即可。
- @Target 也是元注解起到标记作用,它的注解名称就是它的含义,目标,也就是我们这个自定义注解 DoWhiteList 要放在类、接口还是方法上。在 JDK1.8 中 ElementType 一共提供了10中目标枚举,TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE,可以参考自己的自定义注解作用域进行设置
- 自定义注解 @DoMonitor 提供了监控的 key 和 desc描述,这个主要记录你监控方法的为唯一值配置和对监控方法的文字描述。
3. 定义切面拦截
cn.bugstack.middleware.monitor.DoJoinPoint
@Aspect
public class DoJoinPoint {
@Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
public void aopPoint() {
}
@Around("aopPoint() && @annotation(doMonitor)")
public Object doRouter(ProceedingJoinPoint jp, DoMonitor doMonitor) throws Throwable {
long start = System.currentTimeMillis();
Method method = getMethod(jp);
try {
return jp.proceed();
} finally {
System.out.println("监控 - Begin By AOP");
System.out.println("监控索引:" + doMonitor.key());
System.out.println("监控描述:" + doMonitor.desc());
System.out.println("方法名称:" + method.getName());
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("监控 - End\r\n");
}
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
}
复制代码
- 使用注解 @Aspect,定义切面类。这是一个非常常用的切面定义方式。
- @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMoni