1、Prometheus簡介
Prometheus(中文名:普羅米修斯)是由SoundCloud開發的開源監控報警系統和時序列數據庫(TSDB). Prometheus使用Go語言開發, 是Google BorgMon監控系統的開源版本。
Prometheus的基本原理是通過HTTP協議週期性抓取被監控組件的狀態, 任意組件只要提供對應的HTTP接口就可以接入監控. 不需要任何SDK或者其他的集成過程。輸出被監控組件信息的HTTP接口被叫做exporter,目前開發常用的組件大部分都有exporter可以直接使用, 比如Nginx、MySQL、Linux系統信息、Mongo、ES等
企業架構圖:
2、PromQL內置函數
A、聚合函數1)、sum (求和)對樣本值求和;
比如:需要計算整個應用的HTTP請求總量,可以直接使用表達式:
sum(prometheus_http_requests_total)
2)、min (最小值)求取樣本值中的最小者;
3)、max (最大值)求取樣本值中的最大者;
4)、avg (平均值)對樣本值求平均值,這是進行指標數據分析的標準方法;
5)、stddev (標準差)對樣本值求標準差,以幫助用戶瞭解數據的波動大小(或稱之爲波動程度);
6)、stdvar (標準差異)對樣本值求方差,它是求取標準差過程中的中間狀態;
7)、count (樣本數量計數)對分組內的時間序列進行數量統計;
8)、count_values (對 value 進行計數)對分組內的時間序列的樣本值進行數量統計;
count_values用於時間序列中每一個樣本值出現的次數。count_values會爲每一個唯一的樣本值輸出一個時間序列,並且每一個時間序列包含一個額外的標籤。
例如:
count_values("count", prometheus_http_requests_total)
9)、bottomk (樣本值最小的 k 個元素)順序返回分組內的樣本值最小的前k個時間序列及其值;
10)、topk (樣本值最大的k個元素)逆序返回分組內的樣本值最大的前k個時間序列及其值;
例如:獲取HTTP請求數前5位的時序樣本數據,可以使用表達式:
topk(5, prometheus_http_requests_total)
11)、quantile (分佈統計)分位數用於評估數據的分佈狀態,該函數會返回分組內指定的分位數的值,即數值落在小於等於指定的分位區間的比例;
quantile用於計算當前樣本數據值的分佈情況quantile(φ, express)其中0 ≤ φ ≤ 1。
例如,當φ爲0.5時,即表示找到當前樣本數據中的中位數:
quantile(0.5, prometheus_http_requests_total)
B、其他函數
1)、abs(instant-vector) 求瞬時向量絕對值
2)、absent(v instant-vector) 傳入一個瞬時非空向量則返回空向量,否則返回不帶名稱值爲1的指標,用來監控空數據的情況
3)、ceil(v instant-vector) 四捨五入取整
ceil(v instant-vector) 將 v 中所有元素的樣本值向上四捨五入到最接近的整數。例如:
node_load5{instance="192.168.1.15:8090"} # 結果爲 2.79
ceil(node_load5{instance="192.168.1.15:8090"}) # 結果爲 3
4)、floor(v instant-vector) 函數與 ceil() 函數相反,捨棄小數部分取整
5)、changes(v range-vector) 計算區間向量中每個樣本值變化次數,返回的是瞬時向量
changes(v range-vector) 輸入一個區間向量, 返回這個區間向量內每個樣本數據值變化的次數(瞬時向量)。例如:
# 如果樣本數據值沒有發生變化,則返回結果爲 1
changes(node_load5{instance="192.168.1.15:8090"}[1m]) # 結果爲 1
6)、clamp_max(v instant-vector, max scalar) 瞬時向量值如果大於max 則值修改爲max,否則值不變(用於將向量規範到一個不大於max的區間內)
clamp_max(v instant-vector, max scalar) 函數,輸入一個瞬時向量和最大值,樣本數據值若大於 max,則改爲 max,否則不變。例如:
node_load5{instance="192.168.1.15:8090"} # 結果爲 2.79
clamp_max(node_load5{instance="192.168.1.15:8090"}, 2) # 結果爲 2
7)、clamp_min(v instant-vector, min scalar) 同理,將向量規範到一個不小於min值的範圍內
clamp_min(v instant-vector, min scalar) 函數,輸入一個瞬時向量和最小值,樣本數據值若小於 min,則改爲 min,否則不變。例如:
8)、day_of_month(v=vector(time()) instant-vector) 返回 1~31 向量所在UTC時間中的所在月份的第幾天
9)、day_of_week(v=vector(time()) instant-vector) 函數,返回被給定 UTC 時間所在周的第幾天。返回值範圍:0~6,0 表示星期天。
10)、days_in_month(v=vector(time()) instant-vector) 函數,返回當月一共有多少天。返回值範圍:28~31。
11)、delta(v range-vector) 的參數是一個區間向量,返回一個瞬時向量。它計算一個區間向量 v 的第一個元素和最後一個元素之間的差值
delta(v range-vector) 的參數是一個區間向量,返回一個瞬時向量。它計算一個區間向量 v 的第一個元素和最後一個元素之間的差值。
由於這個值被外推到指定的整個時間範圍,所以即使樣本值都是整數,你仍然可能會得到一個非整數值。
例如,下面的例子返回過去兩小時的 CPU 溫度差:
delta(cpu_temp_celsius{host="zeus"}[2h])
該函數一般只用在 Gauge 類型的時間序列上。
12)、idelta(v range-vector) 的參數是一個區間向量, 返回一個瞬時向量。它計算最新的 2 個樣本值之間的差值。(當區間內僅有一個向量時無返回值)
13)、deriv(v range-vector) 的參數是一個區間向量,返回一個瞬時向量。它使用簡單的線性迴歸計算區間向量 v 中各個時間序列的導數。
14)、exp(v instant-vector) 函數,輸入一個瞬時向量,返回各個樣本值的 e 的指數值,即 e 的 N 次方。當得到一個無窮大的值,顯示 +Inf, 反之顯示0, e的負數次方無限趨進0。e的空向量指數依然爲空向量。
15)、year(v=vector(time()) instant-vector) 函數返回被給定 UTC 時間的當前年份。
16、hour(v=vector(time()) instant-vector) 函數返回被給定 UTC 時間的當前第幾個小時,時間範圍:0~23。
17)、minute(v=vector(time()) instant-vector) 函數返回給定 UTC 時間當前小時的第多少分鐘。結果範圍:0~59。
18)、month(v=vector(time()) instant-vector) 函數返回給定 UTC 時間當前屬於第幾個月,結果範圍:0~12。
19)、increase(v range-vector) 函數獲取區間向量中的第一個和最後一個樣本並返回其增長量, 它會在單調性發生變化時(如由於採樣目標重啓引起的計數器復位)自動中斷。
increase(v range-vector) 函數獲取區間向量中的第一個和最後一個樣本並返回其增長量,它會在單調性發生變化時(如由於採樣目標重啓引起的計數器復位)自動中斷。
由於這個值被外推到指定的整個時間範圍,所以即使樣本值都是整數,你仍然可能會得到一個非整數值,如果除以一定時間就可以獲取該時間內的平均增長率。
例如,以下表達式返回區間向量中每個時間序列過去 5 分鐘內 HTTP 請求數的增長數:
increase(http_requests_total{job="apiserver"}[5m])
該函數配合counter數據類型使用,它的返回值類型只能是計數器類型。
20)、rate(v range-vector) 函數用於計算區間向量平均增長率,採用區間向量第一個值和最後值進行計算
rate(v range-vector) 函數可以直接計算區間向量 v 在時間窗口內平均每秒增長速率,它會在單調性發生變化時(如由於採樣目標重啓引起的計數器復位)自動中斷。
該函數配合counter數據類型使用,它的返回值類型只能用計數器,在長期趨勢分析或者告警中推薦使用這個函數。該函數的返回結果不帶有度量指標,只有標籤列表。
例如,以下表達式返回區間向量中每個時間序列過去 5 分鐘內 HTTP 請求數的每秒增長率:
rate(http_requests_total[5m])
結果:
{code="200",handler="label_values",instance="120.77.65.193:9090",job="prometheus",method="get"} 0
{code="200",handler="query_range",instance="120.77.65.193:9090",job="prometheus",method="get"} 0
{code="200",handler="prometheus",instance="120.77.65.193:9090",job="prometheus",method="get"} 0.2
...
[info] 注意
當將 rate() 函數與聚合運算符(例如 sum())或隨時間聚合的函數(任何以 _over_time 結尾的函數)一起使用時,必須先執行 rate 函數,然後再進行聚合操作,
否則當採樣目標重新啓動時 rate() 無法檢測到計數器是否被重置。
# 統計平均時間
rate(http_server_requests_seconds_sum{uri="/recommend"}[1m])/rate(http_server_requests_seconds_count{uri="/recommend"}[1m])
21)、irate(v range-vector) 函數用於計算區間向量的增長率,但是其反應出的是瞬時增長率。irate 函數是通過區間向量中最後兩個兩本數據來計算區間向量的增長速率,它會在單調性發生變化時(如由於採樣目標重啓引起的計數器復位)自動中斷。這種方式可以避免在時間窗口範圍內的“長尾問題”,並且體現出更好的靈敏度,通過irate函數繪製的圖標能夠更好的反應樣本數據的瞬時變化狀態。
irate(v range-vector) 函數用於計算區間向量的增長率,但是其反應出的是瞬時增長率。
irate 函數是通過區間向量中最後兩個兩本數據來計算區間向量的增長速率,它會在單調性發生變化時(如由於採樣目標重啓引起的計數器復位)自動中斷。
這種方式可以避免在時間窗口範圍內的“長尾問題”,並且體現出更好的靈敏度,通過irate函數繪製的圖標能夠更好的反應樣本數據的瞬時變化狀態。
例如,以下表達式返回區間向量中每個時間序列過去 5 分鐘內最後兩個樣本數據的 HTTP 請求數的增長率:
irate(http_requests_total{job="api-server"}[5m])
irate 只能用於繪製快速變化的計數器,在長期趨勢分析或者告警中更推薦使用 rate 函數。因爲使用 irate 函數時,速率的簡短變化會重置 FOR 語句,形成的圖形有很多波峯,難以閱讀。
[info] 注意
當將 irate() 函數與聚合運算符(例如 sum())或隨時間聚合的函數(任何以 _over_time 結尾的函數)一起使用時,必須先執行 irate 函數,然後再進行聚合操作,
否則當採樣目標重新啓動時 irate() 無法檢測到計數器是否被重置。
# 統計訪問 /recommend 的 qps
irate(http_server_requests_seconds_count{uri='/recommend'}[1m])
22)、label_join(v instant-vector, dst_label string, separator string, src_label_1 string, src_label_2 string, ...) 函數可以將時間序列 v 中多個標籤 src_label 的值,通過 separator 作爲連接符寫入到一個新的標籤 dst_label 中。可以有多個 src_label 標籤。
存在 up{name="a", job="b"} 1, up{name="a", job="b"} 0 兩個向量進行 label_join(up{name="a", job="b"}, "full_name", "-", "name", "job") 將會得到一個帶有新label的向量
up{name="a", job="b", full_name="a-b"} 1
23)、label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string) 在原標籤中匹配字符,放到目標標籤中
24)、ln(v instant-vector) 計算瞬時向量 v 中所有樣本數據的自然對數。
ln(0) = -lnf 負無窮
ln(lnf) = lnf
ln(負數) = Nan
ln(Nan) = Nan
25)、log2(v instant-vector) 函數計算瞬時向量 v 中所有樣本數據的二進制對數。特殊情況同ln。
26)、log10(v instant-vector) 計算瞬時向量 v 中所有樣本數據的十進制對數。特殊情況同ln。
27)、predict_linear(v range-vector, t scalar) 函數可以預測時間序列 v 在 t 秒後的值。它基於簡單線性迴歸的方式,對時間窗口內的樣本數據進行統計,從而可以對時間序列的變化趨勢做出預測。
28)、resets(v range-vector) 的參數是一個區間向量。對於每個時間序列,它都返回一個計數器重置的次數。兩個連續樣本之間單調性發生變化被認爲是一次計數器重置。
29)、round(v instant-vector, to_nearest=1 scalar) 函數與 ceil 和 floor 函數類似,返回向量中所有樣本值的最接近to_nearest值整數倍的值,當to_nearest=1時等價於ceil。
30)、scalar(v instant-vector) 函數返回一個單元素瞬時向量的樣本值,當多元素或者沒有元素返回Nan。
31)、vector(s scalar) 函數將標量 s 作爲沒有標籤的向量返回(和scalar作用剛好相反)。
32)、sort(v instant-vector) 函數對向量按元素的值進行升序排序。
33)、sort_desc(v instant-vector) 函數對向量按元素的值進行降序排序。
34)、sqrt(v instant-vector) 函數計算向量 v 中所有元素的平方根。
35)、timestamp(v instant-vector) 函數返回向量 v 中的每個樣本的時間戳(從 1970-01-01 到現在的秒數)。
36)、avg_over_time(range-vector) : 區間向量內每個度量指標的平均值。
37)、min_over_time(range-vector) : 區間向量內每個度量指標的最小值。
38)、max_over_time(range-vector) : 區間向量內每個度量指標的最大值。
39)、sum_over_time(range-vector) : 區間向量內每個度量指標的求和。
40)、count_over_time(range-vector) : 區間向量內每個度量指標的樣本數據個數。
3、指標類型
Prometheus定義了4中不同的指標類型(metric type):Counter(計數器)、Gauge(儀表盤)、Histogram(直方圖)、Summary(摘要)。
比如在NodeExporter返回的樣本數據中,其註釋中也包含了該樣本的類型。例如:
# HELP node_cpu_seconds_total Seconds the cpus spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="0",mode="idle"} 927490.95
node_cpu_seconds_total{cpu="0",mode="iowait"} 27.74...
1)、Counter
統計的數據是遞增的,不能使用計數器來統計可能減小的指標,計數器統計的指標是累計增加的,如http請求的總數,出現的錯誤總數,總的處理時間,api請求數等
第一次抓取 http_response_total{method="GET",endpoint="/api/tracks"} 100
第二次抓取 http_response_total{method="GET",endpoint="/api/tracks"} 150
由於Counter類型的監控指標其特點是隻增不減,在沒有發生重置(如服務器重啓,應用重啓)的情況下其樣本值應該是不斷增大的。爲了能夠更直觀的表示樣本數據的變化劇烈情況,需要計算樣本的增長速率。
載如下圖所示,樣本增長率反映出了樣本變化的劇烈程度:
可以通過以下表達式計算Counter類型指標的增長率:
#下面兩句話取出來的結果是一樣的,只不過rate()是直接算出區間向量的平均速率,而increase()你要自己去除於秒數。
increase(node_cpu_seconds_total[2m]) / 120
rate(node_cpu_seconds_total[2m])
increase函數:這裏通過node_cpu_seconds_total[2m]獲取時間序列最近兩分鐘的所有樣本,increase計算出最近兩分鐘的增長量,
最後除以時間120秒得到node_cpu_seconds_total樣本在最近兩分鐘的平均每秒增長率。並且這個值也近似於主機節點最近兩分鐘內的平均CPU使用率。
rate()函數:該函數配合counter類型數據使用,取counter在這個時間段中的平均每秒增量。比方CPU在2分鐘增長了200,那麼結果就是200/120。
2)、Gauge
量規是一種度量標準,代表可以任意上下波動的單個數值,用於統計cpu使用率,內存使用率,磁盤使用率,溫度等指標,還可以統計上升和下降的計數。如併發請求數等。
例如:
第1次抓取 memory_usage_bytes{host="master-01"} 100
第2秒抓取 memory_usage_bytes{host="master-01"} 30
第3次抓取 memory_usage_bytes{host="master-01"} 50
第4次抓取 memory_usage_bytes{host="master-01"} 80
3)、Histogram
統計在一定的時間範圍內數據的分佈情況。如請求的持續/延長時間,請求的響應大小等,還提供度量指標的總和,數據以直方圖顯示。Histogram由_bucket{le=""},_bucket{le="+Inf"}, _sum,_count 組成
如:
apiserver_request_latencies_sum
apiserver_request_latencies_count
apiserver_request_latencies_bucket
histogram_quantile(φ float, b instant-vector) 從 bucket 類型的向量 b 中計算 φ (0 ≤ φ ≤ 1) 分位數(百分位數的一般形式)的樣本的最大值。(有關 φ 分位數的詳細說明以及直方圖指標類型的使用,請參閱直方圖和摘要)。向量 b 中的樣本是每個 bucket 的採樣點數量。每個樣本的 labels 中必須要有 le 這個 label 來表示每個 bucket 的上邊界,沒有 le 標籤的樣本會被忽略。直方圖指標類型自動提供帶有 _bucket 後綴和相應標籤的時間序列。
可以使用 rate() 函數來指定分位數計算的時間窗口。
例如,一個直方圖指標名稱爲 employee_age_bucket_bucket,要計算過去 10 分鐘內 第 90 個百分位數,請使用以下表達式:
histogram_quantile(0.9, rate(employee_age_bucket_bucket[10m]))
返回:
{instance="10.0.86.71:8080",job="prometheus"} 35.714285714285715
這表示最近 10 分鐘之內 90% 的樣本的最大值爲 35.714285714285715。
這個計算結果是每組標籤組合成一個時間序列。我們可能不會對所有這些維度(如 job、instance 和 method)感興趣,並希望將其中的一些維度進行聚合,則可以使用 sum() 函數。例如,以下表達式根據 job 標籤來對第 90 個百分位數進行聚合:
## 計算TP95
histogram_quantile(0.95, http_server_requests_seconds_bucket{uri='/recommend'})
4)、Summary
和Histogram直方圖類似,主要用於表示一段時間內數據採樣結果(通常是請求持續時間或響應大小之類的東西),還可以計算度量值的總和和度量值的分位數以及在一定時間範圍內的分位數,由{quantile="<φ>"},_sum,_count 組成
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-spring-legacy</artifactId> <version>1.3.15</version> </dependency>
配置訪問地址
#endpoints.prometheus.path=${spring.application.name}/prometheus
management.metrics.tags.application = ${spring.application.name}
2)、Springboot2.X
maven 依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>
配置訪問地址
#Prometheus springboot監控配置
management:
endpoints:
web:
exposure:
include: 'prometheus' # 暴露/actuator/prometheus
metrics:
tags:
application: ${spring.application.name} # 暴露的數據中添加application label
指標自定義開發:
import com.slightech.marvin.api.visitor.app.metrics.VisitorMetrics; import com.slightech.marvin.api.visitor.app.service.VisitorStatisticsService; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author wanglu */ @Configuration public class MeterConfig { @Value("${spring.application.name}") private String applicationName; @Autowired private VisitorStatisticsService visitorStatisticsService; @Bean public MeterRegistryCustomizer<MeterRegistry> configurer() { return (registry) -> registry.config().commonTags("application", applicationName); } @Bean public VisitorMetrics visitorMetrics() { return new VisitorMetrics(visitorStatisticsService); } }
import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; /** * @author wanglu * @date 2019/11/08 */ public class VisitorMetrics implements MeterBinder { private static final String METRICS_NAME_SPACE = "visitor"; private VisitorStatisticsService visitorStatisticsService; public VisitorMetrics(VisitorStatisticsService visitorStatisticsService) { this.visitorStatisticsService = visitorStatisticsService; } @Override public void bindTo(MeterRegistry meterRegistry) { //公寓獲取二維碼次數-當天 //visitor_today_apartment_get_qr_code_count{"application"="xxx", "option"="apartment get qr code"} Gauge.builder(METRICS_NAME_SPACE + "_today_apartment_get_qr_code_count", visitorStatisticsService, VisitorStatisticsService::getTodayApartmentGetQrCodeCount) .description("today apartment get qr code count") .tag("option", "apartment get qr code") .register(meterRegistry); //visitor_today_apartment_get_qr_code_count{"application"="xxx", "option"="employee get qr code"} //員工獲取二維碼次數-當天 Gauge.builder(METRICS_NAME_SPACE + "_today_employee_get_qr_code_count", visitorStatisticsService, VisitorStatisticsService::getTodayEmployeeGetQrCodeCount) .description("today employee get qr code count") .tag("option", "employee get qr code") .register(meterRegistry); } }
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.Calendar; import java.util.Date; import java.util.concurrent.TimeUnit; /** * 數據統計 * * @author laijianzhen * @date 2019/11/08 */ @Service public class VisitorStatisticsService { /** * 公寓獲取二維碼次數-當天 */ private static final String COUNT_TODAY_APARTMENT_GET_QR_CODE_REDIS_KEY = "Visitor:Statistics:CountTodayApartmentGetQrCode"; /** * 員工獲取二維碼次數-當天 */ private static final String COUNT_TODAY_EMPLOYEE_GET_QR_CODE_REDIS_KEY = "Visitor:Statistics:CountTodayEmployeeGetQrCode"; @Autowired private RedisTemplate<String, Object> redisTemplate; public int getTodayApartmentGetQrCodeCount() { return getCountFromRedis(COUNT_TODAY_APARTMENT_GET_QR_CODE_REDIS_KEY); } public int getTodayEmployeeGetQrCodeCount() { return getCountFromRedis(COUNT_TODAY_EMPLOYEE_GET_QR_CODE_REDIS_KEY); } @Async public void countTodayApartmentGetQrCode() { increaseCount(COUNT_TODAY_APARTMENT_GET_QR_CODE_REDIS_KEY); } @Async public void countTodayEmployeeGetQrCode() { increaseCount(COUNT_TODAY_EMPLOYEE_GET_QR_CODE_REDIS_KEY); } private int getCountFromRedis(String key) { Object object = redisTemplate.opsForValue().get(key); if (object == null) { return 0; } return Integer.parseInt(String.valueOf(object)); } private void increaseCount(String redisKey) { Object object = redisTemplate.opsForValue().get(redisKey); if (object == null) { redisTemplate.opsForValue().set(redisKey, String.valueOf(1), getTodayLeftSeconds(), TimeUnit.SECONDS); return; } redisTemplate.opsForValue().increment(redisKey, 1); } private long getTodayLeftSeconds() { Date nowDate = new Date(); Calendar midnight = Calendar.getInstance(); midnight.setTime(nowDate); midnight.add(Calendar.DAY_OF_MONTH, 1); midnight.set(Calendar.HOUR_OF_DAY, 0); midnight.set(Calendar.MINUTE, 0); midnight.set(Calendar.SECOND, 0); midnight.set(Calendar.MILLISECOND, 0); return (midnight.getTime().getTime() - nowDate.getTime()) / 1000; } }