使用Metrics方法级远程监控Java程序

本文以Spring Web的后台开发讲解。

上一篇讲解了如何使用jvisualvm监控Java程序。jvisualvm虽然已经挺强大了,但是在实际的应用中依然不满足我们的需求。现在,我们想要监控应用程序中所有Controller提供的接口的访问数量,频次,响应时长。Service层方法的执行次数,执行时长,频次等等。以便之后对系统的性能优化做准备。这个时候jvisualvm已经不能满足我们的需求了。

1 方法级监控Java程序的方案

这是我对于方法级监控Java程序的方案:

  1. 付费的,比如YourKit,JProfile等。我尝试了YourKit,功能确实强大,但是现在性能并不是我们现在的瓶颈,我们尽量使用不付费的。
  2. Metrics-Spring。Metrics-Spring需要在每个方法上使用注解。我们采用微服务架构,20多个服务,每个工程预计平均有100左右个方法要监控。如果是一开始就用这个我觉得还可以。
  3. Metrics+Spring AOP。从Metrics-Spring中可以看到,Metrics统计的信息基本满足我们的需求。我们的项目需求是统计Controller层和Service层的方法。那么可以通过Spring中的切面完成我们的需求。

我调查的方案和分析基本这样,其他人如果有更好的方案可以提出一起探讨。

下面是讲解+部分代码,本次讲解还有优化篇。

2 Metrics的功能

关于Metrics的使用方法,已经有很多文章介绍了,我在这里推荐我认为还不错的给大家,然后我再介绍的使用方法.

  1. Metrics介绍。这篇文章对Metrics的基本功能介绍的已经很全面了。
  2. Metrics-Spring官方文档。这篇文章介绍了Metrics与Spring的集成,但是文档感觉不全呀。

其他的文章我就不多分享了,感觉大同小异。没什么太大差别。

3 将Metrics相关类装载到Spring容器

要使用Metric,那么首先需要MetricRegistry

我们需要提供Http的报表,所以我们需要将MetricsServlet注册到Spring中,以便可以通过Http接口获取监控结果。下面代码我们将监控接口定义为:/monitor/metrics

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.servlets.MetricsServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MonitorConfig {

    @Bean
    public MetricRegistry metricRegistry() {
        return new MetricRegistry();
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(MetricRegistry metricRegistry) {
        return new ServletRegistrationBean(new MetricsServlet(metricRegistry), "/monitor/metrics");
    }

}
复制代码

4 提供可控的终端报表

另外,为了方便调试,我希望支持终端输出报表的方式。但是要可以配置打开和关闭,于是我使用另外一个配置类,ConditionalOnProperty注解,让配置根据配置属性加载:

import com.codahale.metrics.ConsoleReporter;
import com.codahale.metrics.MetricRegistry;
import lombok.extern.java.Log;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import java.util.concurrent.TimeUnit;

@Configuration
@Log
@ConditionalOnProperty(prefix = "monitor.report", name = "console", havingValue = "true")
@Import(MonitorConfig.class)
public class MonitorReportConfig {


    @Bean
    public ConsoleReporter consoleReporter(MetricRegistry metrics) {
        ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics)
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .build();
        reporter.start(10, TimeUnit.SECONDS);
        return reporter;
    }

}
复制代码

这样可以在工程中的application.properties文件中,通过下面配置开启终端报表,10秒钟输出一次:

monitor.report.console = true
复制代码

5 为要监控的方法准备Timer

Metrics中可以统计的信息很多,其中Timer已经满足了我们需要的信息。

我为什么要先为监控的方法准备Timer,而不是在方法执行的时候再创建呢?原因有两点。

  1. 我们既关心方法被调,也关心它从来没有被调用,如果是在方法执行的时候再创建,那么我们就不知道是方法没有被监控还是方法没有被调用了。
  2. 我们之后打算直接对@RestController,@Controller和@Service注解进行切面。这种类级别的切面力度会包含我们不关心的方法,例如toString等方法,所以准备好关心的方法,调用的时候发现不是我们关心的方法直接放过。

我们使用MethodMonitorCenter类来收集我们想要监控的方法。通过实现ApplicationContextAware接口,在Spring容器装载完毕之后,会回掉setApplicationContext方法,我们通过getBeansWithAnnotation方法找到包含指定注解的类。然后对其进行过滤,并获取我们想要监控的方法。在最后我们通过metricRegistry.timer(method.toString());方法为我们的关心的方法准备一个timer。

@Component
@Getter
@Log
public class MethodMonitorCenter implements ApplicationContextAware {

    public static final String PACKAGE_NAME = "com.sinafenqi";  // 这里换成自己的包名

    @Autowired
    private MetricRegistry metricRegistry;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, Object> monitorBeans = new HashMap<>();
        monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Controller.class));
        monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Service.class));
        monitorBeans.putAll(applicationContext.getBeansWithAnnotation(RestController.class));

        log.info("monitor begin scan methods");
        monitorBeans.values().stream()
                .map(obj -> obj.getClass().getName())
                .map(this::trimString)
                .map(clzName -> {
                    try {
                        return Class.forName(clzName);
                    } catch (Exception e) {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .filter(aClass -> aClass.getName().startsWith(PACKAGE_NAME))
                .forEach(this::getClzMethods);
    }

    private void getClzMethods(Class<?> clz) {
        Stream.of(clz.getDeclaredMethods())
                .filter(method -> method.getName().indexOf('$') < 0)
                .forEach(method -> {
                    log.info("add method timer, method name :" + method.toGenericString());
                    metricRegistry.timer(method.toString());
                });
    }

    private String trimString(String clzName) {
        if (clzName.indexOf('$') < 0) return clzName;
        return clzName.substring(0, clzName.indexOf('$'));
    }

}
复制代码

6 在切面中对方法进行监控

然后我们可以在切面中监控我们关心的方法。这里使用环绕式切面对RestControllerController,和Service三个注解做切面。这样就可以在方法之前和之后加一些监控代码。当进入around函数的时候,我们先去MetricRegistry中查找有没有对应的timer,如果没有说明不是我们关心的方法,那么我们就可以直接执行,如果存在,那么我就对其进行监控。详情可见代码:

@Component
@Aspect
@Log
public class MetricsMonitorAOP {

    @Autowired
    private MetricRegistry metricRegistry;

    @Pointcut("@within(org.springframework.stereotype.Controller)" +
            "||@within(org.springframework.stereotype.Service)" +
            "||@within(org.springframework.web.bind.annotation.RestController)")
    public void monitor() {

    }

    @Around("monitor()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String target = joinPoint.getSignature().toLongString();
        Object[] args = joinPoint.getArgs();
        if (!metricRegistry.getNames().contains(target)) {
            return joinPoint.proceed(args);
        }
        Timer timer = metricRegistry.timer(target);
        Timer.Context context = timer.time();
        try {
            return joinPoint.proceed(args);
        } finally {
            context.stop();
        }
    }
}

复制代码

7 效果

之后访问/monitor/metrics接口,就可以以Json的数据格式获取监控结果。大家实验的时候记得把MethodMonitorCenter类中的PACKAGE_NAME常量换成自己的。

现在基本已经实现监控所有Controller,和Service层我们定义的方法了,但是代码依然有很大的优化空间。这些代码是我从Git的版本库中找出来的,自己没有再去尝试,如有问题欢迎留言。请谅解。目前我已经对代码进行了多处优化,优化内容将在下一篇讲解,并会附上源码。

最后欢迎关注我的个人公众号。提问,唠嗑,都可以。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值