服务监控-如何将Metrics集成到项目中

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对类实例进行拦截,从而实现服务指标的自动注册和收集。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容