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監控的藝術