Metrics+Influxdb+Grafana构建监控系统(可能全网最详)

开篇

  一周一篇技术博文又来了,这周我们讲点什么呢?看标题就知道了,那就是利用Metrics,influxdb,Grafana构建一套生产可用的监控系统,至于为什么选择这套方案呢,因为简单易实现,并且公司有现成的环境可以使用,至于这三个技术的一些简单介绍还有使用方法....那当然还是Google去喽。
  这篇博文的重点是帮助你实现一个生产可用的监控系统,将遇到的坑告诉你,至于枯燥的知识当然需要你们自己补充了,这也是我以后博文的风格,只讲一些在其他博文中看不到的知识,嘿嘿。ok,凌云小课堂正式开始啦,今天要介绍的就是如何利用这三种技术搭建生产可用的监控系统

Metrics的使用和注意事项

  这节点我们讲Metrics的实现和需要的关注点

导包
 <!--metrics相关-->
        <dependency>
            <groupId>io.dropwizard.metrics</groupId>
            <artifactId>metrics-core</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.davidb</groupId>
            <artifactId>metrics-influxdb</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
            <version>0.8.2</version>
        </dependency>

  这里Metrics的版本是4.0.0,metrics-influxdb版本是0.8,2,这两个包都需要JDK1.8版本的支持,如果项目Java版本不是1.8是会有问题的。所以导包是要根据的项目java版本选择相关版本

相关数据写入配置文件
#influxdb 配置参数 
influxdb.ip=172.19.160.94
influxdb.port=8086
influxdb.database=metrics
#metrics数据上报时间间隔
metrics.reporterInterval=10

  这里主要有两个部分

  1. influxdb 的ip,端口号,和数据库名称 类似mysql
  2. metrics采集的项目数据上传influxdb的间隔时间,太过密集对项目本身和influxdb都是一种负担,实践10s是一个较为合理的时间
Spring注册MetricsRegistry
<!-- metrics-->
    <bean id="metricRegistry" class="com.codahale.metrics.MetricRegistry"/>
Metrics和influxdb初始化

  我们直接贴代码

/**
 * Influxdb初始化
 *
 * @author Lingyun
 * @Date 2018-12-10 23:56
 */
@Component
@Data
public class MetricsInitializer implements InitializingBean {

    @Autowired
    private MetricRegistry metricRegistry;

    @Value("#{settings['influxdb.ip']}")
    private String ip;
    @Value("#{settings['influxdb.port']}")
    private int port;
    @Value("#{settings['influxdb.database']}")
    private String dataBase;
    @Value("#{settings['metrics.reporterInterval']}")
    private long reporterInterval;

    @Override
    public void afterPropertiesSet() throws Exception {
    
        // 统计维度:部门和规则名称
        CategoriesMetricMeasurementTransformer example = new CategoriesMetricMeasurementTransformer("noticeName", "ruleName", "measurement");

        // 统计数据上报influxdb调度配置
        ScheduledReporter report = InfluxdbReporter
                .forRegistry(metricRegistry)// 注册metrics
                .protocol(InfluxdbProtocols.http(ip, port, dataBase))//数据库配置
                .tag("ip", InetAddress.getLocalHost().getHostAddress())//标签绑定单机IP
                .transformer(example)//表定义
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .filter(MetricFilter.ALL)
                .skipIdleMetrics(false)
                .build();

        long initalDelay = getBeginTime().getTimeInMillis() - System.currentTimeMillis();//延迟X(ms)启动
        long period = reporterInterval * 1000;//上报间隔时间(ms)

        // 启动
        report.start(initalDelay, period, TimeUnit.MILLISECONDS);
    }

    /**
     * 获取Metrics报告时间:
     * Metrics报告时间设定为启动后1分钟0秒开始,
     * 保证所有机器上的数据的开始时间都是从某分钟开始
     *
     * @return
     */
    private Calendar getBeginTime() {
        Calendar beginTime = Calendar.getInstance();
        beginTime.setTime(new Date());
        beginTime.add(Calendar.MINUTE, 1);
        beginTime.set(Calendar.SECOND, 0);// 秒
        beginTime.set(Calendar.MILLISECOND, 0);// 毫秒
        return beginTime;
    }
}

  初始化阶段需要着重注意的有以下几点

  1. CategoriesMetricMeasurementTransformer 对象的创建,这个对象代表的是influxdb中的表结构,而代码中noticeName和ruleName相当于表字段,是项目监控关注的维度,比如ruleName字段,是我想关注系统在规则这个维度的tps和耗时,而measurement是influxdb的表名,在后面会提到
  2. .tag("ip", InetAddress.getLocalHost().getHostAddress())//标签绑定单机IP 这段代码意思将单机的ip地址绑定到influxdb的表中,可以通过ip统计单机的各维度数据,不如单机的tps
  3. 最后启动方式使用了延迟加载,可以保证所有上传influxdb的数据都是从整点整分钟开始记录的
Metrics工厂方法

  ....我们还是直接贴代码

/**
 * Metrics工厂方法
 */
@Slf4j
public class MetricsFactory {

    private final static String TIMER_MEASUREMENTS = "xxxTimer";
    private final static String METER_MEASUREMENTS = "xxxMeter";
    private final ConcurrentHashMap<String, Meter> meterMap = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Timer> timerMap = new ConcurrentHashMap<>();

    private MetricsFactory() {
    }

    private static class MetricsFactoryInstance {
        private static final MetricsFactory INSTANCE = new MetricsFactory();
    }

    public static MetricsFactory getInstance() {
        return MetricsFactoryInstance.INSTANCE;
    }

    /**
     * xxx接口TPS统计
     *
     * @param noticeName
     * @param ruleName
     * @param metricRegistry
     * @return
     */
    public void getMeter(String noticeName, String ruleName, MetricRegistry metricRegistry) {
        try {
            String metricKey = MetricRegistry.name(noticeName, ruleName, METER_MEASUREMENTS);
            getMeter(metricKey, metricRegistry).mark();
        } catch (Exception e) {
            log.error("[NoticeCenter]create metrics(meter) error", e);
        }
    }

    /**
     * xxx接口耗时统计
     *
     * @param noticeName
     * @param ruleName
     * @param metricRegistry
     * @return
     */
    public Timer.Context getTimer(String noticeName, String ruleName, MetricRegistry metricRegistry) {
        try {
            String metricKey = MetricRegistry.name(noticeName, ruleName, TIMER_MEASUREMENTS);
            return getTimer(metricKey, metricRegistry).time();
        } catch (Exception e) {
            log.error("[NoticeCenter]create metrics(timer) error", e);
            return null;
        }
    }

    /**
     * 获取Meter实例
     *
     * @param metricsKey
     * @return
     */
    private Meter getMeter(String metricsKey, MetricRegistry metricRegistry) {
        Meter m = meterMap.get(metricsKey);
        if (m != null) {
            return m;
        }
        synchronized (MetricsFactory.class) {
            Meter metrics = meterMap.get(metricsKey);
            if (metrics != null) {
                return metrics;
            } else {
                Meter object = metricRegistry.meter(metricsKey);
                meterMap.putIfAbsent(metricsKey, object);
                return object;
            }
        }
    }

    /**
     * 获取Timer实例
     *
     * @param metricsKey
     * @return
     */
    private Timer getTimer(String metricsKey, MetricRegistry metricRegistry) {
        Timer t = timerMap.get(metricsKey);
        if (t != null) {
            return t;
        }
        synchronized (MetricsFactory.class) {
            Timer timer = timerMap.get(metricsKey);
            if (timer != null) {
                return timer;
            } else {
                Timer object = metricRegistry.timer(metricsKey);
                timerMap.putIfAbsent(metricsKey, object);
                return object;
            }
        }
    }

}

  这个工厂方法很简单,就是获取或者创建我们需要的metrics类型(metrics有五种类型,各个类型的用处含义自行Google),还是有几点需要你关心的地方

  1. xxxTimer 这个String字段代表influxdb中的表名
  2. String metricKey = MetricRegistry.name(noticeName, ruleName, TIMER_MEASUREMENTS); 这行代码是获取一个Metrics的Key值,metricRegistry本质是一个map,需要key建去读取和保存Metrics类型对象
  3. 这里一定要加锁保证并发量大的情况下项目数据不会因为取到同一个metrics对象,而导致统计数据的丢失
项目埋点

  这里因为保密原因我们只暴露部分代码,但保证逻辑的连贯性

// 注入metricRegistry
@Autowired
    private MetricRegistry metricRegistry;

  项目埋点

 //Metrics.Timer(请求时长)埋点
            Timer.Context context = MetricsFactory.getInstance().getNoticeTimer(noticeInfo.getNoticeName(), notifyRuleName, metricRegistry);
            // 3.接口逻辑执行点
            result = xxxxService.execute(xxxx);
            //Metrics.Meter(TPS)埋点
            MetricsFactory.getInstance().getNoticeMeter(noticeInfo.getNoticeName(), notifyRuleName, metricRegistry);
            //测量调用时间-结束
            stopTimerContext(context);

  stopTimerContext方法

 /**
     * 停止Metrics.Timer记录
     *
     * @param context
     */
    private void stopTimerContext(Timer.Context context) {
        try {
            if (context != null) {
                context.stop();
            }
        } catch (Exception e) {
            log.error("[NoticeCenter]metrics(timer) stop error", e);
        }
    }

  Timer主要记录接口耗时,Meter主要记录接口TPS
  到这里整个项目中Metrics相关的点都结束了,是不是很简单,只要将以上代码Copy根据实际项目改造一下就是一个生产可用的Metrics代码了

Influxdb相关

  influxdb这里只简单讲几点

  1. influxdb没有建表语句,所有不用跟我一样去找建表语句了,Metrics上报数据时第一条的插入数据就会自动建表。。。。
  2. influxdb 本身不提供数据的删除操作,因此用来控制数据量的方式就是定义数据保留策略。时间策略越短统计数据越准确。我的项目设定的策略为72h,大家可以根据自己系统的需求自行决定保存策略
  3. 最后就是influxdb 的 语法类似sql,但是我可悲的没有仔细去看。。。但不影响构建整个系统。。。
  4. 对于influxdb语法推荐几篇博文,有兴趣的可以去看看
1. https://www.linuxdaxue.com/influxdb-study-influxdb-selectors-funcitons.html
2. https://www.jianshu.com/p/a1344ca86e9b

Grafana相关

  Grafana是一个可视化系统。主要的功能和配置方式大家可以看一下这篇博文

https://www.cnblogs.com/Leo_wl/p/8506377.html 

  而我要讲的是如何在Grafana中配置一个可视化TPS监控页面


测试环境监控

  接下来我们就开始疯狂贴图,首先是系统整体TPS的配置


系统整体TPS.png

  接下来我们就开始疯狂贴图,各个细化维度的TPS配置,主要添加了分组标签 tag(部门),剩下的大家自行发挥,通过通知分组标签把各个单一维度结合起来

部门级TPS

  接下来的就是如何在Grafana中配置一个可视化接口耗时监控页面
单机ITC监控

单机ITC配置项

  最后就是如何在Grafana中配置一个可视化接口调用量饼图监控页


接口调用占比

部门维度配置项
简述

  整个监控系统中,metrics负责采集数据,influxdb负责保存数据,而Grafana负责展示数据,所以influxdb和Grafana只是辅助工具,重点还是数据的采集和维度的选择,数据采集到后,至于如何展示,就是对系统关注点的侧重了。下周再见,拜拜,希望看到这篇文章的你收获到了呢。

推荐阅读更多精彩内容