介绍
各个系统都在追求性能,要做压力测试。
压力测试目的是要找到热点,性能瓶颈,然后解决它或者优化它,这边暂不讨论怎么解决和优化性能问题。如果不做大规模的性能测试,是否有其他方式在开发,单元测试和集成测试的时候就发现一些性能问题呢?
我个人比较喜欢在编码的各个阶段都去注意性能问题,并不希望在把问题留到大规模性能测试之后再发现。那样费时费力,在解决性能瓶颈和发现下一个性能瓶颈的过程中来来回回,压力测试的同学在叹气,开发的同学在抓头。
不说废话了。进入正题。假设我们能够通过DB查询出系统中各个方法的执行时间,那运用各种group by
,order by
。就能轻松知道当前系统的状态,有可能产生的瓶颈。不仅如此,还能清楚知道每一笔交易,对各个方法的执行次数。是否有大量不必要的循环等等。参考下表。
CREATE TABLE
service_method_record
(
id bigint NOT NULL AUTO_INCREMENT,
access_jnl CHAR(32), --流水号
start_time TIMESTAMP NULL, --开始时间
end_time TIMESTAMP NULL, --结束时间
method_name VARCHAR(100), --方法名称
use_time INT, --用时多久,毫秒
env_name VARCHAR(20), --环境名字,DEV,UAT,版本机等
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码
系统中方法太多,还有各种工具方法,根本没必要统计。如果在某个方法上给个注解@LogMethodTime
,这样就表示要统计这个方法的调用时长。岂不是美滋滋。
使用Aspect
做面向切面,拦截方法并且在方法入口和出口做时间统计操作。
看下示意图:
记录方式
1.可以记录到日志中,这样性能消耗比较小
2.可以记录到DB
中,但是性能消耗比较大,建议使用消息队列做异步记录。
记录到日志中
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogMethodTime {
}
复制代码
@Aspect
public class LogMethodTimeAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(LogMethodTimeAspect.class);
@Value("${common.logSwitch}")
private boolean logSwitch;// 开关,true:打开,false:关闭
@Pointcut("@annotation(LogMethodTime)")
public void pointcutName() {
}
@Around("pointcutName()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
if (!logSwitch) {
return pjp.proceed();
}
// 获取方法名(类全路径+.+方法名)
String classFullName = pjp.getSignature().getDeclaringTypeName();
String className = classFullName.substring(classFullName.lastIndexOf(".") + 1);
// 比如:com.xxx.XxxService.xxxMethod
String name = className + "." + pjp.getSignature().getName();
long start = System.currentTimeMillis();
// 记录开始时间
LOGGER.debug("-------------------start method:" + name + "-------------------");
Object result = pjp.proceed();
// 记录结束时间和用时
LOGGER.debug("-------------------end method:" + name + ", use time " + (System.currentTimeMillis() - start) + "-------------------");
return result;
}
}
复制代码
两个东西
① 注解定义
② Aspect的实现(对Aspect
不熟悉可以参考相关文档,这边不做详细赘述)
Aspect
在Spring
中的配置就不细说了。
####记录到DB中
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogMethodTime2DB {
}
复制代码
@Aspect
public class LogMethodTime2DBAspect {
@Autowired
private ServiceMethodRecordService serviceMethodRecordService;
@Value("${common.logSwitch}")
private boolean logSwitch;// 开关,true:打开,false:关闭
@Pointcut("@annotation(LogMethodTime2DB)")
public void pointcutName() {
}
@Around("pointcutName()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
if (!logSwitch) {
return pjp.proceed();
}
// 获取方法名(类全路径+.+方法名)
String classFullName = pjp.getSignature().getDeclaringTypeName();
String className = classFullName.substring(classFullName.lastIndexOf(".") + 1);
// 比如:com.xxx.XxxService.xxxMethod
String name = className + "." + pjp.getSignature().getName();
// 这边用了消息队服务去做初始化
JSONObject recordObj = serviceMethodRecordService.init(name, System.currentTimeMillis());
Object result = pjp.proceed();
// 这边用了消息队服务去做update
serviceMethodRecordService.update(recordObj.getString("id"), recordObj.getLongValue("startTime"), System.currentTimeMillis());
return result;
}
}
复制代码
上面这个记录DB
的案例中,我用消息队列服务。先做初始化,调用完实际invoke
方法后,在发一条update
的消息。
注意:这前后两条消息务必是顺序的。保证消息有序的方式属于各个消息队列的事情,比如kafka
,只要保证两条有前后顺序的消息在分配到同一个partition
中就是有序的。
使用
把上述的一些类,注解,各种都配置好后,使用起来就舒服了。在想要监控的方法体上加上注解即可。
@LogMethodTime
public void method() {
// do something
}
复制代码
@LogMethodTime2DB
public void method() {
// do something
}
复制代码
如果某天想把所有的这些注解都去掉。那就统一搜索下,统一删除下。几分钟就搞定了。但我觉得没必要,既然加了开关了,删除它干嘛?把开关设为false
即可。
总结
这样的小技巧,能在开发,单元测试,集成测试的过程中就发现很多问题,没有必要留到后面性能测试。实现中务必加上配置开关,默认可以是false
,这东西最好不要在生产跑。