Exporter是基於Prometheus實施的監控系統中重要的組成部分,承擔資料指標的採集工作,官方的exporter列表中已經包含了常見的絕大多數的系統指標監控,比如用於機器效能監控的node_exporter, 用於網路裝置監控的snmp_exporter等等。這些已有的exporter對於監控來說,僅僅需要很少的配置工作就能提供完善的資料指標採集。
有時我們需要自己去寫一些與商務邏輯比較相關的指標監控,這些指標無法通過常見的exporter擷取到。比如我們需要提供對於DNS解析情況的整體監控,瞭解如何編寫exporter對於業務監控很重要,也是完善監控系統需要經曆的一個階段。接下來我們就介紹如何編寫exporter, 本篇內容編寫的語言為golang, 官方也提供了python, java等其他的語言實現的庫,採集方式其實大同小異。 搭建環境
首先確保機器上安裝了go語言(1.7版本以上),並設定好了對應的GOPATH。接下來我們就可以開始編寫代碼了。以下是一個簡單的exporter
下載對應的prometheus包
go get github.com/prometheus/client_golang/prometheus/promhttp
程式主函數:
package mainimport ( "log" "net/http" "github.com/prometheus/client_golang/prometheus/promhttp")func main() { http.Handle("/metrics", promhttp.Handler()) log.Fatal(http.ListenAndServe(":8080", nil))}
這個代碼中我們僅僅通過http模組指定了一個路徑,並將client_golang庫中的promhttp.Handler()作為處理函數傳遞進去後,就可以擷取指標資訊了,兩行代碼實現了一個exporter。這裡內部其實是使用了一個預設的收集器將通過NewGoCollector採集當前Go運行時的相關資訊比如go堆棧使用,goroutine的資料等等。 通過訪問http://localhost:8080/metrics即可查看詳細的指標參數。
上面的代碼僅僅展示了一個預設的採集器,並且通過介面調用隱藏了太多實施細節,對於下一步開發並沒什麼作用,為了實現自訂的監控我們需要先瞭解一些基本概念。 指標類別
Prometheus中主要使用的四類指標類型,如下所示
- Counter (累加指標)
- Gauge (測量指標)
- Summary (概略圖)
- Histogram (長條圖)
Counter 一個累加指標資料,這個值隨著時間只會逐漸的增加,比如程式完成的總任務數量,運行錯誤發生的總次數。常見的還有交換器中snmp採集的資料流量也屬於該類型,代表了持續增加的資料包或者傳輸位元組累加值。
Gauge代表了採集的一個單一資料,這個資料可以增加也可以減少,比如CPU使用方式,記憶體使用量量,硬碟當前的空間容量等等
Histogram和Summary使用的頻率較少,兩種都是基於採樣的方式。另外有一些庫對於這兩個指標的使用和支援程度不同,有些僅僅實現了部分功能。這兩個類型對於某一些業務需求可能比較常見,比如查詢單位時間內:總的回應時間低於300ms的佔比,或者查詢95%使用者查詢的門限值對應的回應時間是多少。 使用Histogram和Summary指標的時候同時會產生多組資料,_count代表了採樣的總數,_sum則代表採樣值的和。 _bucket則代表了落入此範圍的資料。
下面是使用historam來定義的一組指標,計算出了平均五分鐘內的查詢請求小於0.3s的請求佔比總量的比例值。
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (job)/ sum(rate(http_request_duration_seconds_count[5m])) by (job)
如果需要彙總資料,可以使用histogram. 並且如果對於分布範圍有明確的值的情況下(比如300ms),也可以使用histogram。但是如果僅僅是一個百分比的值(比如上面的95%),則使用Summary 定義指標
這裡我們需要引入另一個依賴庫
go get github.com/prometheus/client_golang/prometheus
下面先來定義了兩個指標資料,一個是Guage類型, 一個是Counter類型。分別代表了CPU溫度和磁碟失敗次數統計,使用上面的定義進行分類。
cpuTemp = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "cpu_temperature_celsius", Help: "Current temperature of the CPU.", }) hdFailures = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "hd_errors_total", Help: "Number of hard-disk errors.", }, []string{"device"}, )
這裡還可以註冊其他的參數,比如上面的磁碟失敗次數統計上,我們可以同時傳遞一個device裝置名稱進去,這樣我們採集的時候就可以獲得多個不同的指標。每個指標對應了一個裝置的磁碟失敗次數統計。 註冊指標
func init() { // Metrics have to be registered to be exposed: prometheus.MustRegister(cpuTemp) prometheus.MustRegister(hdFailures)}
使用prometheus.MustRegister是將資料直接註冊到Default Registry,就像上面的啟動並執行例子一樣,這個Default Registry不需要額外的任何代碼就可以將指標傳遞出去。註冊後既可以在程式層面上去使用該指標了,這裡我們使用之前定義的指標提供的API(Set和With().Inc)去改變指標的資料內容
func main() { cpuTemp.Set(65.3) hdFailures.With(prometheus.Labels{"device":"/dev/sda"}).Inc() // The Handler function provides a default handler to expose metrics // via an HTTP server. "/metrics" is the usual endpoint for that. http.Handle("/metrics", promhttp.Handler()) log.Fatal(http.ListenAndServe(":8080", nil))}
其中With函數是傳遞到之前定義的label=”device”上的值,也就是產生指標類似於
cpu_temperature_celsius 65.3hd_errors_total{"device"="/dev/sda"} 1
當然我們寫在main函數中的方式是有問題的,這樣這個指標僅僅改變了一次,不會隨著我們下次採集資料的時候發生任何變化,我們希望的是每次執行採集的時候,程式都去自動的抓取指標並將資料通過http的方式傳遞給我們。 Counter資料擷取執行個體
下面是一個採集Counter類型資料的執行個體,這個例子中實現了一個自訂的,滿足採集器(Collector)介面的結構體,並手動註冊該結構體後,使其每次查詢的時候自動執行採集任務。
我們先來看下採集器Collector介面的實現
type Collector interface { // 用於傳遞所有可能的指標的定義描述符 // 可以在程式運行期間添加新的描述,收集新的指標資訊 // 重複的描述符將被忽略。兩個不同的Collector不要設定相同的描述符 Describe(chan<- *Desc) // Prometheus的註冊器調用Collect執行實際的抓取參數的工作, // 並將收集的資料傳遞到Channel中返回 // 收集的指標資訊來自於Describe中傳遞,可以並發的執行抓取工作,但是必須要保證線程的安全。 Collect(chan<- Metric)}
瞭解了介面的實現後,我們就可以寫自己的實現了,先定義結構體,這是一個叢集的指標採集器,每個叢集都有自己的Zone,代表叢集的名稱。另外兩個是儲存的採集的指標。
type ClusterManager struct { Zone string OOMCountDesc *prometheus.Desc RAMUsageDesc *prometheus.Desc}
我們來實現一個採集工作,放到了ReallyExpensiveAssessmentOfTheSystemState函數中實現,每次執行的時候,返回一個按照主機名稱作為鍵採集到的資料,兩個傳回值分別代表了OOM錯誤計數,和RAM使用指標資訊。
func (c *ClusterManager) ReallyExpensiveAssessmentOfTheSystemState() ( oomCountByHost map[string]int, ramUsageByHost map[string]float64,) { oomCountByHost = map[string]int{ "foo.example.org": int(rand.Int31n(1000)), "bar.example.org": int(rand.Int31n(1000)), } ramUsageByHost = map[string]float64{ "foo.example.org": rand.Float64() * 100, "bar.example.org": rand.Float64() * 100, } return}
實現Describe介面,傳遞指標描述符到channel
// Describe simply sends the two Descs in the struct to the channel.func (c *ClusterManager) Describe(ch chan<- *prometheus.Desc) { ch <- c.OOMCountDesc ch <- c.RAMUsageDesc}
Collect函數將執行抓取函數並返回資料,返回的資料傳遞到channel中,並且傳遞的同時綁定原先的指標描述符。以及指標的類型(一個Counter和一個Guage)
func (c *ClusterManager) Collect(ch chan<- prometheus.Metric) { oomCountByHost, ramUsageByHost := c.ReallyExpensiveAssessmentOfTheSystemState() for host, oomCount := range oomCountByHost { ch <- prometheus.MustNewConstMetric( c.OOMCountDesc, prometheus.CounterValue, float64(oomCount), host, ) } for host, ramUsage := range ramUsageByHost { ch <- prometheus.MustNewConstMetric( c.RAMUsageDesc, prometheus.GaugeValue, ramUsage, host, ) }}
建立結構體及對應的指標資訊,NewDesc參數第一個為指標的名稱,第二個為協助資訊,顯示在指標的上面作為注釋,第三個是定義的label名稱數組,第四個是定義的Labels
func NewClusterManager(zone string) *ClusterManager { return &ClusterManager{ Zone: zone, OOMCountDesc: prometheus.NewDesc( "clustermanager_oom_crashes_total", "Number of OOM crashes.", []string{"host"}, prometheus.Labels{"zone": zone}, ), RAMUsageDesc: prometheus.NewDesc( "clustermanager_ram_usage_bytes", "RAM usage as reported to the cluster manager.", []string{"host"}, prometheus.Labels{"zone": zone}, ), }}
執行主程式
func main() { workerDB := NewClusterManager("db") workerCA := NewClusterManager("ca") // Since we are dealing with custom Collector implementations, it might // be a good idea to try it out with a pedantic registry. reg := prometheus.NewPedanticRegistry() reg.MustRegister(workerDB) reg.MustRegister(workerCA)}
如果直接執行上面的參數的話,不會擷取任何的參數,因為程式將自動推出,我們並未定義http介面去暴露資料出來,因此資料在執行的時候還需要定義一個httphandler來處理http請求。
添加下面的代碼到main函數後面,即可實現資料傳遞到http介面上:
gatherers := prometheus.Gatherers{ prometheus.DefaultGatherer, reg, } h := promhttp.HandlerFor(gatherers, promhttp.HandlerOpts{ ErrorLog: log.NewErrorLogger(), ErrorHandling: promhttp.ContinueOnError, }) http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) }) log.Infoln("Start server at :8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Errorf("Error occur when start server %v", err) os.Exit(1) }
其中prometheus.Gatherers用來定義一個採集資料的收集器集合,可以merge多個不同的採集資料到一個結果集合,這裡我們傳遞了預設的DefaultGatherer,所以他在輸出中也會包含go運行時指標資訊。同時包含reg是我們之前產生的一個註冊對象,用來自訂採集資料。
promhttp.HandlerFor()函數傳遞之前的Gatherers對象,並返回一個httpHandler對象,這個httpHandler對象可以調用其自身的ServHTTP函數來接手http請求,並返迴響應。其中promhttp.HandlerOpts定義了採集過程中如果發生錯誤時,繼續採集其他的資料。
嘗試重新整理幾次瀏覽器擷取最新的指標資訊
clustermanager_oom_crashes_total{host="bar.example.org",zone="ca"} 364clustermanager_oom_crashes_total{host="bar.example.org",zone="db"} 90clustermanager_oom_crashes_total{host="foo.example.org",zone="ca"} 844clustermanager_oom_crashes_total{host="foo.example.org",zone="db"} 801# HELP clustermanager_ram_usage_bytes RAM usage as reported to the cluster manager.# TYPE clustermanager_ram_usage_bytes gaugeclustermanager_ram_usage_bytes{host="bar.example.org",zone="ca"} 10.738111282075208clustermanager_ram_usage_bytes{host="bar.example.org",zone="db"} 19.003276633920805clustermanager_ram_usage_bytes{host="foo.example.org",zone="ca"} 79.72085409108028clustermanager_ram_usage_bytes{host="foo.example.org",zone="db"} 13.041384617379178
每次重新整理的時候,我們都會獲得不同的資料,類似於實現了一個數值不斷改變的採集器。當然,具體的指標和採集函數還需要按照需求進行修改,滿足實際的業務需求。