360 基於 Prometheus的在線服務監控實踐
轉自:https://mp.weixin.qq.com/s/lcjZzjptxrUBN1999k_rXw
主題簡介:
-
Prometheus基礎介紹
-
Prometheus打點及查詢技巧
-
Prometheus高可用和服務發現經驗
初衷
最近參與的幾個項目,無一例外對監控都有極強的要求,需要對項目中各組件進行詳細監控,如服務端API的請求次數、響應時間、到達率、接口錯誤率、分布式存儲中的集群IOPS、節點在線情況、偏移量等。
比較常見的方式是寫日誌,將日誌采集到遠端進行分析和繪圖,或寫好本地監控腳本進行數據采集後,通過監控系統客戶端push到監控系統中進行打點。基本上我們需要的都能覆蓋,但仍然有一些問題在使用上不太舒服,如在大規模請求下日誌采集和分析的效率比較難控制,或push打點的粒度和緯度以及查詢不夠靈活等。
後來在同事對《Google SRE》這本書中的一些運維思想進行一番安利後,抱著試一試的態度,開始嘗試使用Prometheus做為幾個項目的監控解決方案。
Prometheus的特點
-
多維數據模型(時序數據由 metric 名和一組K/V標簽構成)。
-
靈活強大的查詢語句(PromQL)。
-
不依賴存儲,支持local和remote(OpenTSDB、InfluxDB等)不同模型。
-
采用 HTTP協議,使用Pull模式采集數據。
-
監控目標,可以采用服務發現或靜態配置的方式。
-
支持多種統計數據模型,圖形化友好(Grafana)。
數據類型Counter
Counter表示收集的數據是按照某個趨勢(增加/減少)一直變化的。
Gauge
Gauge表示搜集的數據是瞬時的,可以任意變高變低。
Histogram
Histogram可以理解為直方圖,主要用於表示一段時間範圍內對數據進行采樣,(通常是請求持續時間或響應大小),並能夠對其指定區間以及總數進行統計。
Summary
Summary和Histogram十分相似,主要用於表示一段時間範圍內對數據進行采樣,(通常是請求持續時間或響應大小),它直接存儲了 quantile 數據,而不是根據統計區間計算出來的。
在我們的使用場景中,大部分監控使用Counter來記錄,例如接口請求次數、消息隊列數量、重試操作次數等。比較推薦多使用Counter類型采集,因為Counter類型不會在兩次采集間隔中間丟失信息。
一小部分使用Gauge,如在線人數、協議流量、包大小等。Gauge模式比較適合記錄無規律變化的數據,而且兩次采集之間可能會丟失某些數值變化的情況。隨著時間周期的粒度變大,丟失關鍵變化的情況也會增多。
還有一小部分使用Histogram和Summary,用於統計平均延遲、請求延遲占比和分布率。另外針對Historgram,不論是打點還是查詢對服務器的CPU消耗比較高,通過查詢時查詢結果的返回耗時會有十分直觀的感受。
時序數據-打點-查詢
我們知道每條時序數據都是由 metric(指標名稱),一個或一組label(標簽),以及float64的值組成的。
標準格式為 <metric name>{<label name>=<label value>,...}
例如:
rpc_invoke_cnt_c{code="0",method="Session.GenToken",job="Center"} 5
rpc_invoke_cnt_c{code="0",method="Relation.GetUserInfo",job="Center"} 12
rpc_invoke_cnt_c{code="0",method="Message.SendGroupMsg",job="Center"} 12
rpc_invoke_cnt_c{code="4",method="Message.SendGroupMsg",job="Center"} 3
rpc_invoke_cnt_c{code="0",method="Tracker.Tracker.Get",job="Center"} 70
這是一組用於統計RPC接口處理次數的監控數據。
其中rpc_invoke_cnt_c為指標名稱,每條監控數據包含三個標簽:code 表示錯誤碼,service表示該指標所屬的服務,method表示該指標所屬的方法,最後的數字代表監控值。
針對這個例子,我們共有四個維度(一個指標名稱、三個標簽),這樣我們便可以利用Prometheus強大的查詢語言PromQL進行極為復雜的查詢。
PromQL
PromQL(Prometheus Query Language) 是 Prometheus 自己開發的數據查詢 DSL 語言,語言表現力非常豐富,支持條件查詢、操作符,並且內建了大量內置函,供我們針對監控數據的各種維度進行查詢。
我們想統計Center組件Router.Logout的頻率,可使用如下Query語句:
rate(rpc_invoke_cnt_c{method="Relation.GetUserInfo",job="Center"}[1m])
或者基於方法和錯誤碼統計Center的整體RPC請求錯誤頻率:
sum by (method, code)(rate(rpc_invoke_cnt_c{job="Center",code!="0"}[1m]))
如果我們想統計Center各方法的接口耗時,使用如下Query語句即可:
rate(rpc_invoke_time_h_sum{job="Center"}[1m]) / rate(rpc_invoke_time_h_count{job="Center"}[1m])
更多的內建函數這裏不展開介紹了。函數使用方法和介紹可以詳細參見官方文檔中的介紹:https://prometheus.io/docs/querying/functions/
另外,配合查詢,在打點時metric和labal名稱的定義也有一定技巧。
比如在我們的項目中:
-
rpc_invoke_cnt_c 表示rpc調用統計
-
api_req_num_cv 表示httpapi調用統計
-
msg_queue_cnt_c 表示隊列長度統計
盡可能使用各服務或者組件通用的名稱定義metric然後通過各種lable進行區分。
最開始我們的命名方式是這樣的,比如我們有三個組件center、gateway、message。RPC調用統計的metric相應的命名成了三個:
-
center_rpc_invoke_cnt_c
-
gateway_rpc_invoke_cnt_c
-
message_rpc_invoke_cnt_c
這種命名方式,對於各組件的開發同學可能讀起來會比較直觀,但是在實際查詢過程中,這三個metric相當於三個不同的監控項。
例如我們查詢基於method統計所有組件RPC請求錯誤頻率,如果我們使用通用名稱定義metric名,查詢語句是這樣的:
sum by (method, code) (rate(rpc_invoke_cnt_c{code!="0"}[1m]))
但如果我們各個組件各自定義名稱的話,這條查詢需要寫多條。雖然我們可以通過 {__name__=~".*rpc_invoke_cnt_c"} 的方式來規避這個問題,但在實際使用和操作時體驗會差很多。
例如在Grafana中,如果合理命名相對通用的metric名稱,同樣一個Dashboard可以套用給多個相同業務,只需簡單修改template匹配一下label選擇即可。不然針對各個業務不同的metric進行針對性的定制繪圖也是一個十分痛苦的過程。
同時通過前面的各類查詢例子也會發現,我們在使用label時也針對不同的含義進行了區分如 method=GroupJoin|GetUserInfo|PreSignGet|... 來區分調用的函數方法,code=0|1|4|1004|...來區分接口返回值,使查詢的分類和結果展示更加方便直觀,並且label在Grafana中是可以直接作為變量進行更復雜的模版組合。
更多的metric和label相關的技巧可以參考官方文檔:
https://prometheus.io/docs/practices/naming/
服務發現
在使用初期,參與的幾個項目的Prometheus都是各自獨立部署和維護的。其配置也是按照官方文檔中的標準配置來操作。機器數量少的時候維護簡單,增刪機器之後簡單reload一下即可。例如:
但隨著服務器量級增長,業務整合到同一組Prometheus時,每次上下線實例都是一個十分痛苦的過程,配置文件龐大,列表過長,修改的過程極其容易眼花導致誤操作。所以我們嘗試使用了Prometheus的服務發現功能。
從配置文檔中不難發現Prometheus對服務發現進行了大量的支持,例如大家喜聞樂見的Consul、etcd和K8S。
<scrape_config>
<tls_config>
<azure_sd_config>
<Consul_sd_config>
<dns_sd_config>
<ec2_sd_config>
<openstack_sd_config>
<file_sd_config>
<gce_sd_config>
<kubernetes_sd_config>
<marathon_sd_config>
<nerve_sd_config>
<serverset_sd_config>
<triton_sd_config>
由於最近參與的幾個項目深度使用公司內部的配置管理服務gokeeper,雖然不是Prometheus原生支持,但是通過簡單適配也是同樣能滿足服務發現的需求。我們最終選擇通過file_sd_config進行服務發現的配置。
file_sd_config 接受json格式的配置文件進行服務發現。每次json文件的內容發生變更,Prometheus會自動刷新target列表,不需要手動觸發reload操作。所以針對我們的gokeeper編寫了一個小工具,定時到gokeeper中采集服務分類及分類中的服務器列表,並按照file_sd_config的要求生成對應的json格式。
下面是一個測試服務生成的json文件樣例。
[
{
"targets": [
"10.10.10.1:65160",
"10.10.10.2:65160"
],
"labels": {
"job":"Center",
"service":"qtest"
}
},
{
"targets": [
"10.10.10.3:65110",
"10.10.10.4:65110"
],
"labels": {
"job":"Gateway",
"service":"qtest"
}
}
]
Prometheus配置文件中將file_sd_configs的路徑指向json文件即可。
-job_name: ‘qtest‘
scrape_interval: 5s
file_sd_configs:
- files: [‘/usr/local/prometheus/qtestgroups/*.json‘]
如果用etcd作為服務發現組件也可以使用此種方式,結合confd配合模版和file_sd_configs可以極大地減少配置維護的復雜度。只需要關註一下Prometheus後臺采集任務的分組和在線情況是否符合期望即可。社區比較推崇Consul作為服務發現組件,也有非常直接的內部配置支持。
感興趣的話可以直接參考官方文檔進行配置和測試-https://prometheus.io/docs/operating/configuration/#<Consul_sd_config>
高可用
高可用目前暫時沒有太好的方案。官方給出的方案可以對數據做Shard,然後通過federation來實現高可用方案,但是邊緣節點和Global節點依然是單點,需要自行決定是否每一層都要使用雙節點重復采集進行保活。
使用方法比較簡單,例如我們一個機房有三個Prometheus節點用於Shard,我們希望Global節點采集歸檔數據用於繪圖。首先需要在Shard節點進行一些配置。
Prometheus.yml:
global:
external_labels:
slave: 0 #給每一個節點指定一個編號 三臺分別標記為0,1,2
rule_files:
- node_rules/zep.test.rules #指定rulefile的路徑
scrape_configs:
- job_name: myjob
file_sd_configs:
- files: [‘/usr/local/prometheus/qtestgroups/*.json‘]
relabel_configs:
- source_labels: [__address__]
modulus: 3 # 3節點
target_label: __tmp_hash
action: hashmod
- source_labels: [__tmp_hash]
regex: ^0$ # 表示第一個節點
action: keep
編輯規則文件:
node_rules/zep.test.rules:
job:rpc_invoke_cnt:rate:1m=rate(rpc_invoke_cnt_c{code!="0"}[1m])
在這裏job:rpc_invoke_cnt:rate:1m 將作為metric名,用來存放查詢語句的結果。
在Global節點Prometheus.yml也需要進行修改。
-job_name: slaves
honor_labels: true
scrape_interval: 5s
metrics_path: /federate
params:
match[]:
- ‘{__name__=~"job:.*"}‘
static_configs:
- targets:
- 10.10.10.150:9090
- 10.10.10.151:9090
- 10.10.10.152:9090
在這裏我們只采集了執行規則後的數據用於繪圖,不建議將Shard節點的所有數據采集過來存儲再進行查詢和報警的操作。這樣不但會使Shard節點計算和查詢的壓力增大(通過HTTP讀取原始數據會造成大量IO和網絡開銷),同時所有數據寫入Global節點也會使其很快達到單Prometheus節點的承載能力上限。
另外部分敏感報警盡量不要通過global節點觸發,畢竟從Shard節點到Global節點傳輸鏈路的穩定性會影響數據到達的效率,進而導致報警實效降低。例如服務updown狀態,API請求異常這類報警我們都放在s hard節點進行報警。
此外我們還編寫了一個實驗性質的Prometheus Proxy工具,代替Global節點接收查詢請求,然後將查詢語句拆解,到各shard節點抓取基礎數據,然後再在Proxy這裏進行Prometheus內建的函數和聚合操作,最後將計算數據拋給查詢客戶端。這樣便可以直接節約掉Global節點和大量存儲資源,並且Proxy節點由於不需要存儲數據,僅接受請求和計算數據,橫向擴展十分方便。
當然問題還是有的,由於每次查詢Proxy到shard節點拉取的都是未經計算的原始數據,當查詢的metric數據量比較大時,網絡和磁盤IO開銷巨大。因此在繪圖時我們對查詢語句限制比較嚴格,基本不允許進行無label限制的模糊查詢。
報警
Prometheus的報警功能目前來看相對計較簡單。主要是利用Alertmanager這個組件。已經實現了報警組分類,按標簽內容發送不同報警組、報警合並、報警靜音等基礎功能。配合rules_file中編輯的查詢觸發條件,Prometheus會主動通知Alertmanager然後發出報警。由於我們公司內使用的自研的Qalarm報警系統,接口比較豐富,和Alertmanager的webhook簡單對接即可使用。
Alertmanager也內建了一部分報警方式,如Email和第三方的Slack,初期我們的存儲集群報警使用的就是Slack,響應速度還是很不錯的。
需要註意的是,如果報警已經觸發,但是由於一些原因,比如刪除業務監控節點,使報警恢復的規則一直不能觸發,那麽已出發的報警會按照Alertmanager配置的周期一直重復發送,要麽從後臺silence掉,要麽想辦法使報警恢復。例如前段時間我們縮容Ceph集群,操作前沒有關閉報警,觸發了幾個osddown的報警,報警刷新周期2小時,那麽每過兩小時Alertmanager都會發來一組osddown的報警短信。
對應編號的osd由於已經刪掉已經不能再寫入up對應的監控值,索性停掉osddown報警項,直接重啟ceph_exporter,再調用Prometheus API刪掉對應osd編號的osdupdown監控項,隨後在啟用osddown報警項才使報警恢復。
如下圖的報警詳情頁面,紅色的是已觸發的報警,綠色的是未觸發報警:
繪圖展示
對於頁面展示,我們使用的是Grafana,如下面兩張圖,是兩個不同服務的Dashboard,可以做非常多的定制化,同時Grafana的template也可以作為參數傳到查詢語句中,對多維度定制查詢提供了極大的便利。
Q&A
Q1:Promethues Alertmanager,能結合案例來一個麽?
A1:直接演示一條報警規則吧。
ALERT SlowRequest
IF ceph_slow_requests{service="ceph"} > 10
FOR 1m
LABELS { qalarm = "true" }
ANNOTATIONS {
summary = "Ceph Slow Requests",
description = "slow requests count: {{ $value }} - Region:{{ $labels.group }}",
}
這條規則在查詢到ceph slow_request > 10並且超過1分鐘時觸發一條報警。
Q2:exporter的編寫及使用方法,以及 promethues 如何結合 grafana使用和promethues 是如何進行報警的。
A2:exporter的編寫可以單獨拿出來寫一篇文章了。我們這邊主要使用的Prometheus Golang SDK,在項目中註冊打點,並通過Http接口暴露出來。報警沒有結合Grafana,不過大多數Grafana中使用的查詢語句,簡單修改即可成為Alertmanager的報警規則。
Q3:刪除配置文件job,但是通過查詢還有job記錄,怎麽刪除job記錄?
A3:直接通過Prometheus接口處理即可
curl -X "DELETE" "http://prometheus:9090/api/v1/series?match[]={job="your job"}"
和查詢接口的使用方式類似
360 基於 Prometheus的在線服務監控實踐