天天看點

自定義Metrics:讓Prometheus監控你的應用程式

前言

Prometheus社群提供了大量的官方以及第三方Exporters,可以滿足Prometheus的采納者快速實作對關鍵業務,以及基礎設施的監控需求。

如上所示,一個簡單的應用以及環境架構。一般而言,我們通常會從幾個層面進行監控名額的采集:

  • 入口網關:這裡可以是Nginx/HaProxy這一類的負載均衡器,也可以是注入Spring Cloud Zuul這一類架構提供的微服務入口。一般來說我們需要對所有Http Request相關的名額資料進行采集。如請求位址,Http Method,傳回狀态碼,響應時長等。進而可以通過這些名額曆史資料去分析業務壓力,服務狀态等資訊。
  • 應用服務:對于應用服務而言,基本的如應用本身的資源使用率,比如如果是Java類程式可以直接通過JVM資訊來進行統計,如果是部署到容器中,則可以通過Container的資源使用情況來統計。除了資源用量外,某些特殊情況下,我們可能還會對應用中的某些業務名額進行采集。
  • 基礎設施:虛拟機或者實體機的資源使用情況等。
  • 其它:叢集環境中所使用到的資料庫,緩存,消息隊列等中間件狀态等。

對于以上的集中場景中,除了直接使用Prometheus社群提供的Exporter外,不同的項目可能還需要實作一些自定義的Exporter用于實作對于特定目的的名額的采集和監控需求。

本文将以Spring Boot/Spring Cloud為例,介紹如果使用Prometheus SDK實作自定義監控名額的定義以及暴露,并且會介紹Prometheus中四種不同名額類型(Counter, Gauge, Histogram, Summary)的實際使用場景;

擴充Spring應用程式,支援Prometheus采集

添加Prometheus Java Client依賴

> 這裡使用0.0.24的版本,在之前的版本中Spring Boot暴露的監控位址,無法正确的處理Prometheus Server的請求,詳情:https://github.com/prometheus/ ... s/265

build.gradle

dependencies {     ...     compile 'io.prometheus:simpleclient:0.0.24'     compile "io.prometheus:simpleclient_spring_boot:0.0.24"     compile "io.prometheus:simpleclient_hotspot:0.0.24"     }      

啟用Prometheus Metrics Endpoint

添加注解@EnablePrometheusEndpoint啟用Prometheus Endpoint,這裡同時使用了simpleclient_hotspot中提供的DefaultExporter該Exporter會在metrics endpoint中放回目前應用JVM的相關資訊

@SpringBootApplication     @EnablePrometheusEndpoint     public class SpringApplication implements CommandLineRunner {     public static void main(String[] args) {         SpringApplication.run(GatewayApplication.class, args);     }     @Override     public void run(String... strings) throws Exception {         DefaultExports.initialize();     }     }      

預設情況下Prometheus暴露的metrics endpoint為 /prometheus,可以通過endpoint配置進行修改

endpoints:     prometheus:     id: metrics     metrics:     id: springmetrics     sensitive: false     enabled: true      

  

啟動應用程式通路 http://localhost:8080/metrics 可以看到以下輸出:

#HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds.     TYPE jvm_gc_collection_seconds summary     jvm_gc_collection_seconds_count{gc="PS Scavenge",} 11.0     jvm_gc_collection_seconds_sum{gc="PS Scavenge",} 0.18     jvm_gc_collection_seconds_count{gc="PS MarkSweep",} 2.0     jvm_gc_collection_seconds_sum{gc="PS MarkSweep",} 0.121     #HELP jvm_classes_loaded The number of classes that are currently loaded in the JVM     TYPE jvm_classes_loaded gauge     jvm_classes_loaded 8376.0     #HELP jvm_classes_loaded_total The total number of classes that have been loaded since the JVM has started execution     TYPE jvm_classes_loaded_total counter     ...      

添加攔截器,為監控埋點做準備

除了擷取應用JVM相關的狀态以外,我們還可能需要添加一些自定義的監控Metrics實作對系統性能,以及業務狀态進行采集,以提供日後優化的相關支撐資料。首先我們使用攔截器處理對應用的所有請求。

繼承WebMvcConfigurerAdapter類,複寫addInterceptors方法,對所有請求/**添加攔截器

@SpringBootApplication     @EnablePrometheusEndpoint     public class SpringApplication extends WebMvcConfigurerAdapter implements CommandLineRunner {     @Override     public void addInterceptors(InterceptorRegistry registry) {         registry.addInterceptor(new PrometheusMetricsInterceptor()).addPathPatterns("/**");     }     }      

PrometheusMetricsInterceptor內建HandlerInterceptorAdapter,通過複寫父方法,實作對請求處理前/處理完成的處理。

public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {     @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         return super.preHandle(request, response, handler);     }     @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         super.afterCompletion(request, response, handler, ex);     }     }      

自定義Metrics名額

Prometheus提供了4中不同的Metrics類型:Counter,Gauge,Histogram,Summary

1)Counter:隻增不減的計數器

計數器可以用于記錄隻會增加不會減少的名額類型,比如記錄應用請求的總量(http_requests_total),cpu使用時間(process_cpu_seconds_total)等。

對于Counter類型的名額,隻包含一個inc()方法,用于計數器+1

一般而言,Counter類型的metrics名額在命名中我們使用_total結束。

public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {     static final Counter requestCounter = Counter.build()             .name("io_namespace_http_requests_total").labelNames("path", "method", "code")             .help("Total requests.").register();     @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         String requestURI = request.getRequestURI();         String method = request.getMethod();         int status = response.getStatus();         requestCounter.labels(requestURI, method, String.valueOf(status)).inc();         super.afterCompletion(request, response, handler, ex);     }     }      

使用Counter.build()建立Counter metrics,name()方法,用于指定該名額的名稱 labelNames()方法,用于聲明該metrics擁有的次元label。在addInterceptors方法中,我們擷取目前請求的,RequesPath,Method以及狀态碼。并且調用inc()方法,在每次請求發生時計數+1。

Counter.build()...register(),會像Collector中注冊該名額,并且當通路/metrics位址時,傳回該名額的狀态。

通過名額io_namespace_http_requests_total我們可以:

  • 查詢應用的請求總量
sum(io_namespace_http_requests_total)      
  • 查詢每秒Http請求量
sum(rate(io_wise2c_gateway_requests_total[5m]))      
  • 查詢目前應用請求量Top N的URI
topk(10, sum(io_namespace_http_requests_total) by (path))      

2)Gauge: 可增可減的儀表盤

對于這類可增可減的名額,可以用于反應應用的__目前狀态__,例如在監控主機時,主機目前空閑的記憶體大小(node_memory_MemFree),可用記憶體大小(node_memory_MemAvailable)。或者容器目前的cpu使用率,記憶體使用率。

對于Gauge名額的對象則包含兩個主要的方法inc()以及dec(),使用者添加或者減少計數。在這裡我們使用Gauge記錄目前正在處理的Http請求數量。

public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {     ...省略的代碼     static final Gauge inprogressRequests = Gauge.build()             .name("io_namespace_http_inprogress_requests").labelNames("path", "method", "code")             .help("Inprogress requests.").register();     @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         ...省略的代碼         // 計數器+1         inprogressRequests.labels(requestURI, method, String.valueOf(status)).inc();         return super.preHandle(request, response, handler);     }     @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         ...省略的代碼         // 計數器-1         inprogressRequests.labels(requestURI, method, String.valueOf(status)).dec();         super.afterCompletion(request, response, handler, ex);     }     }      

通過名額io_namespace_http_inprogress_requests我們可以直接查詢應用目前正在進行中的Http請求數量:

io_namespace_http_inprogress_requests{}      

 3)Histogram:自帶buckets區間用于統計分布統計圖

主要用于在指定分布範圍内(Buckets)記錄大小(如http request bytes)或者事件發生的次數。

以請求響應時間requests_latency_seconds為例,假如我們需要記錄http請求響應時間符合在分布範圍{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10}中的次數時。

public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {     static final Histogram requestLatencyHistogram = Histogram.build().labelNames("path", "method", "code")             .name("io_namespace_http_requests_latency_seconds_histogram").help("Request latency in seconds.")             .register();     private Histogram.Timer histogramRequestTimer;     @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         ...省略的代碼         histogramRequestTimer = requestLatencyHistogram.labels(requestURI, method, String.valueOf(status)).startTimer();         ...省略的代碼     }     @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         ...省略的代碼         histogramRequestTimer.observeDuration();         ...省略的代碼     }     }      

使用Histogram構造器可以建立Histogram監控名額。預設的buckets範圍為{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10}。如何需要覆寫預設的buckets,可以使用.buckets(double... buckets)覆寫。

Histogram會自動建立3個名額,分别為:

  • 事件發生總次數: basename_count

 實際含義: 目前一共發生了2次http請求

io_namespace_http_requests_latency_seconds_histogram_count{path="/",method="GET",code="200",} 2.0      

 所有事件産生值的大小的總和: basename_sum

 實際含義: 發生的2次http請求總的響應時間為13.107670803000001 秒

io_namespace_http_requests_latency_seconds_histogram_sum{path="/",method="GET",code="200",} 13.107670803000001      
  • 事件産生的值分布在bucket中的次數: basename_bucket{le="上包含"}

在總共2次請求當中,http請求響應時間 <=0.005 秒 的請求次數為0

io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.005",} 0.0      

在總共2次請求當中,http請求響應時間 <=0.01 秒 的請求次數為0

io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.01",} 0.0      

在總共2次請求當中,http請求響應時間 <=0.025 秒 的請求次數為0

io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.025",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.05",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.075",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.1",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.25",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.5",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.75",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="1.0",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="2.5",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="5.0",} 0.0     io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="7.5",} 2.0      

在總共2次請求當中,http請求響應時間 <=10 秒 的請求次數為0

io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="10.0",} 2.0      

在總共2次請求當中,ttp請求響應時間 10 秒 的請求次數為0

io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="+Inf",} 2.0      

Summary: 用戶端定義的資料分布統計圖

Summary和Histogram非常類型相似,都可以統計事件發生的次數或者大小,以及其分布情況。

Summary和Histogram都提供了對于事件的計數_count以及值的彙總_sum。 是以使用_count,和_sum時間序列可以計算出相同的内容,例如http每秒的平均響應時間:rate(basename_sum[5m]) / rate(basename_count[5m])。

同時Summary和Histogram都可以計算和統計樣本的分布情況,比如中位數,9分位數等等。其中 0.0<= 分位數Quantiles <= 1.0。

不同在于Histogram可以通過histogram_quantile函數在伺服器端計算分位數。 而Sumamry的分位數則是直接在用戶端進行定義。是以對于分位數的計算。 Summary在通過PromQL進行查詢時有更好的性能表現,而Histogram則會消耗更多的資源。相對的對于用戶端而言Histogram消耗的資源更少。

public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {     static final Summary requestLatency = Summary.build()             .name("io_namespace_http_requests_latency_seconds_summary")             .quantile(0.5, 0.05)             .quantile(0.9, 0.01)             .labelNames("path", "method", "code")             .help("Request latency in seconds.").register();     @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         ...省略的代碼         requestTimer = requestLatency.labels(requestURI, method, String.valueOf(status)).startTimer();         ...省略的代碼     }     @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         ...省略的代碼         requestTimer.observeDuration();         ...省略的代碼     }     }      

使用Summary名額,會自動建立多個時間序列:

  • 事件發生總的次數

含義:目前http請求發生總次數為12次

io_namespace_http_requests_latency_seconds_summary_count{path="/",method="GET",code="200",} 12.0      
  • 事件産生的值的總和

含義:這12次http請求的總響應時間為 51.029495508s

io_namespace_http_requests_latency_seconds_summary_sum{path="/",method="GET",code="200",} 51.029495508      
  • 事件産生的值的分布情況

含義:這12次http請求響應時間的中位數是3.052404983s

io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.5",} 3.052404983      

含義:這12次http請求響應時間的9分位數是8.003261666s

io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.9",} 8.003261666      

使用Collector暴露業務名額

除了在攔截器中使用Prometheus提供的Counter,Summary,Gauage等構造監控名額以外,我們還可以通過自定義的Collector實作對相關業務名額的暴露

@SpringBootApplication     @EnablePrometheusEndpoint     public class SpringApplication extends WebMvcConfigurerAdapter implements CommandLineRunner {     @Autowired     private CustomExporter customExporter;     ...省略的代碼     @Override     public void run(String... args) throws Exception {         ...省略的代碼         customExporter.register();     }     }      
CustomExporter內建自io.prometheus.client.Collector,在調用Collector的register()方法後,當通路/metrics時,則會自動從Collector的collection()方法中擷取采集到的監控名額。      

由于這裡CustomExporter存在于Spring的IOC容器當中,這裡可以直接通路業務代碼,傳回需要的業務相關的名額。

import io.prometheus.client.Collector;     import io.prometheus.client.GaugeMetricFamily;     import org.springframework.stereotype.Component;     import java.util.ArrayList;     import java.util.Collections;     import java.util.List;     @Component     public class CustomExporter extends Collector {     @Override     public List<MetricFamilySamples> collect() {         List<MetricFamilySamples> mfs = new ArrayList<>();         # 建立metrics名額         GaugeMetricFamily labeledGauge =                 new GaugeMetricFamily("io_namespace_custom_metrics", "custom metrics", Collections.singletonList("labelname"));         # 設定名額的label以及value         labeledGauge.addMetric(Collections.singletonList("labelvalue"), 1);         mfs.add(labeledGauge);         return mfs;     }     }      

當然這裡也可以使用CounterMetricFamily,SummaryMetricFamily聲明其它的名額類型。

小結

好了。 目前為止,啟動應用程式,并且通路 http://localhost:8080/metrics。我們可以看到如下結果。

這部分分别介紹了兩種方式,在Spring應用中實作對于自定義Metrics名額的定義:

  • 攔截器/過濾器:用于統計所有應用請求的情況
  • 自定義Collector: 可以用于統計應用業務能力相關的監控情況

同時介紹了4中Metrics名額類型以及使用場景:

    • Counter,隻增不減的計數器
    • Gauge,可增可減的儀表盤
    • Histogram,自帶buckets區間用于統計分布統計圖
    • Summary, 用戶端定義的資料分布統計圖

繼續閱讀