当前位置: 首页 > 科技观察

用AOP记录Javaweb应用的性能数据

时间:2023-03-17 19:25:35 科技观察

作为开发者,应用的性能永远是我们最感兴趣的话题之一。然而,并非所有开发人员都了解他们维护的应用程序的性能,更不用说快速定位性能瓶颈并实施解决方案了。今年北京Velocity的赞助商大多从事APM领域,提供性能分析、可视化甚至优化方案。这些厂商的产品看似能够帮助中小企业的开发者解决应用性能上的缺陷,但这些产品几乎都有一个致命的缺陷:侵入性极强。开发者需要在业务生产代码中嵌入APM厂商提供的嵌入式代码,才能使用APM厂商提供的Saas服务。在瞬息万变的技术大潮中,这种代码级的侵入和绑定,总是让开发者心存忧虑。如果我是架构师,我也会仔细考虑是自己搭建APM还是使用SaasAPM。但是,无论是自建APM还是使用Saas服务,底层模型无非是对海量日志的实时处理,数据源是应用产生的性能日志。如果我们有数据,让我们看看数据。如果我们只有意见,那就听我的吧。JimBarksdale这是一个数据为王的时代。夸张一点,数据可以指导一切!言归正传,如果我们不想使用APM试图提供的侵入式服务,只能自己搭建服务,比如收集线程内调用树和AOP中的调用开销和输出日志,然后使用ELK(Elasticsearch、Logstash、Kibana)收集日志,提供搜索、可视化等功能。如果采集的日志只是用于离线计算,可以直接使用Flume将日志写入HDFS。随着系统的流量越来越大,上述方案逐渐变得无法承受,这时就需要自己实现一个高性能的日志收集代理,将收集到的日志写入到Kafuka等可以承载大量的MQ中在累积的消息里面,然后用Storm/JStorm做实时流计算。几天前,我做了一个简单的尝试,基于AOP捕获调用树和开销。觉得有点意思,分享一下。获取调用树和时间开销在Java中获取代码块的时间开销的最常见方法是System.currentTimeMillis()。Apache和Guava等流行的库都有一个包装类StopWatch用于获取时间开销的功能。没有通用封装来捕获调用树。一种推荐的方法是在一次调用中给每个待分析的代码块一个唯一的标记,这个标记应该能够反映代码块之间的嵌套、顺序等关系。比如我们有如下调用关系。func1+-func2|+-func3|/-func4/-func5为了体现调用之间的嵌套和顺序,我们将func1标记为0,func2标记为0.1,func3标记为0.1.1,func4标记为0.1.2,标记func5为0.2。这样,我们可以很容易地从标记中重建调用树。我们可以用线程安全的方式封装捕获调用树和记录每个代码块的时间开销的功能,并给这个包取一个类似于Profiler的名字。Profiler提供了2个静态方法,enter在进入代码块前调用,exit在代码块结束后调用。在实现Profiler时,需要为每个线程维护一个调用堆栈和一个性能分析结果列表。基本上可以实现为enter压栈,exit出栈,将结果放入结果列表。当调用栈清空后,输出完整的分析结果。AOP和方法拦截器Profiler有一个契约需要严格执行,就是enter和exit必须成对调用,就像C++中的new和delete必须成对出现,否则直接炸毁内存,远不是内存泄漏那么简单。如果把这种约定写入业务代码,那就丑死了。各种尝试终于硬生生打断了业务逻辑。业务代码已经很恶心了,不可能再这么维护了。所以我们需要一种更科学的方式,以非侵入的方式实现对Profiler的正确调用。AOP是一个合适的工具。这里我们以SpringAOP为例,实现一个简单的例子。首先引入SpringAOP的依赖,或者说包含org.aopalliance.intercept.MethodInterceptor的包。org.springframeworkspring-aop2.5.6如果需要代码运行,还需要引入cglib依赖项。cglibcglib-nodep2.2方法拦截器的参考实现如下,使用类似try的代码模式最后确保Profiler被正确使用。publicclassInterceptorimplementsMethodInterceptor{@OverridepublicObjectinvoke(MethodInvocationinvocation)throwsThrowable{Classclazz=invocation.getMethod().getDeclaringClass();Stringmethod=invocation.getMethod().getName();Stringmark=clazz.getCanonicalName()+"#"+method;Profiler.ent(标记);try{returninvocation.proceed();}finally{Stringlog=Profiler.exit();if(log!=null){System.out.println(log);}}}}