elastic APM针对java应用的高阶用法(java agent)

文章目录

    • 事件(event)
    • SQL的监控
      • 栗子1,看不到的多个SQL
      • 栗子2,batch SQL
    • 手动添加跨度
    • 定时任务中的远程通信监控
    • 异步执行的函数监控
    • 异常监控
    • 与日志分析系统的配合

elastic APM还在不停的迭代当中。相对于其他的APM工具,我觉得如果有elastic APM有几个优势:

  • 背后是elasticsearch集群,可承载PB级数据,并且大部分的企业都会部署es集群,技术和资源共享
  • elasticsearch作为最常用的开源日志分析工具,APM可与日志分析相结合
  • 目前是免费的。属于elastic basic license中的基本功能
  • 支持多种语言,监控客户端包括java/go/python/ruby/nodejs/rail等
  • 社区还算活跃,版本升级比较快

之前做了一些比较简单的测试,今天我们来看看如果使用elastic APM来发现和优化java应用的性能问题。另外,之前也说过,目前elastic APM主要是自动监控HTTP和SQL,对于非用户访问接口,或者非DB访问的任务,我们是否也能够监控呢

事件(event)

在开始之前,我们需要再熟悉一下APM中的事件(event)。

APM agent从其已监测的应用程序中捕获不同类型的信息,称为事件。事件可以是Errors,Spans或Transactions。然后将这些事件流式传输到APM server,由server验证并处理事件。

  • Errors 包含捕获的错误或异常的相关信息。
  • Spans包含已执行的特定代码路径的相关信息。它们从活动的开始到结束进行测量,并且可以与其他跨度建立父/子关系。
  • Transactions是一种特殊的跨度,具有与之关联的额外元数据。您可以将Transactions视为你在服务中衡量的最高级别的工作。例如,提供HTTP请求或运行特定的后台作业。

SQL的监控

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调用的函数中,则需要手动创建事务以进行监测。

以下是该栗子的基本信息:

  • 为了简便,我们先将SQL语句放在HTTP请求的处理函数中。
  • 因为已经很少有人直接使用JDBC,栗子中使用比较流行的Mybatis作为数据层框架
  • 对应的数据库为MySQL
  • 为了减少文章长度,这里只展示最基本的代码。对于如何搭建APM环境,请查看我之前的文章
  • 程序在启动的时候,默认attach java apm agent
    elastic APM针对java应用的高阶用法(java agent)_第1张图片

栗子1,看不到的多个SQL

写一个简单的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页面。可以看到以下信息:

  • 10次的平均响应时间是194ms
  • 95%的响应都是在772ms以下
  • 因为我按的时间有间隔,tpm是0.7

elastic APM针对java应用的高阶用法(java agent)_第2张图片
然后我们进入该事务。我们可以观察到:

  • 大部分的HTTP request都在200ms内完成响应
  • 有一条HTTP request的响应是772ms
  • 每个次调用中都包含了大量的insert和select语句

根据APM的数据我们可以分析得出:

  • 代码中每次只插入一条数据,并且每次插入之后还做了一次select操作
  • 服务的响应时间不均衡,有的服务响应时间特别长

因为代码写得比较简短,开发经验不丰富的开发人员会很容易漏掉其中的性能问题,因为无论是通过UT还是人工测试,140ms的响应时间对我们来说是无感的,很难发现。但是如果我们在开发的过程中使用了APM,我们就能很清楚的通过调用栈发现其中的问题。

栗子2,batch SQL

现在,我们优化一下代码,在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 records);函数,增加一个新的HTTP接口

    @RequestMapping( "/batchSql" )
    @ResponseBody
    public void addUsers()
    {
        this.userService.addUsers( createRandomUsers() );
    }

重启服务,这次通过postman运行100次,访问 batchSql。再次打开APM的界面,可以看到有几个明显的变化,平均的响应时间只有13ms,另外,SQL语句只剩下一条insert和一条select,点击后,还可以看到变成了batch insert。
elastic APM针对java应用的高阶用法(java agent)_第3张图片
elastic APM针对java应用的高阶用法(java agent)_第4张图片
但,我们仍然有一次调用的时间特别的长。而且529毫秒,对于没有100% UT 覆盖的同学,或者不会用postman等工具,低等手工测试的同学,仍然是无法感知到这个性能问题的。
elastic APM针对java应用的高阶用法(java agent)_第5张图片
从调用的时间戳上我们还可以看到,该耗时最长的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下的所有类和方法。

这里需要注意的是:

  • trace_methods接受一个列表,列表中的对象或者方法,会作为transation或者span的触发者
  • 列表中的元素以,进行分隔
  • 元素可以是package,class或method,可使用通配符
  • class和method之间需要用#分隔

具体查看官网解释 。

重启服务,再访问一次接口。打开APM界面,可以看到很大的不同:

从上图我们可以看到:

  1. 在监控的程序下多出了很多与mybatis相关的事务,这是因为我们对org.mybatis.*使用了通配符的缘故
  2. 我们可以通过filter by type过滤掉所有我们额外添加的事务
  3. createRandomUsers耗时很少,不是我们的性能瓶颈
  4. 主要的耗时来自于SpringManagedTransaction#getConnectionSpringManagedTransaction#openConnection

由此可知,因为没有初始化和使用连接池,当我们第一次访问数据库的时候,我们需要花额外的时间去建立与DB的连接。另外,虽然我们没有明确的使用连接池,但是spring很贴心的为我们保留了与DB的连接,在后续的交互中,spring是自动使用了之前未释放的DB连接。所以,我们的测试结果是呈现有一条耗时很长的连接,其余的时间都很短的情况。借助于APM,我们可以发现问题,并深入跟踪。

定时任务中的远程通信监控

那除了HTTP和SQL之外,我们在做开发的时候,仍然有很多其他的组件需要我们进行远程调用和访问,并有可能变为性能瓶颈。这里我们以用得比较多的redis来举例,看看elastic APM如何完成任务。另外,除了向外暴露的接口调用需要我们注意性能问题之外,我们在应用程序中也会创建很多定时任务来完成特殊的工作,它们无法被外部直接感知,但仍然有可能是性能瓶颈或者是关键任务,需要被监测。下面的栗子,我们将对redis的访问,放在定时任务中。

在主程序入口处增加@EnableScheduling注解,在原有的代码中加入一个新的component。这里假设我们有一个任务:

  • 需要每20秒运行一次
  • 需要在redis上读取某些状态
  • 然后对DB进行操作

代码如下:

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中我们可以了解到如下信息:

  • 平均响应时间是271ms
  • 代码是每隔20秒运行一次,因为每次运行消耗~300ms,所以tpm不是3,而是2.9
  • 绝大部分的耗时用在jedis.auth( "123456" );,即,我们仍然应该使用redis连接池来优化
  • 在redis调用结束,select语句开始之前,仍然有一段空白,表明获取DB connection仍然是性能瓶颈之一
  • 虽然每次调用都需要建立redis和db的connection,仍然有一个次调用是远远多于其他调用,需要更深入的分析

异步执行的函数监控

到目前为止,我们都在同一个线程里面做阻塞式的测试,比如上例,一直在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界面:

从上面的数据我们可以判定如下事实:

  • 整个transaction的耗时,只与work函数有关。
  • 第一次调用的时候,因为需要获取DB connection,花了700ms。但之后的调用,直接获取DB connection。我们发现,在没有直接使用连接池的情况下,spring对同步任务和异步任务出现了不同的处理。没做异步处理之前,每次访问DB,都需要花时间来做初始化,在做异步处理之后,这段时间没了,db的平均访问时间降到了3ms
  • 虽然整个transaction的耗时,只与work函数有关,但elastic APM还是在事务里面包含了整个异步调用 reachRedis() 的生命周期。通过APM,我们同时观察到了异步函数的运行情况。这点,对于实际的测试来说是相当重要的,即我们可以知道通过异步,我们可以让用户触发任务之后,就能快速的得知结果,而不需要等待任务结束,同时,我们又能观察到与该事务相关联的异步任务到底运行了多长时间

异常监控

未完待续,太长了,截断。请查看下该系列一篇文章

与日志分析系统的配合

未完待续,太长了,截断。请查看下该系列一篇文章

你可能感兴趣的:(ELK,Java,点火三周的Elastic,Stack专栏)