服務監控-友好地整合Metrics到專案中

夢之痕發表於2019-01-30

1、概述

Metrics的基本介紹可以參考之前的文章:Metrics-服務指標度量

本文簡單介紹下如何將Metrics監控整合到我們的專案中。

本文所使用的metrics-core為3.1.0版本。

        <dependency>
            <groupId>io.dropwizard.metrics</groupId>
            <artifactId>metrics-core</artifactId>
            <version>3.1.0</version>
        </dependency>
複製程式碼

2、場景

我們的主要的監控需求有以下方面:

  • 機器指標

記憶體、執行緒、硬碟、服務GC情況等基本資訊是我們關心的核心指標。我們可以考慮通過Gauge指標項把這些機器指標做統一收集。

  • 服務介面請求頻率及耗時

請求頻率及耗時是我們服務介面效能的核心指標,我們可以考慮通過Timer指標項來採集相關資訊。

  • 服務內部基本資料

在某些場景下我們將內部Service統計到的瞬時指標上報,如Web Filter裡面統計當前正在處理的請求數等。我們也可以使用Gauge指標項來收集。

3、方案

針對於以上場景,我們雖然可以通過寫程式碼的方式建立和註冊相應的服務指標,可是在使用上卻不太友好。如何更方便靈活地將Metrics指標統計整合到我們的專案中呢?

3.1、MetricSet自動註冊,收集機器指標

  • (1) 預先定義好MetricSet;

指標集合MetricSet可參看metrics-jvm庫的MemoryUsageGaugeSet來定義,MemoryUsageGaugeSet定義了記憶體使用情況的基本指標,如下所示。

/**
 * A set of gauges for JVM memory usage, including stats on heap vs. non-heap memory, plus
 * GC-specific memory pools.
 */
public class MemoryUsageGaugeSet implements MetricSet {
    private static final Pattern WHITESPACE = Pattern.compile("[\\s]+");

    private final MemoryMXBean mxBean;
    private final List<MemoryPoolMXBean> memoryPools;

    public MemoryUsageGaugeSet() {
        this(ManagementFactory.getMemoryMXBean(),
             ManagementFactory.getMemoryPoolMXBeans());
    }

    public MemoryUsageGaugeSet(MemoryMXBean mxBean,
                               Collection<MemoryPoolMXBean> memoryPools) {
        this.mxBean = mxBean;
        this.memoryPools = new ArrayList<MemoryPoolMXBean>(memoryPools);
    }

    @Override
    public Map<String, Metric> getMetrics() {
        final Map<String, Metric> gauges = new HashMap<String, Metric>();

        gauges.put("total.init", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getInit() +
                        mxBean.getNonHeapMemoryUsage().getInit();
            }
        });

        gauges.put("total.used", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getUsed() +
                        mxBean.getNonHeapMemoryUsage().getUsed();
            }
        });

        gauges.put("total.max", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getMax() +
                        mxBean.getNonHeapMemoryUsage().getMax();
            }
        });

        gauges.put("total.committed", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getCommitted() +
                        mxBean.getNonHeapMemoryUsage().getCommitted();
            }
        });


        gauges.put("heap.init", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getInit();
            }
        });

        gauges.put("heap.used", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getUsed();
            }
        });

        gauges.put("heap.max", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getMax();
            }
        });

        gauges.put("heap.committed", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getHeapMemoryUsage().getCommitted();
            }
        });

        gauges.put("heap.usage", new RatioGauge() {
            @Override
            protected Ratio getRatio() {
                final MemoryUsage usage = mxBean.getHeapMemoryUsage();
                return Ratio.of(usage.getUsed(), usage.getMax());
            }
        });

        gauges.put("non-heap.init", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getNonHeapMemoryUsage().getInit();
            }
        });

        gauges.put("non-heap.used", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getNonHeapMemoryUsage().getUsed();
            }
        });

        gauges.put("non-heap.max", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getNonHeapMemoryUsage().getMax();
            }
        });

        gauges.put("non-heap.committed", new Gauge<Long>() {
            @Override
            public Long getValue() {
                return mxBean.getNonHeapMemoryUsage().getCommitted();
            }
        });

        gauges.put("non-heap.usage", new RatioGauge() {
            @Override
            protected Ratio getRatio() {
                final MemoryUsage usage = mxBean.getNonHeapMemoryUsage();
                return Ratio.of(usage.getUsed(), usage.getMax());
            }
        });

        for (final MemoryPoolMXBean pool : memoryPools) {
            gauges.put(name("pools",
                            WHITESPACE.matcher(pool.getName()).replaceAll("-"),
                            "usage"),
                       new RatioGauge() {
                           @Override
                           protected Ratio getRatio() {
                               final long max = pool.getUsage().getMax() == -1 ?
                                       pool.getUsage().getCommitted() :
                                       pool.getUsage().getMax();
                               return Ratio.of(pool.getUsage().getUsed(), max);
                           }
                       });
        }

        return Collections.unmodifiableMap(gauges);
    }
}
複製程式碼
  • (2) 通過BeanPostProcessor處理器自動註冊MetricSet物件Bean;
public class UserDefinedMetricBeanPostProcessor implements BeanPostProcessor {

    private final Logger LOG = LoggerFactory.getLogger(getClass());

    private final MetricRegistry metrics = MetricBeans.getRegistry();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        if (bean instanceof MetricSet) {
            MetricSet metricSet = (MetricSet) bean;
            if (!canRegister(beanName)) {
                return bean;
            }
            String metricName;
            if (isJvmCollector(beanName)) {
                metricName = Config.getProjectPrefix() + "." + beanName;
            } else {
                //根據規則生成Metric的名字
                metricName = Util.forMetricBean(bean.getClass(), beanName);
            }
            try {
                metrics.register(metricName, metricSet);
                LOG.debug("Registered metric named {} in registry. class: {}.", metricName, metricSet);
            } catch (IllegalArgumentException ex) {
                LOG.warn("Error injecting metric for field. bean named {}.", metricName, ex);
            }

        }
        return bean;
    }

    private boolean isJvmCollector(String beanName) {
        return beanName.indexOf("jvm") != -1;
    }

    private boolean canRegister(String beanName) {
        return !isJvmCollector(beanName) || Config.canJvmCollectorStart();
    }
}
複製程式碼
  • (3) 在spring xml檔案或通過spring註解定義bean物件;
    <!--定義Jvm監控物件-->
    <bean id="jvm.memory" class="com.codahale.metrics.jvm.MemoryUsageGaugeSet"/>
    <!--自動新增使用者定義的監控物件Metric-->
    <bean class="com.test.metrics.collector.UserDefinedMetricBeanPostProcessor"/>
複製程式碼

可以根據需要定製MetricSet集合,實現服務指標的自動註冊及上報。

3.2、結合註解實現成員變數自動註冊

我們可以結合註解實現成員變數的自動註冊。在BeanPostProcessor可以獲取到成員變數的註解,若是我們的目標註解,可以通過反射的方式獲取到變數資訊進行自動註冊。

下面以Gauged註解為例說明,Gauged註解可以讓成員變數自動註冊並上報。

  • (1) Gauged註解定義;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
public @interface Gauged {
    String name() default "";
}
複製程式碼
  • (2) 使用BeanPostProcessor解析Gauge註解並註冊;

核心程式碼如下所示:

    protected void withField(final Object bean, String beanName, Class<?> targetClass, final Field field) {
        ReflectionUtils.makeAccessible(field);

        final Gauged annotation = field.getAnnotation(Gauged.class);
        final String metricName = Util.forGauge(targetClass, field, annotation);

        metrics.register(metricName, new Gauge<Object>() {
            @Override
            public Object getValue() {
                return ReflectionUtils.getField(field, bean);
            }
        });

        LOG.debug("Created gauge {} for field {}.{}", metricName, targetClass.getCanonicalName(), field.getName());
    }
複製程式碼
  • (3) 在spring.xml檔案定義對應BeanPostProcessor即可使用。

基本使用如下:

@Component
public class GaugeUsage {

    @Gauged(name = "gaugeField")
    private int gaugedField = 999;
    
}
複製程式碼

3.3、結合註解實現方法切面的攔截統計

基於Spring AOP可以實現介面呼叫的耗時統計。

下面以Timed註解為例,Timed註解可以統計介面方法耗時情況。

  • (1) Timed註解定義;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Timed {
    String name() default "";
}
複製程式碼
  • (2) Timed註解切面定義;
@Component
@Aspect
public class MetricAspect {
    @Around("@annotation(timed)")
    public Object processTimerAnnotation(ProceedingJoinPoint joinPoint, Timed timed) throws Throwable {
        Class clazz = joinPoint.getTarget().getClass();
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        String metricName = Util.forTimedMethod(clazz, method, timed);
        Timer timer = MetricBeans.timer(metricName);
        final Timer.Context context = timer.time();
        try {
            return joinPoint.proceed();
        } finally {
            context.stop();
        }
    }
}
複製程式碼
  • (3) 在spring.xml檔案定義MetricAspect即可實現帶Timed註解介面的請求頻率和耗時統計。

基本示例如下:

@Component
public class TimedUsage {

    //@Timed註解會讓監控元件建立Timer物件,統計該方法的執行次數和執行時間等指標
    @Timed(name = "simple-timed-method")
    public void timedMethod() {
        for (int i = 0; i < 1000; i++) {
        }
    }
}
複製程式碼

4、總結

當前我們主要通過BenPostProcessorSpring AOP對類例項進行攔截,從而實現服務指標的自動註冊和收集。

相關文章