前言
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, 用戶端定義的資料分布統計圖