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配置,主要添加了分組標籤 tag(部門),剩下的大家自行發揮,通過通知分組標籤把各個單一維度結合起來


  接下來的就是如何在Grafana中配置一個可視化接口耗時監控頁面

  最後就是如何在Grafana中配置一個可視化接口調用量餅圖監控頁



簡述

  整個監控系統中,metrics負責採集數據,influxdb負責保存數據,而Grafana負責展示數據,所以influxdb和Grafana只是輔助工具,重點還是數據的採集和維度的選擇,數據採集到後,至於如何展示,就是對系統關注點的側重了。下週再見,拜拜,希望看到這篇文章的你收穫到了呢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章