您好,登錄后才能下訂單哦!
Prometheus是SoundCloud公司開源的監控系統,同時也是繼Kubernetes之后,第二個加入CNCF的項目。Prometheus是一個優秀的監控系統,沃趣圍繞著Prometheus先后開發了多個組件,包括基礎告警組件,服務發現組件、各種采集的Exporters等,這些組件結合Prometheus支撐了沃趣大部分的監控業務。本文主要介紹Prometheus,從他的來源,架構以及一個具體的例子等方面來說明,以及沃趣圍繞Prometheus做了哪些工作。
SoundCloud公司的之前的應用架構是巨石架構,也就是所有的功能放在一個大的模塊里,各個功能之間沒有明顯的界線。巨石架構的應用主要存在兩方面的問題,一方面在于很難對其進行水平擴展,只能垂直擴展,但是單臺機器的能力畢竟是有限的;另外一方面在于各個功能耦合在一塊,新增一個功能需要在已有的技術棧上進行開發,并且要確保不會對已有的功能造成影響。于是他們轉向了微服務架構,將原有的功能拆分成了幾百個獨立的服務,整個系統運行上千個實例。遷移到微服務架構給監控帶來一定的挑戰,現在不僅需要知道某個組件的運行的情況,還要知道服務的整體運行情況。他們當時的監控方案是:StatsD + Graphite + Nagios,StatsD結合Graphite構建監控圖表,各個服務將樣本數據推送給StatsD,StatsD將推送來的樣本數據聚合在一起,定時地推送給Graphite,Graphite將樣本數據保存在時序數據庫中,用戶根據Graphite提供的API,結合自身監控的需求,構建監控圖表,通過圖表分析服務的指標(例如,延遲,每秒的請求數,每秒的錯誤數等)。
那么這樣一種方案能滿足微服務架構對監控的要求么?什么要求呢:既能知道服務整體的運行情況,也能夠保持足夠的粒度,知道某個組件的運行情況。答案是很難,為什么呢?例如,我們要統計api-server服務響應POST /tracks請求錯誤的數量,指標的名稱為api-server.tracks.post.500,這個指標可以通過http狀態碼來測量,服務響應的狀態碼為500就是錯誤的。Graphite指標名稱的結構是一種層次結構,api-server指定服務的名稱,tracks指定服務的handler,post指定請求的方法,500指定請求響應的狀態碼,api-server服務實例將該指標推送給StatsD,StatsD聚合各個實例推送來的指標,然后定時推送給Graphite。查詢api-server.tracks.post.500指標,我們能獲得服務錯誤的響應數,但是,如果我們的api-server服務跑了多個實例,想知道某個實例錯誤的響應數,該怎么查詢呢?問題出在使用這樣一種架構,往往會將各個服務實例發送來的指標聚合到一塊,聚合到一起之后,實例維度的信息就丟失掉了,也就無法統計某個具體實例的指標信息。
StatsD與Graphite的組合用來構建監控圖表,告警是另外一個系統-Nagios-來做的,這個系統運行檢測腳本,判斷主機或服務運行的是否正常,如果不正常,發送告警。Nagios最大的問題在于告警是面向主機的,每個告警的檢查項都是圍繞著主機的,在分布式系統的環境底下,主機down掉是正常的場景,服務本身的設計也是可以容忍節點down掉的,但是,這種場景下Nagios依然會觸發告警。
如果大家之前看過這篇
https://landing.google.com/sre ... arker
介紹Google Borgmon的文章,對比Prometheus,你會發現這兩個系統非常相似。實際上,Prometheus深受Borgmon系統的影響,并且當時參與構建Google監控系統的員工加入了SoundCloud公司。總之,種種因素的結合,促使了Prometheus系統的誕生。
那么,Prometheus是如何解決上面這些問題的?之前的方案中,告警與圖表的構建依賴于兩個不同的系統,Prometheus采取了一種新的模型,將采集時序數據作為整個系統的核心,無論是告警還是構建監控圖表,都是通過操縱時序數據來實現的。Prometheus通過指標的名稱以及label(key/value)的組合來識別時序數據,每個label代表一個維度,可以增加或者減少label來控制所選擇的時序數據,前面提到,微服務架構底下對監控的要求:既能知道服務整體的運行情況,也能夠保持足夠的粒度,知道某個組件的運行情況。借助于這種多維度的數據模型可以很輕松的實現這個目標,還是拿之前那個統計http錯誤響應的例子來說明,我們這里假設api_server服務有三個運行的實例,Prometheus采集到如下格式的樣本數據(其中intance label是Prometheus自動添加上去的):
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample1"} -> 34 api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample2"} -> 28 api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample3"} -> 31
如果我們只關心特定實例的錯誤數,只需添加instance label即可,例如我們想要查看實例名稱為sample1的錯誤的請求數,那么我就可以用api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample1"}這個表達式來選擇時序數據,選擇的數據如下:
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample1"} -> 34
如果我們關心整個服務的錯誤數,只需忽略instance label去除,然后將結果聚合到一塊,即可,例如
sum without(instance) (api_server_http_requests_total{method="POST",handler="/tracks",status="500"})計算得到的時序數據為:
api_server_http_requests_total{method="POST",handler="/tracks",status="500"} -> 93
告警是通過操縱時序數據而不是運行一個自定義的腳本來實現的,因此,只要能夠采集到服務或主機暴露出的指標數據,那么就可以告警。
我們再來簡單的分析一下Prometheus的架構,看一下各個組件的功能,以及這些組件之間是如何交互的。
Prometheus Server是整個系統的核心,它定時地從監控目標(Exporters)暴露的API中拉取指標,然后將這些數據保存到時序數據庫中,如果是監控目標是動態的,可以借助服務發現的機制動態地添加這些監控目標,另外它還會暴露執行PromQL(用來操縱時序數據的語言)的API,其他組件,例如Prometheus Web,Grafana可以通過這個API查詢對應的時序數據。Prometheus Server會定時地執行告警規則,告警規則是PromQL表達式,表達式的值是true或false,如果是true,就將產生的告警數據推送給alertmanger。告警通知的聚合、分組、發送、禁用、恢復等功能,并不是Prometheus Server來做的,而是Alertmanager來做的,Prometheus Server只是將觸發的告警數據推送給Alertmanager,然后Alertmanger根據配置將告警聚合到一塊,發送給對應的接收人。
如果我們想要監控定時任務,想要instrument任務的執行時間,任務執行成功還是失敗,那么如何將這些指標暴露給Prometheus Server?例如每隔一天做一次數據庫備份,我們想要知道每次備份執行了多長時間,備份是否成功,我們備份任務只會執行一段時間,如果備份任務結束了,Prometheus Server該如何拉取備份指標的數據呢?解決這種問題,可以通過Prometheus的pushgateway組件來做,每個備份任務將指標推送pushgateway組件,pushgateway將推送來的指標緩存起來,Prometheus Server從Pushgateway中拉取指標。
前面都是從比較大的層面——背景、架構——來介紹Prometheus,現在,讓我們從一個具體的例子出發,來看一下如何借助Prometheus來構建監控圖表、分析系統性能以及告警。
我們有個服務,暴露出四個API,每個API只返回一些簡單的文本數據,現在,我們要對這個服務進行監控,希望借助監控能夠查看、分析服務的請求速率,請求的平均延遲以及請求的延遲分布,并且當應用的延遲過高或者不可訪問時能夠觸發告警,代碼示例如下:
package main import ( "math/rand" "net/http" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( Latency = promauto.NewHistogramVec(prometheus.HistogramOpts{ Help: "latency of sample app", Name: "sample_app_latency_milliseconds", Buckets: prometheus.ExponentialBuckets(10, 2, 9), }, []string{"handler", "method"}) ) func instrumentationFilter(f http.HandlerFunc) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { now := time.Now() f(writer, request) duration := time.Now().Sub(now) Latency.With(prometheus.Labels{"handler": request.URL.Path, "method": request.Method}). Observe(float64(duration.Nanoseconds()) / 1e6) } } // jitterLatencyFilter make request latency between d and d*maxFactor func jitterLatencyFilter(d time.Duration, maxFactor float64, f http.HandlerFunc) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { time.Sleep(d + time.Duration(rand.Float64()*maxFactor*float64(d))) f(writer, request) } } func main() { rand.Seed(time.Now().UnixNano()) http.Handle("/metrics", promhttp.Handler()) http.Handle("/a", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 256, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("success")) }))) http.Handle("/b", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 128, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("success")) }))) http.Handle("/c", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 64, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("success")) }))) http.Handle("/d", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 32, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("success")) }))) http.ListenAndServe(":5001", nil) }
我們按照instrumentation、exposition、collection、query這樣的流程構建監控系統,instrumentation關注的是如何測量應用的指標,有哪些指標需要測量;exposition關注的是如何通過http協議將指標暴露出來;collection關注的是如何采集指標;query關注的是如何構建查詢時序數據的PromQL表達式。我們首先從instrumentation這里,有四個指標是我們關心的:
var ( Latency = promauto.NewHistogramVec(prometheus.HistogramOpts{ Help: "latency of sample app", Name: "sample_app_latency_milliseconds", Buckets: prometheus.ExponentialBuckets(10, 2, 9), }, []string{"handler", "method"}) )
首先將指標注冊進來,然后追蹤、記錄指標的值。用Prometheus提供的golang客戶端庫可以方便的追蹤、記錄指標的值,我們將instrumentation code放到應用的代碼里,每次請求,對應的指標狀態的值就會被記錄下來。
client golang提供了四種指標類型,分別為Counter, Gauge, Histogram, Summary,Counter類型的指標用來測量只會增加的值,例如服務的請求數;Gauge類型的指標用來測量狀態值,即可以變大,也可以變小的值,例如請求的延遲時間;Histogram與Summary指標類似,這兩個指標取樣觀察的值,記錄值的分布,統計觀察值的數量,累計觀察到的值,可以用它來統計樣本數據的分布。為了采集請求速率、平均延遲以及延遲分布指標,方便起見用Histogram類型的指標追蹤、記錄每次請求的情況,Histogram類型的指標與普通類型(Counter、Gauge)不同的地方在于會生成多條樣本數據,一個是觀察樣本的總數,一個是觀察樣本值的累加值,另外是一系列的記錄樣本百分位數的樣本數據。訪問狀態可以使用up指標來表示,每次采集時,Prometheus會將采集的健康狀態記錄到up指標中。
http.Handle("/metrics", promhttp.Handler())
instrumentation完成之后,下一步要做的就是exposition,只需將Prometheus http handler添加進來,指標就可以暴露出來。訪問這個Handler返回的樣本數據如下(省略了一些無關的樣本數據):
sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="10"} 0 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="20"} 0 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="40"} 0 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="80"} 0 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="160"} 0 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="320"} 0 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="640"} 1 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="1280"} 1 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="2560"} 1 sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="+Inf"} 1 sample_app_latency_milliseconds_sum{handler="/d",method="GET"} 326.308075 sample_app_latency_milliseconds_count{handler="/d",method="GET"} 1
僅僅將指標暴露出來,并不能讓prometheus server來采集指標,我們需要進行第三步collection,配置prometheus server發現我們的服務,從而采集服務暴露出的樣本數據。我們簡單地看下prometheus server的配置,其中,global指定采集時全局配置,
scrape_interval
指定采集的間隔,
evaluation_interval
指定
alerting rule
(alerting rule是PromQL表達式,值為布爾類型,如果為true就將相關的告警通知推送給Alertmanager)也就是告警規則的求值時間間隔,scrape_timeout指定采集時的超時時間;alerting指定Alertmanager服務的地址;scrape_configs指定如何發現監控對象,其中job_name指定發現的服務屬于哪一類,static_configs指定服務靜態的地址,前面我們也提到,Prometheus支持動態服務發現,例如文件、kubernetes服務發現機制,這里我們使用最簡單的靜態服務發現機制。
# my global config global: scrape_interval: 2s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). rule_files: - rule.yaml # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: - localhost:9093 scrape_configs: - job_name: sample-app scrape_interval: 3s static_configs: - targets: - sample:5001
采集完指標,就可以利用Prometheus提供的PromQL語言來操縱采集的時序數據,例如,我們想統計請求的平均速率,可以用這個表達式
irate(sample_app_latency_milliseconds_sum[1m]) / irate(sample_app_latency_milliseconds_count[1m])來計算。
有了時序數據之后,就可以借助Grafana來構建監控圖表,具體怎么配置Grafana圖表在這里就不展開了,核心點是利用PromQL表達式選擇、計算時序數據。
Prometheus的告警是通過對Alerting Rule求值來實現的,alerting rule是一系列的PromQL表達式,alerting rule保存在配置文件中。我們想要對應用的延遲以及可用狀態進行告警,當應用過高或者不可訪問時就觸發告警,規則可以如下這樣定義:
- name: sample-up rules: - alert: UP expr: up{instance="sample:5001"} == 0 for: 1m labels: severity: page annotations: summary: Service health - alert: 95th-latency expr: histogram_quantile(0.95, rate(sample_app_latency_milliseconds_bucket[1m])) > 1000 for: 1m labels: severity: page annotations: summary: 95th service latency
其中UP指定服務的可用狀態,95th-latency指定95%的請求大于1000毫秒就觸發告警。Prometheus定時的對這些規則進行求值,如果條件滿足,就將告警通知發送給Alertmanger,Alertmanger會根據自身路由配置,對告警進行聚合,分發到指定的接收人,我們想通過郵箱接收到告警,可以如下進行配置:
global: smtp_smarthost: <your_smtp_server> smtp_auth_username: <your_username> smtp_from: <from> smtp_auth_password: <secret> smtp_require_tls: false resolve_timeout: 5m route: receiver: me receivers: - name: me email_configs: - to: example@domain.com templates: - '*.tmpl'
這樣,我們就可以通過郵箱收到告警郵件了。
無論是監控圖表相關的業務,還是告警相關的業務,都離不開相關指標的采集工作,沃趣是一家做數據庫產品的公司,我們花費了很多的精力去采集數據庫相關的指標,從Oracle到MySQL,再到SQL Server,主流的關系型數據庫的指標都有采集。對于一些通用的指標,例如操作系統相關的指標,我們主要是借助開源的Exporters來采集的。沃趣的產品是軟、硬一體交付的,其中有大量硬件相關的指標需要采集,因此,我們也有專門采集硬件指標的Expoters。
沃趣大部分場景中,要監控的服務都是動態的。比如,用戶從平臺上申請了一個數據庫,需要增加相關的監控服務,用戶刪除數據庫資源,需要移除相關的監控服務,要監控的數據庫服務處于動態的變化之中。沃趣每個產品線的基礎架構都不相同,數據庫服務有跑在Oracle RAC上的,有跑在ZStack的,有跑在Kubernetes上的。對于跑在Kubernetes上的應用來說,并需要擔心Prometheus怎么發現要監控的服務,只需要配置相關的服務發現的機制就可以了。對于其他類型的,我們主要借助Prometheus的file_sd服務發現機制來實現,基于文件的服務發現機制是一種最通用的機制,我們將要監控的對象寫到一個文件中,Prometheus監聽這個文件的變動,動態的維護要監控的對象,我們在file_sd基礎上構建了專門的組件去負責服務的動態更新,其他應用調用這個組件暴露的API來維護自身想要監控的對象。
Prometheus本身的機制的并不能滿足我們業務上對告警的要求,一方面我們需要對告警通知進行統計,但是Alertmanager本身并沒有對告警通知做持久化,服務重啟之后告警通知就丟失掉了;另外一方面用戶通過Web頁面來配置相關的告警,告警規則以及告警通知的路由需要根據用戶的配置動態的生成。為了解決這兩方面的問題,我們將相關的業務功能做成基礎的告警組件,供各個產品線去使用。針對Alertmanager不能持久化告警通知的問題,基礎告警組件利用Alertmanager webhook的機制來接收告警通知,然后將通知保存到數據庫中;另外用戶的告警配置需要動態的生成,我們定義了一種新的模型來描述我們業務上的告警模型。
Promtheus將采集時序數據作為整個系統的核心,無論是構建監控圖表還是告警,都是通過操縱時序數據來完成的。Prometheus借助多維度的數據模型,以及強大的查詢語言滿足了微服務架構底下對監控的要求:既能知道服務整體的運行情況,也能夠保持足夠的粒度,知道某個組件的運行情況。沃趣站在巨人的肩旁上,圍繞Prometheus構建了自己的監控系統,從滿足不同采集要求的Exporters到服務發現,最后到基礎告警組件,這些組件結合Prometheus,構成了沃趣監控系統的核心。
作者:郭振,沃趣科技開發工程師,多年的Python、Golang等語言的開發經驗,熟悉Kubernetes、Prometheus等云原生應用,負責QFusion RDS平臺以及基礎告警平臺的研發工作。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。