開發(fā)一個接口監(jiān)控的Prometheus Exporter
想必大家對于黑盒監(jiān)控都不陌生,我們經(jīng)常使用blackbox_exporter來進行黑盒監(jiān)控,在K8s中進行黑盒監(jiān)控可以參考這里。
既然已經(jīng)有成熟的工具,為何自己還要再來嘗試開發(fā)一個?
我說是為了學(xué)習(xí),你信嗎?
既然是為了學(xué)習(xí),整體邏輯就不用太復(fù)雜,主要需要實現(xiàn)以下功能:
- 可以通過配置文件的方式增加監(jiān)控項
- 吐出Prometheus可收集指標(biāo)
- 支持tcp和http探測
- 支持配置檢測頻率
寫在前面
在正式開始之前,先簡單介紹一下Prometheus以及Prometheus Exporter。
Prometheus是CNCF的一個開源監(jiān)控工具,是近幾年非常受歡迎的開源項目之一。在云原生場景下,經(jīng)常使用它來進行指標(biāo)監(jiān)控。
Prometheus支持4種指標(biāo)類型:
- Counter(計數(shù)器):只增不減的指標(biāo),比如請求數(shù),每來一個請求,該指標(biāo)就會加1。
- Gauge(儀表盤):動態(tài)變化的指標(biāo),比如CPU,可以看到它的上下波動。
- Histogram(直方圖):數(shù)據(jù)樣本分布情況的指標(biāo),它將數(shù)據(jù)按Bucket進行劃分,并計算每個Bucket內(nèi)的樣本的一些統(tǒng)計信息,比如樣本總量、平均值等。
- Summary(摘要):類似于Histogram,也用于表示數(shù)據(jù)樣本的分布情況,但同時展示更多的統(tǒng)計信息,如樣本數(shù)量、總和、平均值、上分位數(shù)、下分位數(shù)等。
在實際使用中,常常會將這些指標(biāo)組合起來使用,以便能更好的觀測系統(tǒng)的運行狀態(tài)和性能指標(biāo)。
這些指標(biāo)從何而來?
Prometheus Exporter就是用來收集和暴露指標(biāo)的工具,通常情況下是Prometheus Exporter收集并暴露指標(biāo),然后Prometheus收集并存儲指標(biāo),使用Grafana或者Promethues UI可以查詢并展示指標(biāo)。
Prometheus Exporter主要包含兩個重要的組件:
- Collector:收集應(yīng)用或者其他系統(tǒng)的指標(biāo),然后將其轉(zhuǎn)化為Prometheus可識別收集的指標(biāo)。
- Exporter:它會從Collector獲取指標(biāo)數(shù)據(jù),并將其轉(zhuǎn)成為Prometheus可讀格式。
那Prometheus Exporter是如何生成Prometheus所支持的4種類型指標(biāo)(Counter、Gauge、Histogram、Summary)的呢?
Prometheus提供了客戶端包github.com/prometheus/client_golang,通過它可以聲明不通類型的指標(biāo),比如:
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// 創(chuàng)建一個Counter指標(biāo)
counterMetric := prometheus.NewCounter(prometheus.CounterOpts{
Name: "example_counter", // 指標(biāo)名稱
Help: "An example counter metric.", // 指標(biāo)幫助信息
})
// 注冊指標(biāo)
prometheus.MustRegister(counterMetric)
// 增加指標(biāo)值
counterMetric.Inc()
// 創(chuàng)建一個HTTP處理器來暴露指標(biāo)
http.Handle("/metrics", promhttp.Handler())
// 啟動Web服務(wù)器
http.ListenAndServe(":8080", nil)
}
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// 創(chuàng)建一個Gauge指標(biāo)
guageMetric := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "example_gauge", // 指標(biāo)名稱
Help: "An example gauge metric.", // 指標(biāo)幫助信息
})
// 注冊指標(biāo)
prometheus.MustRegister(guageMetric)
// 設(shè)置指標(biāo)值
guageMetric.Set(100)
// 創(chuàng)建一個HTTP處理器來暴露指標(biāo)
http.Handle("/metrics", promhttp.Handler())
// 啟動Web服務(wù)器
http.ListenAndServe(":8080", nil)
}
import (
"math/rand"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// 創(chuàng)建一個Histogram指標(biāo)
histogramMetric := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "example_histogram", // 指標(biāo)名稱
Help: "An example histogram metric.", // 指標(biāo)幫助信息
Buckets: prometheus.LinearBuckets(0, 10, 10), // 設(shè)置桶寬度
})
// 注冊指標(biāo)
prometheus.MustRegister(histogramMetric)
// 定期更新指標(biāo)值
go func() {
for {
time.Sleep(time.Second)
histogramMetric.Observe(rand.Float64() * 100)
}
}()
// 創(chuàng)建一個HTTP處理器來暴露指標(biāo)
http.Handle("/metrics", promhttp.Handler())
// 啟動Web服務(wù)器
http.ListenAndServe(":8080", nil)
}
import (
"math/rand"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// 創(chuàng)建一個Summary指標(biāo)
summaryMetric := prometheus.NewSummary(prometheus.SummaryOpts{
Name: "example_summary", // 指標(biāo)名稱
Help: "An example summary metric.", // 指標(biāo)幫助信息
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, // 設(shè)置分位數(shù)和偏差
})
// 注冊指標(biāo)
prometheus.MustRegister(summaryMetric)
// 定期更新指標(biāo)值
go func() {
for {
time.Sleep(time.Second)
summaryMetric.Observe(rand.Float64() * 100)
}
}()
// 創(chuàng)建一個HTTP處理器來暴露指標(biāo)
http.Handle("/metrics", promhttp.Handler())
// 啟動Web服務(wù)器
http.ListenAndServe(":8080", nil)
}
上面的例子都是直接在創(chuàng)建指標(biāo)的時候聲明了指標(biāo)描述,我們也可以先聲明描述,再創(chuàng)建指標(biāo),比如:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "net/http")
// 1. 定義一個結(jié)構(gòu)體,用于存放描述信息
type Exporter struct {
summaryDesc *prometheus.Desc
}
// 2. 定義一個Collector接口,用于存放兩個必備函數(shù),Describe和Collect
type Collector interface {
Describe(chan<- *prometheus.Desc)
Collect(chan<- prometheus.Metric)
}
// 3. 定義兩個必備函數(shù)Describe和Collect
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
// 將描述信息放入隊列
ch <- e.summaryDesc
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
// 采集業(yè)務(wù)指標(biāo)數(shù)據(jù)
ch <- prometheus.MustNewConstSummary(
e.summaryDesc, // 將指標(biāo)數(shù)據(jù)與自定義描述信息綁定
4711, 403.34, // 是該指標(biāo)數(shù)據(jù)的值,這里表示該 Summary 指標(biāo)的計數(shù)值和總和值。
map[float64]float64{0.5: 42.3, 0.9: 323.3}, // 是一個 map,其中包含了 Summary 指標(biāo)的 quantile 值及其對應(yīng)的值。例如,0.5 表示 50% 的樣本值處于這個值以下,0.9 表示 90% 的樣本值處于這個值以下
"200", "get", // 是指標(biāo)的標(biāo)簽值,用于標(biāo)識和區(qū)分指標(biāo)實例的特征。這些標(biāo)簽值與在 NewExporter 中創(chuàng)建的 prometheus.NewDesc 函數(shù)的第三個參數(shù)相對應(yīng)。
)
}
// 4. 定義一個實例化函數(shù),用于生成prometheus數(shù)據(jù)
func NewExporter() *Exporter {
return &Exporter{
summaryDesc: prometheus.NewDesc(
"example_summary", // 指標(biāo)名
"An example summary metric.", // 幫助信息
[]string{"code", "method"}, // 變量標(biāo)簽名,值是可變的
prometheus.Labels{"owner": "joker"}, // 常量標(biāo)簽,固定的
),
}
}
func main() {
// 實例化exporter
exporter := NewExporter()
// 注冊指標(biāo)
prometheus.MustRegister(exporter)
// 創(chuàng)建一個HTTP處理器來暴露指標(biāo)
http.Handle("/metrics", promhttp.Handler())
// 啟動Web服務(wù)器
http.ListenAndServe(":8080", nil)
}
通過上面的介紹,對于怎么創(chuàng)建一個Prometheus Exporter是不是有了初步的了解?主要可分為下面幾步:
- 定義一個Exporter結(jié)構(gòu)體,用于存放描述信息
- 實現(xiàn)Collector接口
- 實例化exporter
- 注冊指標(biāo)
- 暴露指標(biāo)
現(xiàn)在開始
有了一定的基本知識后,我們開始開發(fā)自己的Exporter。
我們再來回顧一下需要實現(xiàn)的功能:
- 可以通過配置文件的方式增加監(jiān)控項
- 吐出Prometheus可收集指標(biāo)
- 支持tcp和http探測
- 支持配置檢測頻率
(1)我們的采集對象是通過配置文件加載的,所以我們可以先確定配置文件的格式,我希望的是如下格式:
- url: "http://www.baidu.com"
name: "百度測試"
protocol: "http"
check_interval: 2s
- url: "localhost:2222"
name: "本地接口2222檢測"
protocol: "tcp"
其中check_interval是檢測頻率,如果不寫,默認(rèn)是1s。
我們需要解析配置文件里的內(nèi)容,所以需要先定義配置文件的結(jié)構(gòu)體,如下:
// InterfaceConfig 定義接口配置結(jié)構(gòu)
type InterfaceConfig struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Protocol string `yaml:"protocol"`
CheckInterval time.Duration `yaml:"check_interval,omitempty"`
}
然后,我們使用的是yaml格式的配置文件,保存在config.yaml文件中,意味著我們需要解析config.yaml這個文件,然后再解析。
// loadConfig 從配置文件加載接口配置
func loadConfig(configFile string) ([]InterfaceConfig, error) {
config := []InterfaceConfig{}
// 從文件加載配置
data, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
// 解析配置文件
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}
// 設(shè)置默認(rèn)的檢測時間間隔為1s
for i := range config {
if config[i].CheckInterval == 0 {
config[i].CheckInterval = time.Second
}
}
return config, nil
}
因為監(jiān)控對象可以是多個,所以使用[]InterfaceConfig{}來保存多個對象。
(2)定義接口探測的Collector接口,實現(xiàn)Promethues Collector接口
type HealthCollector struct {
interfaceConfigs []InterfaceConfig
healthStatus *prometheus.Desc
}
這里將配置文件也放進去,期望在初始化HealthCollector的時候?qū)⑴渲梦募徊⒓虞d了。
// NewHealthCollector 創(chuàng)建HealthCollector實例
func NewHealthCollector(configFile string) (*HealthCollector, error) {
// 從配置文件加載接口配置
config, err := loadConfig(configFile)
if err != nil {
return nil, err
}
// 初始化HealthCollector
collector := &HealthCollector{
interfaceConfigs: config,
healthStatus: prometheus.NewDesc(
"interface_health_status",
"Health status of the interfaces",
[]string{"name", "url", "protocol"},
nil, ),
}
return collector, nil
}
在這里定義了[]string{"name", "url", "protocol"}動態(tài)標(biāo)簽,方便使用PromQL查詢指標(biāo)和做監(jiān)控告警。
(3)實現(xiàn)Prometheus Collector接口的Describe和Collect方法
// Describe 實現(xiàn)Prometheus Collector接口的Describe方法
func (c *HealthCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.healthStatus
}
// Collect 實現(xiàn)Prometheus Collector接口的Collect方法
func (c *HealthCollector) Collect(ch chan<- prometheus.Metric) {
var wg sync.WaitGroup
for _, iface := range c.interfaceConfigs {
wg.Add(1)
go func(iface InterfaceConfig) {
defer wg.Done()
// 檢測接口健康狀態(tài)
healthy := c.checkInterfaceHealth(iface)
// 創(chuàng)建Prometheus指標(biāo)
var metricValue float64
if healthy {
metricValue = 1
} else {
metricValue = 0
}
ch <- prometheus.MustNewConstMetric(
c.healthStatus,
prometheus.GaugeValue,
metricValue,
iface.Name,
iface.URL,
iface.Protocol,
)
}(iface)
}
wg.Wait()
}
在Collect方法中,我們通過checkInterfaceHealth來獲取檢測對象的監(jiān)控狀態(tài),然后創(chuàng)建Prometheus對應(yīng)的指標(biāo),這里規(guī)定1就是存活狀態(tài),0就是異常狀態(tài)。
(4)實現(xiàn)http和tcp檢測方法
// checkInterfaceHealth 檢測接口健康狀態(tài)
func (c *HealthCollector) checkInterfaceHealth(iface InterfaceConfig) bool {
switch iface.Protocol {
case "http":
return c.checkHTTPInterfaceHealth(iface)
case "tcp":
return c.checkTCPInterfaceHealth(iface)
default:
return false
}
}
// checkHTTPInterfaceHealth 檢測HTTP接口健康狀態(tài)
func (c *HealthCollector) checkHTTPInterfaceHealth(iface InterfaceConfig) bool {
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get(iface.URL)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
// checkTCPInterfaceHealth 檢測TCP接口健康狀態(tài)
func (c *HealthCollector) checkTCPInterfaceHealth(iface InterfaceConfig) bool {
conn, err := net.DialTimeout("tcp", iface.URL, 5*time.Second)
if err != nil {
return false
}
defer conn.Close()
return true
}
http和tcp的檢測方法這里比較粗暴,http的就請求一次查看狀態(tài)碼,tcp的就檢查能不能建立連接。
(5)創(chuàng)建main方法,完成開發(fā)。
func main() {
// 解析命令行參數(shù)
configFile := flag.String("config", "", "Path to the config file")
flag.Parse()
if *configFile == "" {
// 默認(rèn)使用當(dāng)前目錄下的config.yaml
*configFile = "config.yaml"
}
// 加載配置文件
collector, err := NewHealthCollector(*configFile)
if err != nil {
fmt.Println("Failed to create collector:", err)
return
}
// 注冊HealthCollector
prometheus.MustRegister(collector)
// 啟動HTTP服務(wù),暴露Prometheus指標(biāo)
http.Handle("/metrics", promhttp.Handler())
err = http.ListenAndServe(":2112", nil)
if err != nil {
fmt.Println("Failed to start HTTP server:", err)
os.Exit(1)
}
}
在這里增加了解析命令行參數(shù),支持通過--config的方式來指定配置文件,如果不指定默認(rèn)使用config.yaml。
到這里就開發(fā)完了,雖然沒有嚴(yán)格在寫在前面中梳理的開發(fā)步驟,但是整體大差不差。
應(yīng)用部署
開發(fā)出來的東西如果不上線,那就等于沒做,你的KPI是0,領(lǐng)導(dǎo)才不關(guān)心你做事的過程,只看結(jié)果。所以不論好或是不好,先讓它跑起來才是真的好。
(1)編寫Dockerfile,當(dāng)然要用容器來運行應(yīng)用了。
FROM golang:1.19 AS build-env
ENV GOPROXY https://goproxy.cn
ADD . /go/src/app
WORKDIR /go/src/app
RUN go mod tidy
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/go-interface-health-check
FROM alpine
COPY --from=build-env /go/src/app/go-interface-health-check /usr/local/bin/go-interface-health-check
COPY --from=build-env /go/src/app/config.yaml /opt/
WORKDIR /opt
EXPOSE 2112
CMD [ "go-interface-health-check","--config=/opt/config.yaml" ]
(2)編寫docker-compose配置文件,這里直接使用docker-compose部署,相比K8s的yaml來說更簡單快捷。
version: '3.8'
services:
haproxy:
image: go-interface-health-check:v0.3
container_name: interface-health-check
network_mode: host
restart: unless-stopped
command: [ "go-interface-health-check","--config=/opt/config.yaml" ]
volumes:
- /u01/interface-health-check:/opt
- /etc/localtime:/etc/localtime:ro
user: root
logging:
driver: json-file
options:
max-size: 20m
max-file: 100
使用docker-compose up -d運行容器后,就可以使用curl http://127.0.0.1:2112/metrics查看指標(biāo)。
收集展示
Prometheus的搭建這里不再演示,如果有不清楚的,可以移步這里。
在Prometheus里配置抓取指標(biāo)的配置:
scrape_configs:
- job_name: 'interface-health-check'
static_configs:
- targets: ['127.0.0.1:2112']
配置完重載prometheus,可以查看抓取的target是否存活。
最后,為了方便展示,可以創(chuàng)建一個Grafana面板,比如:
當(dāng)然,可以根據(jù)需要創(chuàng)建告警規(guī)則,當(dāng)interface_health_status==0表示接口異常。
最后
以上就完成了自己開發(fā)一個Prometheus Exporter,上面的例子寫的比較簡單粗暴,可以根據(jù)實際情況來進行調(diào)整。
前兩天刷到馮唐的一句話:“越是底層的人,處理人際關(guān)系的能力就越差,你越往上走,你就會發(fā)現(xiàn),你以為人家天天在研究事,其實他們在研究人。”
你怎么理解這句話?
鏈接
[1] https://www.yuque.com/coolops/kubernetes/dff1cg。
[2] https://www.yuque.com/coolops/kubernetes/wd2vts。
[3] https://github.com/prometheus/client_golang/blob/main/prometheus/examples_test.go。
[4] https://www.cnblogs.com/0x00000/p/17557743.html。