之前做了一些比较简单的测试,今天我们来看看如果使用elastic APM来发现和优化java应用的性能问题。另外,之前也说过,目前elastic APM主要是自动监控HTTP和SQL,对于非用户访问接口,或者非DB访问的任务,我们是否也能够监控呢
在开始之前,我们需要再熟悉一下APM中的事件(event)。
APM agent从其已监测的应用程序中捕获不同类型的信息,称为事件。事件可以是Errors,Spans或Transactions。然后将这些事件流式传输到APM server,由server验证并处理事件。
apm java agent默认会监控HTTP和SQL的请求,会在jvm中主动拦截常见数据库驱动层的类以进行监控。但并非所有的SQL访问都会被APM所监控,只有在判定为事务(transaction)或者跨度(span)的事件(event)中才会被监测(instrumented)。而目前,APM自动创建一个transaction的依据是,这是一个HTTP API(即Restful API或Web service,SOAP等)。因此,如果SQL语句位于一个HTTP API的调用链中,则该SQL肯定会被监测;如果你的SQL语句位于非HTTP API调用的函数中,则需要手动创建事务以进行监测。
以下是该栗子的基本信息:
写一个简单的rest controller,通过HTTP request,调用SQL语句。其中的UserService
接口封装了对DB的访问,createRandomUsers()
函数会创建多个随机User对象,addUser()
函数会将User对象插入到数据库当中。
在我们运行函数之前,请大家肉眼观察一下该函数的问题
@Controller
@RequestMapping( "/apm" )
public class UserController
{
@Resource
private UserService userService;
@RequestMapping( "/multiSql" )
@ResponseBody
public String addUser()
{
boolean AllSuccess = true;
List<User> users = createRandomUsers();
for( User user : users )
{
try{
AllSuccess &= this.userService.addUser( user );
}catch( Exception e ){
AllSuccess &= false;
}
}
return AllSuccess ? "All success" : "Some failure";
}
}
然后,我们运行程序。通过 http://localhost:9090/apm/multiSql 访问该程序10次后,进入APM页面。可以看到以下信息:
根据APM的数据我们可以分析得出:
因为代码写得比较简短,开发经验不丰富的开发人员会很容易漏掉其中的性能问题,因为无论是通过UT还是人工测试,140ms的响应时间对我们来说是无感的,很难发现。但是如果我们在开发的过程中使用了APM,我们就能很清楚的通过调用栈发现其中的问题。
现在,我们优化一下代码,在mybatis中加入batch操作:
SELECT LAST_INSERT_ID()
selectKey>
insert into user_t
(id, userName, password, age)
values
( #{User.id,jdbcType=INTEGER}, #{User.userName,jdbcType=VARCHAR}, #{User.password,jdbcType=VARCHAR},#{User.age,jdbcType=INTEGER})
foreach >
insert >
然后接口中新增一个boolean addUsers(List
函数,增加一个新的HTTP接口
@RequestMapping( "/batchSql" )
@ResponseBody
public void addUsers()
{
this.userService.addUsers( createRandomUsers() );
}
重启服务,这次通过postman运行100次,访问 batchSql
。再次打开APM的界面,可以看到有几个明显的变化,平均的响应时间只有13ms,另外,SQL语句只剩下一条insert和一条select,点击后,还可以看到变成了batch insert。
但,我们仍然有一次调用的时间特别的长。而且529毫秒,对于没有100% UT 覆盖的同学,或者不会用postman等工具,低等手工测试的同学,仍然是无法感知到这个性能问题的。
从调用的时间戳上我们还可以看到,该耗时最长的request是时间最靠前的。
在上一个栗子中,在APM的事务里,我们无法看到在INSERT语句开始之前到底发生了什么,无法直观的分析出为什么有一个HTTP的响应远超其他的响应(有经验的开发应该是一眼能够看到问题所在的,毕竟这个栗子比较简单)。也就是说,我们发现了问题,但是没有找到root cause。这时我们就需要手动将一些函数加入到我们到监测范围当中
首先,是我们的函数createRandomUsers()
,其次,是我们的数据层接口UserService userService;
方法是修改apm的配置文件:elasticapm.properties
。注意,该文件必须和apm java agent放在同一目录。增加以下内容:
trace_methods=com.example.sqldemo.controller.UserController#createRandomUsers,org.mybatis.*
在该修改中,我额外监控了createRandomUsers
函数,以及org.mybatis
下的所有类和方法。
这里需要注意的是:
,
进行分隔#
分隔具体查看官网解释 。
重启服务,再访问一次接口。打开APM界面,可以看到很大的不同:
从上图我们可以看到:
org.mybatis.*
使用了通配符的缘故filter by type
过滤掉所有我们额外添加的事务createRandomUsers
耗时很少,不是我们的性能瓶颈SpringManagedTransaction#getConnection
和SpringManagedTransaction#openConnection
由此可知,因为没有初始化和使用连接池,当我们第一次访问数据库的时候,我们需要花额外的时间去建立与DB的连接。另外,虽然我们没有明确的使用连接池,但是spring很贴心的为我们保留了与DB的连接,在后续的交互中,spring是自动使用了之前未释放的DB连接。所以,我们的测试结果是呈现有一条耗时很长的连接,其余的时间都很短的情况。借助于APM,我们可以发现问题,并深入跟踪。
那除了HTTP和SQL之外,我们在做开发的时候,仍然有很多其他的组件需要我们进行远程调用和访问,并有可能变为性能瓶颈。这里我们以用得比较多的redis来举例,看看elastic APM如何完成任务。另外,除了向外暴露的接口调用需要我们注意性能问题之外,我们在应用程序中也会创建很多定时任务来完成特殊的工作,它们无法被外部直接感知,但仍然有可能是性能瓶颈或者是关键任务,需要被监测。下面的栗子,我们将对redis的访问,放在定时任务中。
在主程序入口处增加@EnableScheduling
注解,在原有的代码中加入一个新的component
。这里假设我们有一个任务:
代码如下:
package com.example.sqldemo.service;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import javax.annotation.Resource;
import java.util.Iterator;
import java.util.Set;
/**
* Created by lij021 on 2019/2/18.
*/
@Component
public class MyTask
{
@Resource
private UserService userService;
@Scheduled( cron = "0/20 * * * * *" )
public void work()
{
System.out.println( Thread.currentThread().getName() );
Jedis jedis = new Jedis( "x.x.x.x", 6611 );
jedis.auth( "123456" );
Set<String> keys = jedis.keys( "*" );
Iterator<String> it = keys.iterator();
while( it.hasNext() )
{
String key = it.next();
//
}
// 对DB的访问
System.out.println( "getUserById" );
userService.getUserById( 1 );
}
}
因为这是一个内部运行的定时任务,elastic APM不会对其进行主动监测。因此,我们需要在启动项里面增加对该类的监控。另外,因为引用了Jedis
包,为了探测Jedis
的性能,我们也需要增加额外的对Jedis
的监测。
trace_methods=com.example.sqldemo.service.MyTask#*,redis.clients.jedis.Jedis#keys,redis.clients.jedis.BinaryJedis#*
其中:
com.example.sqldemo.service.MyTask#*
是对MyTask的监控redis.clients.jedis.Jedis#keys
是看看我们获取所有的keys需要多少时间redis.clients.jedis.BinaryJedis#*
是看看连接到Redis的性能重启程序,打开APM:
从APM中我们可以了解到如下信息:
jedis.auth( "123456" );
,即,我们仍然应该使用redis连接池来优化到目前为止,我们都在同一个线程里面做阻塞式的测试,比如上例,一直在scheduling-1线程中运行:
scheduling-1
getUserById
scheduling-1
getUserById
scheduling-1
getUserById
这里只需要改动代码,在主程序入口增加@EnableAsync
注解。
将redis的逻辑放到接口和接口实现类中:
public interface RedisService
{
void reachRedis();
}
实现类:
@Component
public class RedisServiceImpl
implements RedisService
{
@Override
@Async
public void reachRedis()
{
System.out.println( Thread.currentThread().getName() );
Jedis jedis = new Jedis( "*.*.*.*", 6611 );
jedis.auth( "123456" );
Set<String> keys = jedis.keys( "*" );
Iterator<String> it = keys.iterator();
while( it.hasNext() )
{
String key = it.next();
}
}
}
把之前的类修改为:
@Component
public class MyTask
{
@Resource
private UserService userService;
@Autowired
private RedisService redisService;
@Scheduled( cron = "0/20 * * * * *" )
public void work()
{
System.out.println( Thread.currentThread().getName() );
redisService.reachRedis();
// 对DB的访问
System.out.println( "getUserById" );
userService.getUserById( 1 );
}
}
因为增加了一个接口,一个类,将配置文件稍微改一下,将com.example.sqldemo.service.MyTask#*
改为com.example.sqldemo.service.*
。
运行程序,打开APM界面:
从上面的数据我们可以判定如下事实:
work
函数有关。work
函数有关,但elastic APM还是在事务里面包含了整个异步调用 reachRedis()
的生命周期。通过APM,我们同时观察到了异步函数的运行情况。这点,对于实际的测试来说是相当重要的,即我们可以知道通过异步,我们可以让用户触发任务之后,就能快速的得知结果,而不需要等待任务结束,同时,我们又能观察到与该事务相关联的异步任务到底运行了多长时间。未完待续,太长了,截断。请查看下该系列一篇文章
未完待续,太长了,截断。请查看下该系列一篇文章