使用Metrics方法級遠程監控Java程序

本文以Spring Web的後臺開發講解。html

上一篇講解了如何使用jvisualvm監控Java程序。jvisualvm雖然已經挺強大了,可是在實際的應用中依然不知足咱們的需求。如今,咱們想要監控應用程序中全部Controller提供的接口的訪問數量,頻次,響應時長。Service層方法的執行次數,執行時長,頻次等等。以便以後對系統的性能優化作準備。這個時候jvisualvm已經不能知足咱們的需求了。java

1 方法級監控Java程序的方案

這是我對於方法級監控Java程序的方案:程序員

  1. 付費的,好比YourKit,JProfile等。我嘗試了YourKit,功能確實強大,可是如今性能並非咱們如今的瓶頸,咱們儘可能使用不付費的。
  2. Metrics-Spring。Metrics-Spring須要在每一個方法上使用註解。咱們採用微服務架構,20多個服務,每一個工程預計平均有100左右個方法要監控。若是是一開始就用這個我以爲還能夠。
  3. Metrics+Spring AOP。從Metrics-Spring中能夠看到,Metrics統計的信息基本知足咱們的需求。咱們的項目需求是統計Controller層和Service層的方法。那麼能夠經過Spring中的切面完成咱們的需求。

我調查的方案和分析基本這樣,其餘人若是有更好的方案能夠提出一塊兒探討。web

下面是講解+部分代碼,本次講解還有優化篇。spring

2 Metrics的功能

關於Metrics的使用方法,已經有不少文章介紹了,我在這裏推薦我認爲還不錯的給你們,而後我再介紹的使用方法.性能優化

  1. Metrics介紹。這篇文章對Metrics的基本功能介紹的已經很全面了。
  2. Metrics-Spring官方文檔。這篇文章介紹了Metrics與Spring的集成,可是文檔感受不全呀。

其餘的文章我就很少分享了,感受大同小異。沒什麼太大差異。bash

3 將Metrics相關類裝載到Spring容器

要使用Metric,那麼首先須要MetricRegistry架構

咱們須要提供Http的報表,因此咱們須要將MetricsServlet註冊到Spring中,以即可以經過Http接口獲取監控結果。下面代碼咱們將監控接口定義爲:/monitor/metricsapp

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註解,讓配置根據配置屬性加載:ide

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的版本庫中找出來的,本身沒有再去嘗試,若有問題歡迎留言。請諒解。目前我已經對代碼進行了多處優化,優化內容將在下一篇講解,並會附上源碼。

最後歡迎關注個人我的公衆號。提問,嘮嗑,均可以。

假不正經程序員
相關文章
相關標籤/搜索