基於Prometheus閘道器的監控完整實現參考

等你歸去來發表於2020-09-20

  prometheus 是一個非常好的監控元件,尤其是其與grafana配合之後,更是如虎添翼。而prometheus的監控有兩種實現方式。1. server端主動拉取應用監控資料;2. 主動推送監控資料到prometheus閘道器。這兩種方式各有優劣,server端主動拉取實現可以讓應用專心做自己的事,根本無需關心外部監控問題,但有一個最大的麻煩就是server端需要主動發現應用的存在,這個問題也並不簡單(雖然現在的基於k8s的部署方式可以實現自動發現)。而基本prometheus閘道器推送的實現,則需要應用主動傳送相應資料到閘道器,即應用可以根據需要傳送監控資料,可控性更強,但也同時帶來一個問題就是需要應用去實現這個上報過程,實現得不好往往會給應用帶來些不必要的麻煩,而且基於閘道器的實現,還需要考慮閘道器的效能問題,如果應用無休止地傳送資料給閘道器,很可能將閘道器衝跨,這就得不償失了。

  而prometheus的sdk實現也非常多,我們可以任意選擇其中一個來做業務埋點。如: dropwizard, simpleclient ...  也並無好壞之分,主要看自己的業務需要罷了。比如 dropwizard 操作簡單功能豐富,但只支援單值的監控。而 simpleclient 支援多子標籤的的監控,可以用於豐富的圖表展現,但也需要更麻煩的操作等等。

  由於我們也許更傾向於多子標籤的支援問題,今天我們就基於 simpleclient 來實現一個完整地閘道器推送的元件吧。給大家提供一些思路和一定的解決方案。

 

1. pom 依賴引入

  如果我們想簡單化監控以及如果需要一些tps方面的資料,則可以使用 dropwizard 的依賴:

        <!-- jmx 埋點依賴 -->
        <dependency>
            <groupId>io.dropwizard.metrics</groupId>
            <artifactId>metrics-core</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>io.dropwizard.metrics</groupId>
            <artifactId>metrics-jmx</artifactId>
            <version>4.0.0</version>
        </dependency>

  當然,以上不是我們本文的基礎,我們基於simpleclient 依賴實現:

        <!-- https://mvnrepository.com/artifact/io.prometheus/simpleclient_pushgateway -->
        <dependency>
            <groupId>io.prometheus</groupId>
            <artifactId>simpleclient_pushgateway</artifactId>
            <version>0.9.0</version>
        </dependency>

  看起來simpleclient的依賴更簡單些呢!但實際上因為我們需要使用另外元件將dropwizard的資料暴露原因,不過這無關緊要。

 

2. metrics 埋點簡單使用

  dropwizard 的使用樣例如下:

public class PrometheusMetricManager {
    // 監控資料寫入容器
    private static final MetricRegistry metricsContainer = new MetricRegistry();

    static {
        // 使用 jmx_exporter 將埋點資料暴露出去
        JmxReporter jmxReporter = JmxReporter.forRegistry(metricsContainer).build();
        jmxReporter.start();
    }

    // 測試使用
    public static void main(String[] args) {
        // tps 類資料監控
        Meter meter = metricsContainer.meter("tps_meter");
        meter.mark();
        Map<String, Object> queue = new HashMap<>();
        queue.put("sss", 1);
        // 監控陣列大小
        metricsContainer.register("custom_metric", new Gauge<Integer>() {
            @Override
            public Integer getValue() {
                return queue.size();
            }
        });
    }
}

  

  simpleclient 使用樣例如下:

public class PrometheusMetricManager {

    /**
     * prometheus 註冊例項
     */
    private static final CollectorRegistry registry = new CollectorRegistry();


    /**
     * prometheus 閘道器例項
     */
    private static volatile PushGateway pushGatewayIns = new PushGateway("172.30.12.167:9091");

    public static void main(String[] args) {
        try{
            // 測試 gauge, counter
            Gauge guage = Gauge.build("my_custom_metric", "This is my custom metric.")
                                .labelNames("a").create().register(registry);
            Counter counter = Counter.build("my_counter", "counter help")
                                .labelNames("a", "b").create().register(registry);

            guage.labels("1").set(23.12);

            counter.labels("1", "2").inc();
            counter.labels("1", "3").inc();

            Map<String, String> groupingKey = new HashMap<String, String>();
            groupingKey.put("instance", "my_instance");
            // 推送閘道器資料
            pushGatewayIns.pushAdd(registry, "my_job", groupingKey);
        } catch (Exception e){
            e.printStackTrace();
        }
    }

}

  以上就是簡單快速使用prometheus的sdk進行埋點資料監控了,使用都非常簡單,即註冊例項、業務埋點、暴露資料; 

  但要做好管理埋點也許並不是很簡單,因為你可能需要做到易用性、可管理性、及效能。

 

3. 一個基於pushgateway 的管理metrics完整實現

  從上節,我們知道要做埋點很簡單,但要做到好的管理不簡單。比如如何做到易用?如何做到可管理性強?

  解決問題會有很多方法,我這邊給到方案是,要想易用,那麼我就封裝一些必要的介面給到應用層,比如應用層只做資料量統計,那麼我就只暴露一個counter的增加方法,其他一概隱藏,應用層想要使用埋點時也不用管什麼底層推送,資料結構之類,只需呼叫一個工廠方法即可得到操作簡單的例項。想要做可管理,那麼就必須要依賴於外部的配置系統,只需從外部配置系統一調整,應用立馬可以感知到,從而做出相應的改變,比如推送頻率、推送開關、閘道器地址。。。

  下面一個完整的實現樣例:

import com.my.mvc.app.common.util.ArraysUtil;
import com.my.mvc.app.common.util.IpUtil;
import com.my.mvc.app.component.metrics.types.*;
import io.prometheus.client.*;
import io.prometheus.client.exporter.PushGateway;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

/**
 * 功能描述: prometheus指標埋點 操作類
 *
 */
@Slf4j
public class PrometheusMetricManager {

    /**
     * prometheus 註冊例項
     */
    private static final CollectorRegistry registry = new CollectorRegistry();

    /**
     * 指標統一容器
     *
     *      counter: 計數器類
     *      gauge: 儀表盤類
     *      histogram: 直方圖類
     *      summary: 摘要類
     */
    private static final Map<String, CounterMetric> counterMetricContainer = new ConcurrentHashMap<>();
    private static final Map<String, TimerMetric> timerMetricContainer = new ConcurrentHashMap<>();
    private static final Map<String, CustomValueMetricCollector> customMetricContainer = new ConcurrentHashMap<>();
    private static final Map<String, Histogram> histogramMetricContainer = new ConcurrentHashMap<>();
    private static final Map<String, Summary> summaryMetricContainer = new ConcurrentHashMap<>();


    /**
     * prometheus 閘道器例項
     */
    private static volatile PushGateway pushGatewayIns;

    /**
     * prometheus gateway api 地址
     */
    private static volatile String gatewayApiCurrent;

    /**
     * 專案埋點統一字首
     */
    private static final String METRICS_PREFIX = "sys_xxx_";

    /**
     * 指標的子標籤key名, 統一定義
     */
    private static final String METRIC_LABEL_NAME_SERVER_HOST = "server_host";

    /**
     * 推送gateway執行緒池
     */
    private static final ScheduledExecutorService executorService =
            Executors.newScheduledThreadPool(1,
                    r -> new Thread(r, "Prometheus-push"));

    static {
        // 自動進行gateway資料上報
        startPrometheusThread();
    }

    private PrometheusMetricManager() {

    }

    /**
     * 註冊一個prometheus的監控指標, 並返回指標例項
     *
     * @param metricName 指標名稱(只管按業務命名即可: 數字+下劃線)
     * @param labelNames 所要監控的子指標名稱,會按此進行分組統計
     * @return 註冊好的counter例項
     */
    public static CounterMetric registerCounter(String metricName, String... labelNames) {
        CounterMetric counter = counterMetricContainer.get(metricName);
        if(counter == null) {
            synchronized (counterMetricContainer) {
                counter = counterMetricContainer.get(metricName);
                if(counter == null) {
                    String[] labelNameWithServerHost = ArraysUtil.addFirstValueIfAbsent(
                                        METRIC_LABEL_NAME_SERVER_HOST, labelNames);
                    Counter counterProme = Counter.build()
                            .name(PrometheusMetricManager.METRICS_PREFIX + metricName)
                            .labelNames(labelNameWithServerHost)
                            .help(metricName + " counter")
                            .register(registry);
                    counter = new PrometheusCounterAdapter(counterProme,
                            labelNameWithServerHost != labelNames);
                    counterMetricContainer.put(metricName, counter);
                }
            }
        }
        return counter;
    }

    /**
     * 註冊一個儀表盤指標例項
     *
     * @param metricName 指標名稱
     * @param labelNames 子標籤名列表
     * @return 儀表例項
     */
    public static TimerMetric registerTimer(String metricName, String... labelNames) {
        TimerMetric timerMetric = timerMetricContainer.get(metricName);
        if(timerMetric == null) {
            synchronized (timerMetricContainer) {
                timerMetric = timerMetricContainer.get(metricName);
                if(timerMetric == null) {
                    String[] labelNameWithServerHost = ArraysUtil.addFirstValueIfAbsent(
                                    METRIC_LABEL_NAME_SERVER_HOST, labelNames);
                    Gauge gauge = Gauge.build()
                            .name(METRICS_PREFIX + metricName)
                            .labelNames(labelNameWithServerHost)
                            .help(metricName + " gauge")
                            .register(registry);
                    timerMetric = new PrometheusTimerAdapter(gauge,
                            labelNameWithServerHost != labelNames);
                    timerMetricContainer.put(metricName, timerMetric);
                }
            }
        }
        return timerMetric;
    }

    /**
     * 註冊一個儀表盤指標例項
     *
     * @param metricName 指標名稱
     * @param valueSupplier 使用者自定義實現的單值提供實現
     */
    public static void registerSingleValueMetric(String metricName,
                                                 CustomMetricValueSupplier<? extends Number> valueSupplier) {
        CustomValueMetricCollector customMetric = customMetricContainer.get(metricName);
        if(customMetric == null) {
            synchronized (customMetricContainer) {
                customMetric = customMetricContainer.get(metricName);
                if(customMetric == null) {
                    String[] labelNameWithServerHost = {
                                METRIC_LABEL_NAME_SERVER_HOST };
                    CustomValueMetricCollector customCollector
                            = CustomValueMetricCollector.build()
                            .name(METRICS_PREFIX + metricName)
                            .labelNames(labelNameWithServerHost)
                            .valueSupplier(valueSupplier)
                            .help(metricName + " custom value metric")
                            .register(registry);
                    // 主動觸發固定引數的value計數
                    customCollector.labels(IpUtil.getLocalIp());
                    customMetricContainer.put(metricName, customCollector);
                }
            }
        }
    }

    /**
     * 定時推送指標到PushGateway
     */
    private static void pushMetric() throws IOException {
        refreshPushGatewayIfNecessary();
        pushGatewayIns.pushAdd(registry, "my_job");
    }

    /**
     * 保證pushGateway 為最新版本
     */
    private static void refreshPushGatewayIfNecessary() {
        // PushGateway地址
        com.ctrip.framework.apollo.Config config = com.ctrip.framework.apollo.ConfigService.getAppConfig();
        String gatewayApi = config.getProperty("prometheus_gateway_api", "10.1.20.121:9091");
        if(pushGatewayIns == null) {
            pushGatewayIns = new PushGateway(gatewayApi);
            return;
        }
        if(!gatewayApi.equals(gatewayApiCurrent)) {
            gatewayApiCurrent = gatewayApi;
            pushGatewayIns = new PushGateway(gatewayApi);
        }
    }

    /**
     * 開啟推送 gateway 執行緒
     *
     * @see #useCustomMainLoopPushGateway()
     * @see #useJdkSchedulerPushGateway()
     */
    private static void startPrometheusThread() {
        useCustomMainLoopPushGateway();
    }

    /**
     * 使用自定義純種迴圈處理推送閘道器資料
     */
    private static void useCustomMainLoopPushGateway() {
        executorService.submit(() -> {
            while (isPrometheusMetricsPushSwitchOn()) {
                try {
                    pushMetric();
                }
                catch (IOException e) {
                    log.error("【prometheus】推送gateway失敗:" + e.getMessage(), e);
                }
                finally {
                    sleep(getPrometheusPushInterval());
                }
            }

            // 針對關閉埋點採集後,延時檢測是否重新開啟了, 以便重新恢復埋點上報
            executorService.schedule(PrometheusMetricManager::startPrometheusThread,
                    30, TimeUnit.SECONDS);
        });
    }

    /**
     * 休眠指定時間(毫秒)
     *
     * @param millis 指定時間(毫秒)
     */
    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        }
        catch (InterruptedException e) {
            log.error("sleep異常", e);
        }
    }

    /**
     * 獲取prometheus推送閘道器頻率(單位:s)
     *
     * @return 頻率如: 60(s)
     */
    private static Integer getPrometheusPushInterval() {
        com.ctrip.framework.apollo.Config config = com.ctrip.framework.apollo.ConfigService.getAppConfig();
        return config.getIntProperty("prometheus_metrics_push_gateway_interval", 10) * 1000;
    }

    /**
     * 檢測apollo是否開啟推送閘道器資料開關
     *
     * @return true:已開啟, false:已關閉(不得推送指標資料)
     */
    private static boolean isPrometheusMetricsPushSwitchOn() {
        com.ctrip.framework.apollo.Config config = com.ctrip.framework.apollo.ConfigService.getAppConfig();
        return "1".equals(config.getProperty("prometheus_metrics_push_switch", "1"));
    }

    /**
     * 使用 scheduler 進行推送採集指標資料
     */
    private static void useJdkSchedulerPushGateway() {
        executorService.scheduleAtFixedRate(() -> {
            if(!isPrometheusMetricsPushSwitchOn()) {
                return;
            }
            try {
                PrometheusMetricManager.pushMetric();
            }
            catch (Exception e) {
                log.error("【prometheus】推送gateway失敗:" + e.getMessage(), e);
            }
        }, 1, getPrometheusPushInterval(), TimeUnit.SECONDS);
    }

    // 測試功能
    public static void main(String[] args) throws Exception {
        // 測試counter
        CounterMetric myCounter1 = PrometheusMetricManager.registerCounter(
                "hello_counter", "topic", "type");
        myCounter1.incWithLabelValues("my-spec-topic", "t1");

        // 測試 timer
        TimerMetric timerMetric = PrometheusMetricManager.registerTimer("hello_timer", "sub_label1");
        timerMetric.startWithLabelValues("key1");
        Thread.sleep(1000);
        timerMetric.stop();

        Map<String, Object> queue = new HashMap<>();
        queue.put("a", 11);
        queue.put("b", 1);
        queue.put("c", 222);

        // 測試佇列大小監控,自定義監控實現
        PrometheusMetricManager.registerSingleValueMetric(
                "custom_value_supplier", queue::size);

        // 佇列值發生變化
        Thread.sleep(60000);
        queue.put("d", 22);

        // 等待傳送執行緒推送資料
        System.in.read();
    }
}

  以上,就是我們們整個推送閘道器的管理框架了,遵循前面說的原則,只暴露幾個註冊介面,返回的例項按自定義實現,只保留必要的功能。使用時只管註冊,及使用有限功能即可。(注意:這不是寫一個通用框架,而僅是為某類業務服務的管理元件)

  實際也是比較簡單的,如果按照這些原則來做的話,應該會為你的埋點監控工作帶來些許的方便。另外,有些細節的東西我們稍後再說。

 

4. 自定義監控的實現

  上面我們看到,我們有封裝 CounterMetric, TimerMetric 以減少不必要的操作。這些主要是做了一下prometheus的一些代理工作,本身是非常簡單的,我們可以簡單看看。

/**
 * 功能描述: 計數器型埋點指標介面定義
 *
 *           <p>簡化不必要的操作方法暴露</p>
 *
 */
public interface CounterMetric {

    /**
     * 計數器 +1, 作別名使用 (僅對無多餘labelNames 情況), 預設無需實現該方法
     */
    default void inc() {
        incWithLabelValues();
    }

    /**
     * 帶子標籤型別填充的計數器 +1
     *
     * @param labelValues 子標籤值(與最初設定時順序個數一致)
     */
    void incWithLabelValues(String... labelValues);

}
 
// -------------- 以下是實現類 ---------------
import com.my.common.util.ArraysUtil;
import com.my.common.util.IPAddressUtil;
import io.prometheus.client.Counter;

/**
 * 功能描述: prometheus counter 介面卡實現
 *
 */
public class PrometheusCounterAdapter implements CounterMetric {

    private Counter counter;

    /**
     * 是否在頭部新增 主機名
     */
    private boolean appendServerHost;

    public PrometheusCounterAdapter(Counter counter, boolean appendServerHost) {
        this.counter = counter;
        this.appendServerHost = appendServerHost;
    }

    @Override
    public void incWithLabelValues(String... labelValues) {
        if(appendServerHost) {
            labelValues = ArraysUtil.addFirstValue(IPAddressUtil.getLocalIp(), labelValues);
        }
        counter.labels(labelValues).inc();
    }

}

  不復雜,看業務需要實現某些功能即可。供參考,其他類似功能可自行實現。

  我們主要來看一下自定義監控值的實現,主要場景如佇列大小監控。Prometheus 的 simpleclient 中給我們提供了幾種監控型別 Counter, Gauge, Histogram, Summary, 可能都不能很好的支援到我們這種自定義的實現。所以,需要自己幹下這件事。其與 Gauge 的實現是非常相似的,值都是可大可小可任意,所以我們可以參考Gauge的實現做出我們的自定義值監控。具體實現如下:

import io.prometheus.client.Collector;
import io.prometheus.client.GaugeMetricFamily;
import io.prometheus.client.SimpleCollector;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * 功能描述: prometheus 自定義單值監控工具(如:元素大小監控)
 *
 */
public class CustomValueMetricCollector
        extends SimpleCollector<CustomValueMetricCollector.Child>
        implements Collector.Describable {

    private CustomMetricValueSupplier<? extends Number> valueSupplier;

    private CustomValueMetricCollector(Builder b) {
        super(b);
        this.valueSupplier = b.valueSupplier;
        if(valueSupplier == null) {
            throw new IllegalArgumentException("unknown value supplier");
        }
    }

    /**
     *  Return a Builder to allow configuration of a new Gauge.
     */
    public static Builder build() {
        return new Builder();
    }

    @Override
    protected Child newChild() {
        return new Child(valueSupplier);
    }

    @Override
    public List<MetricFamilySamples> describe() {
        return Collections.<MetricFamilySamples>singletonList(
                    new GaugeMetricFamily(fullname, help, labelNames));
    }

    @Override
    public List<MetricFamilySamples> collect() {
        List<MetricFamilySamples.Sample> samples = new ArrayList<>(children.size());
        for(Map.Entry<List<String>, Child> c: children.entrySet()) {
            samples.add(new MetricFamilySamples.Sample(
                            fullname, labelNames, c.getKey(), c.getValue().get()));
        }
        return familySamplesList(Type.GAUGE, samples);
    }

    public static class Builder extends SimpleCollector.Builder
            <CustomValueMetricCollector.Builder, CustomValueMetricCollector> {
        private CustomMetricValueSupplier<? extends Number> valueSupplier;

        @Override
        public CustomValueMetricCollector create() {
            return new CustomValueMetricCollector(this);
        }

        /**
         * 自定義值提供者
         *
         * @param valueSupplier 提供者使用者實現實現
         * @param <T> 使用者返回的數值型別
         */
        public <T extends Number> Builder valueSupplier(
                        CustomMetricValueSupplier<T> valueSupplier) {
            this.valueSupplier = valueSupplier;
            return this;
        }
    }


    /**
     * 多標籤時使用的子項描述類
     *
     *      實際上並不支援多標籤配置,除了一些統一標籤如 IP
     */
    public static class Child {

        private CustomMetricValueSupplier<? extends Number> valueSupplier;

        Child(CustomMetricValueSupplier<? extends Number> valueSupplier) {
            this.valueSupplier = valueSupplier;
        }

        /**
         * Get the value of the gauge.
         */
        public double get() {
            return Double.valueOf(valueSupplier.getValue().toString());
        }
    }
}

  之所以要使用到 Child, 是因為我們需要支援多子標籤的操作,所以稍微繞了一點。不過總體也不復雜。而且對於單值提供者的實現,也只有一個 getValue 方法,這會很好地讓我們利用 Lamda 表示式,寫出極其簡單的提供者實現。介面定義如下:

/**
 * 功能描述:  單值型度量 提供者(使用者自定義實現)
 *
 * @param <T> 返回的資料型別,一定是數值型喲
 */
public interface CustomMetricValueSupplier<T extends Number> {

    /**
     * 使用者實現的提供度量值方法
     */
    T getValue();
}

  具體使用時就非常簡單了:

    // 測試佇列大小監控,自定義監控實現
    PrometheusMetricManager.registerSingleValueMetric(
            "custom_value_supplier", queue::size);

  

  如此,一個完整的監控資料上報功能就完成了。你要做的僅是找到需要監控的業務點,然後使用僅有api呼叫就可以了,至於後續是使用jmx上報,主動上報,閘道器推送。。。 你都不需要關心了,而且還可以根據情況隨時做出調整。

  至於後續的監控如何做,可以參考我另一篇文章(grafana方案): 快速構建業務監控體系,觀grafana監控的藝術

 

相關文章