基于Prometheus網關的監(jiān)控完整實現(xiàn)參考
prometheus 是一個非常好的監(jiān)控組件,尤其是其與grafana配合之后,更是如虎添翼。而prometheus的監(jiān)控有兩種實現(xiàn)方式。1. server端主動拉取應用監(jiān)控數(shù)據(jù);2. 主動推送監(jiān)控數(shù)據(jù)到prometheus網關。這兩種方式各有優(yōu)劣,server端主動拉取實現(xiàn)可以讓應用專心做自己的事,根本無需關心外部監(jiān)控問題,但有一個最大的麻煩就是server端需要主動發(fā)現(xiàn)應用的存在,這個問題也并不簡單(雖然現(xiàn)在的基于k8s的部署方式可以實現(xiàn)自動發(fā)現(xiàn))。而基本prometheus網關推送的實現(xiàn),則需要應用主動發(fā)送相應數(shù)據(jù)到網關,即應用可以根據(jù)需要發(fā)送監(jiān)控數(shù)據(jù),可控性更強,但也同時帶來一個問題就是需要應用去實現(xiàn)這個上報過程,實現(xiàn)得不好往往會給應用帶來些不必要的麻煩,而且基于網關的實現(xiàn),還需要考慮網關的性能問題,如果應用無休止地發(fā)送數(shù)據(jù)給網關,很可能將網關沖跨,這就得不償失了。
而prometheus的sdk實現(xiàn)也非常多,我們可以任意選擇其中一個來做業(yè)務埋點。如:dropwizard, simpleclient ... 也并無好壞之分,主要看自己的業(yè)務需要罷了。比如 dropwizard 操作簡單功能豐富,但只支持單值的監(jiān)控。而 simpleclient 支持多子標簽的的監(jiān)控,可以用于豐富的圖表展現(xiàn),但也需要更麻煩的操作等等。
由于我們也許更傾向于多子標簽的支持問題,今天我們就基于 simpleclient 來實現(xiàn)一個完整地網關推送的組件吧。給大家提供一些思路和一定的解決方案。
1:pom 依賴引入
如果我們想簡單化監(jiān)控以及如果需要一些tps方面的數(shù)據(jù),則可以使用 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 依賴實現(xiàn):
<!-- 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的數(shù)據(jù)暴露原因,不過這無關緊要。
2. metrics 埋點簡單使用
dropwizard 的使用樣例如下:
public class PrometheusMetricManager {// 監(jiān)控數(shù)據(jù)寫入容器private static final MetricRegistry metricsContainer = new MetricRegistry();static {// 使用 jmx_exporter 將埋點數(shù)據(jù)暴露出去JmxReporter jmxReporter = JmxReporter.forRegistry(metricsContainer).build();jmxReporter.start();}// 測試使用public static void main(String[] args) {// tps 類數(shù)據(jù)監(jiān)控Meter meter = metricsContainer.meter("tps_meter");meter.mark();Map<String, Object> queue = new HashMap<>();queue.put("sss", 1);// 監(jiān)控數(shù)組大小metricsContainer.register("custom_metric", new Gauge<Integer>() {@Overridepublic 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, counterGauge 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");// 推送網關數(shù)據(jù)pushGatewayIns.pushAdd(registry, "my_job", groupingKey);} catch (Exception e){e.printStackTrace();}}}
以上就是簡單快速使用prometheus的sdk進行埋點數(shù)據(jù)監(jiān)控了,使用都非常簡單,即注冊實例、業(yè)務埋點、暴露數(shù)據(jù);
但要做好管理埋點也許并不是很簡單,因為你可能需要做到易用性、可管理性、及性能。
3. 一個基于pushgateway 的管理metrics完整實現(xiàn)
從上節(jié),我們知道要做埋點很簡單,但要做到好的管理不簡單。比如如何做到易用?如何做到可管理性強?
解決問題會有很多方法,我這邊給到方案是,要想易用,那么我就封裝一些必要的接口給到應用層,比如應用層只做數(shù)據(jù)量統(tǒng)計,那么我就只暴露一個counter的增加方法,其他一概隱藏,應用層想要使用埋點時也不用管什么底層推送,數(shù)據(jù)結構之類,只需調用一個工廠方法即可得到操作簡單的實例。想要做可管理,那么就必須要依賴于外部的配置系統(tǒng),只需從外部配置系統(tǒng)一調整,應用立馬可以感知到,從而做出相應的改變,比如推送頻率、推送開關、網關地址。。。
下面一個完整的實現(xiàn)樣例:
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指標埋點 操作類**/@Slf4jpublic class PrometheusMetricManager {/*** prometheus 注冊實例*/private static final CollectorRegistry registry = new CollectorRegistry();/*** 指標統(tǒng)一容器** counter: 計數(shù)器類* 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;/*** 項目埋點統(tǒng)一前綴*/private static final String METRICS_PREFIX = "sys_xxx_";/*** 指標的子標簽key名, 統(tǒng)一定義*/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數(shù)據(jù)上報startPrometheusThread();}private PrometheusMetricManager() {}/*** 注冊一個prometheus的監(jiān)控指標, 并返回指標實例** @param metricName 指標名稱(只管按業(yè)務命名即可: 數(shù)字+下劃線)* @param labelNames 所要監(jiān)控的子指標名稱,會按此進行分組統(tǒng)計* @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 用戶自定義實現(xiàn)的單值提供實現(xiàn)*/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);// 主動觸發(fā)固定參數(shù)的value計數(shù)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();}/*** 使用自定義純種循環(huán)處理推送網關數(shù)據(jù)*/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是否開啟推送網關數(shù)據(jù)開關** @return true:已開啟, false:已關閉(不得推送指標數(shù)據(jù))*/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 進行推送采集指標數(shù)據(jù)*/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 {// 測試counterCounterMetric myCounter1 = PrometheusMetricManager.registerCounter("hello_counter", "topic", "type");myCounter1.incWithLabelValues("my-spec-topic", "t1");// 測試 timerTimerMetric 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);// 測試隊列大小監(jiān)控,自定義監(jiān)控實現(xiàn)PrometheusMetricManager.registerSingleValueMetric("custom_value_supplier", queue::size);// 隊列值發(fā)生變化Thread.sleep(60000);queue.put("d", 22);// 等待發(fā)送線程推送數(shù)據(jù)System.in.read();}}
以上,就是咱們整個推送網關的管理框架了,遵循前面說的原則,只暴露幾個注冊接口,返回的實例按自定義實現(xiàn),只保留必要的功能。使用時只管注冊,及使用有限功能即可。(注意:這不是寫一個通用框架,而僅是為某類業(yè)務服務的管理組件)
實際也是比較簡單的,如果按照這些原則來做的話,應該會為你的埋點監(jiān)控工作帶來些許的方便。另外,有些細節(jié)的東西我們稍后再說。
4. 自定義監(jiān)控的實現(xiàn)
上面我們看到,我們有封裝 CounterMetric, TimerMetric 以減少不必要的操作。這些主要是做了一下prometheus的一些代理工作,本身是非常簡單的,我們可以簡單看看。
/*** 功能描述: 計數(shù)器型埋點指標接口定義** <p>簡化不必要的操作方法暴露</p>**/public interface CounterMetric {/*** 計數(shù)器 +1, 作別名使用 (僅對無多余l(xiāng)abelNames 情況), 默認無需實現(xiàn)該方法*/default void inc() {incWithLabelValues();}/*** 帶子標簽類型填充的計數(shù)器 +1** @param labelValues 子標簽值(與最初設置時順序個數(shù)一致)*/void incWithLabelValues(String... labelValues);}// -------------- 以下是實現(xiàn)類 ---------------import com.my.common.util.ArraysUtil;import com.my.common.util.IPAddressUtil;import io.prometheus.client.Counter;/*** 功能描述: prometheus counter 適配器實現(xiàn)**/public class PrometheusCounterAdapter implements CounterMetric {private Counter counter;/*** 是否在頭部添加 主機名*/private boolean appendServerHost;public PrometheusCounterAdapter(Counter counter, boolean appendServerHost) {this.counter = counter;this.appendServerHost = appendServerHost;}@Overridepublic void incWithLabelValues(String... labelValues) {if(appendServerHost) {labelValues = ArraysUtil.addFirstValue(IPAddressUtil.getLocalIp(), labelValues);}counter.labels(labelValues).inc();}}
不復雜,看業(yè)務需要實現(xiàn)某些功能即可。供參考,其他類似功能可自行實現(xiàn)。
我們主要來看一下自定義監(jiān)控值的實現(xiàn),主要場景如隊列大小監(jiān)控。Prometheus 的 simpleclient 中給我們提供了幾種監(jiān)控類型 Counter, Gauge, Histogram, Summary, 可能都不能很好的支持到我們這種自定義的實現(xiàn)。所以,需要自己干下這件事。其與 Gauge 的實現(xiàn)是非常相似的,值都是可大可小可任意,所以我們可以參考Gauge的實現(xiàn)做出我們的自定義值監(jiān)控。具體實現(xiàn)如下:
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 自定義單值監(jiān)控工具(如:元素大小監(jiān)控)**/public class CustomValueMetricCollectorextends 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();}@Overrideprotected Child newChild() {return new Child(valueSupplier);}@Overridepublic List<MetricFamilySamples> describe() {return Collections.<MetricFamilySamples>singletonList(new GaugeMetricFamily(fullname, help, labelNames));}@Overridepublic 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;@Overridepublic CustomValueMetricCollector create() {return new CustomValueMetricCollector(this);}/*** 自定義值提供者** @param valueSupplier 提供者用戶實現(xiàn)實現(xiàn)* @param <T> 用戶返回的數(shù)值類型*/public <T extends Number> Builder valueSupplier(CustomMetricValueSupplier<T> valueSupplier) {this.valueSupplier = valueSupplier;return this;}}/*** 多標簽時使用的子項描述類** 實際上并不支持多標簽配置,除了一些統(tǒng)一標簽如 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, 是因為我們需要支持多子標簽的操作,所以稍微繞了一點。不過總體也不復雜。而且對于單值提供者的實現(xiàn),也只有一個 getValue 方法,這會很好地讓我們利用 Lamda 表達式,寫出極其簡單的提供者實現(xiàn)。接口定義如下:
/*** 功能描述: 單值型度量 提供者(用戶自定義實現(xiàn))** @param <T> 返回的數(shù)據(jù)類型,一定是數(shù)值型喲*/public interface CustomMetricValueSupplier<T extends Number> {/*** 用戶實現(xiàn)的提供度量值方法*/T getValue();}
具體使用時就非常簡單了:
// 測試隊列大小監(jiān)控,自定義監(jiān)控實現(xiàn)PrometheusMetricManager.registerSingleValueMetric("custom_value_supplier", queue::size);
如此,一個完整的監(jiān)控數(shù)據(jù)上報功能就完成了。你要做的僅是找到需要監(jiān)控的業(yè)務點,然后使用僅有api調用就可以了,至于后續(xù)是使用jmx上報,主動上報,網關推送。。。你都不需要關心了,而且還可以根據(jù)情況隨時做出調整。
至于后續(xù)的監(jiān)控如何做,可以參考我另一篇文章(grafana方案): 快速構建業(yè)務監(jiān)控體系,觀grafana監(jiān)控的藝術
5. 使用springmvc暴露指標數(shù)據(jù)
prometheus網關,實際上并不被官方推薦使用,因為他認為這具有侵入性。那么,如果我們能夠同時提供prometheus自主查詢的能力,那就再好不過了。
基于以上的實現(xiàn),只要稍加改造,就可以支持spring 的restful接口暴露數(shù)據(jù)了。主要分三步:1. 引入servlet依賴;2. 配置servlet服務;3. 修改注冊源;
1. 引入servlet依賴
<dependency><groupId>io.prometheus</groupId><artifactId>simpleclient_servlet</artifactId><version>0.9.0</version></dependency>
2. 配置servlet服務
springmvc中就是web.xml中配置即可:
<servlet><servlet-name>metrics</servlet-name><servlet-class>io.prometheus.client.exporter.MetricsServlet</servlet-class></servlet><servlet-mapping><servlet-name>metrics</servlet-name><url-pattern>/metrics</url-pattern></servlet-mapping>
3. 修改適配注冊源
因為網關的實現(xiàn)中,我們是自己new的一個注冊源,那么它自然不會被其他框架發(fā)現(xiàn)。所以要稍微改下,使用默認注冊源,這樣大家都方便取數(shù)據(jù)了。
public class PrometheusMetricsManager {/*** prometheus 注冊實例** 所有prometheus共享注冊實例*/private static final CollectorRegistry registry = CollectorRegistry.defaultRegistry;...}
如此,我們既支持網關的推送,又支持prometheus主動采集了。

騰訊、阿里、滴滴后臺面試題匯總總結 — (含答案)
面試:史上最全多線程面試題 !
最新阿里內推Java后端面試題
JVM難學?那是因為你沒認真看完這篇文章

關注作者微信公眾號 —《JAVA爛豬皮》
了解更多java后端架構知識以及最新面試寶典


看完本文記得給作者點贊+在看哦~~~大家的支持,是作者源源不斷出文的動力
作者:等你歸去來
出處:https://www.cnblogs.com/yougewe/p/13698833.html
