前导知识:监控的两种模式
- 拉取(
PULL)模型(prometheus): 被监控的对象 自己暴露出自己状态- 优点:
- 批量拉取(压缩)
- 监控耦合度比较低(按需更换 Exporter)
- 易于排错
- 独立于监控系统之外, 部署采集器,不需要依赖和知道 Server
- 缺点:
- 实时性不高
- 历史数据无法补充(拉取只能拉到最新的状态)
- 无法跨域防火墙(Ios App 性能上报, 获取浏览器 异常上报)
- 优点:
- 推送(
PUSH)模型(zabbix,tig,openflcon)- 优点:
- 数据及时性,能及时到达 Server
- 跨域防火墙
- 缺点:
- agent 过多,会造成DDOS
- 监控耦合度太高, 会给监控系统的迭代带来很大阻碍
- 优点:
Exporter 开发
我们将要开发的本机的: 8050:/metrics
数据格式
- 通讯协议
- HTTP 协议
- 服务端实现了 gzip
- 数据格式
- text/plain:文本协议
prometheus是拉取数据的监控模型, 它对客户端暴露的数据格式要求如下:
- text/plain:文本协议
简单粗暴
我们直接开发一个满足 prometheus 格式的 API 接口
package main
import (
"fmt"
"net/http"
)
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "lexporter_request_count{user="admin"} 1000" ) // prometheus类型的data
}
func main () {
http.HandleFunc("/metrics", HelloHandler)
http.ListenAndServe(":8050", nil)
}
使用SDK
大多数场景可以利用 Prometheus 提供的 SDK 快速完成 Metric 数据的暴露
默认指标
Prometheus 准备了一个客户端, 可以基于客户端快速添加监控
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// Serve the default Prometheus metrics registry over HTTP on /metrics.
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8050", nil)
}
可以在浏览器中访问 http://127.0.0.1:8050/metrics 来获得默认的监控指标数据:
代码中并什么业务逻辑,但依然有一些指标数据输出,Go 客户端库默认在我们暴露的全局默认指标注册表 中注册了一些关于 promhttp 处理器和runtime 相关的默认指标,根据不同指标名称的前缀可以看出:
go_*:以 go_ 为前缀的指标是关于 Go 运行时相关的指标,比如垃圾回收时间、goroutine 数量等,这些都是 Go 客户端库特有的,其他语言的客户端库可能会暴露各自语言的其他运行时指标。promhttp_*:来自 promhttp 工具包的相关指标,用于跟踪对指标请求的处理。
这些默认的指标是非常有用,但是更多的时候我们需要自己控制,来暴露一些自定义指标。这就需要我们去实现自定义的指标了。
自定义指标
Prometheus 的 Server 端, 只认如下数据格式:
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 19
但是 Prometheus 客户端本身也提供一些简单数据二次加工的能力, 他把这种能力描述为4种指标类型:
Gauges(仪表盘):Gauge类型代表一种样本数据可以任意变化的指标,即可增可减。Counters(计数器):counter类型代表一种样本数据单调递增的指标,即只增不减,除非监控系统发生了重置。Histograms(直方图):需要配置把观测值归入的bucket的数量,以及每个bucket的上边界。Prometheus中的直方图是累积的,所以每一个后续的bucket都包含前一个bucket的观察计数,所有bucket的下限都从 0 开始的,所以我们不需要明确配置每个bucket的下限,只需要配置上限即可。Summaries(摘要):与Histogram类似类型,用于表示一段时间内的数据采样结果(通常是请求持续时间或响应大小等),但它直接存储了分位数(通过客户端计算,然后展示出来),而不是通过区间计算
指标采集
下面以SDK的方式演示4种指标的采集方式
Gauges
最常见的 Metric 类型,即实时指标, 值是什么就返回什么, 并不会加工处理
SDK提供了该指标的构造函数: NewGauge
ChengDuHot := prometheus.NewGauge(prometheus.GaugeOpts{
// Namespace, Subsystem, Name 会拼接成指标的名称: China_SiChuan_ChengDu
// 其中Name是必填参数
Namespace: "China",
Subsystem: "SiChuan",
Name: "ChengDu",
// 指标的描信息
Help: "成都的火热指数",
// 指标的标签
ConstLabels: map[string]string{
"module": "http-server",
},
})
Gauge对象提供了如下方法用来设置他的值:
// 使用 Set() 设置指定的值
ChengDuHot.Set(0)
// 增加或减少
ChengDuHot.Inc() // +1:gauge增加1.
ChengDuHot.Dec() // -1:gauge减少1.
ChengDuHot.Add(23) // 增加23
ChengDuHot.Sub(42) // 减少42
测试用例:
package metric_test
import (
"fmt"
"os"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/expfmt"
)
func TestGauge(t *testing.T) {
ChengDuHot := prometheus.NewGauge(prometheus.GaugeOpts{
// Namespace, Subsystem, Name 会拼接成指标的名称: China_SiChuan_ChengDu
// 其中Name是必填参数
Namespace: "China",
Subsystem: "SiChuan",
Name: "ChengDu",
// 指标的描信息
Help: "成都的火热指数",
// 指标的标签
ConstLabels: map[string]string{
"module": "http-server",
},
})
ChengDuHot.Set(100)
// 创建一个自定义的注册表
registry := prometheus.NewRegistry()
registry.MustRegister(ChengDuHot)
// 获取注册所有数据
data, err := registry.Gather()
if err != nil {
panic(err)
}
// 编码输出
enc := expfmt.NewEncoder(os.Stdout, expfmt.FmtText)
fmt.Println(enc.Encode(data[0]))
}
执行后他的输出
# HELP China_SiChuan_ChengDu 成都的火热指数
# TYPE China_SiChuan_ChengDu gauge
China_SiChuan_ChengDu{module="http-server"} 100
Counters
Counters 是计算器指标, 用于统计次数使用, 通过 prometheus.NewCounter() 函数来初始化指标对象
totalRequests := prometheus.NewCounter(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "The total number of handled HTTP requests.",
})
Inc(): +1:计数器增加1Add(float64): +n:计数器增加n
func TestCounter(t *testing.T) {
totalRequests := prometheus.NewCounter(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "The total number of handled HTTP requests.",
})
for i := 0; i < 10; i++ {
totalRequests.Inc()
}
registry := prometheus.NewRegistry()
registry.MustRegister(totalRequests)
data, err := registry.Gather()
if err != nil {
panic(err)
}
enc := expfmt.NewEncoder(os.Stdout, expfmt.FmtText)
fmt.Println(enc.Encode(data[0]))
}
输出结果
# HELP http_requests_total The total number of handled HTTP requests.
# TYPE http_requests_total counter
http_requests_total 10
Histograms
Histograms 直方图/柱状图, 主要用于统计指标值的一个分布情况, 也就是常见的概率统计问题
比如, 我们要统计一个班级的 成绩分布情况:
- 横轴表示 分数的区间(0-59, 60-70, 70-80, ...)
- 纵轴表示 落在该区间的人数
prometheus 的 Histograms 用于解决这类问题, 用于设置横轴区间的概念叫 Bucket, 不同于传统的区间设置之处, Bucket 只能设置上限, 下限就是最小值,换用 prometheus Histograms, 上面的区间会变成这样:
0 ~ 59
0 ~ 70
0 ~ 80
...
设置好 Bucket 后, prometheus 的客户端需要统计落入每个 Bucket 中的值的数量(即:一个Counter), 也就是 Histograms 这种指标类型的计算逻辑
在监控里面, Histograms 典型的应用场景 就是统计请求耗时分布, 比如
0 ~ 100ms 请求个数
0 ~ 500ms 请求个数
0 ~ 5000ms 请求个数
那为啥不用平均值来进行统计? 提示: 平均值里面的噪点, 比如一个值 远远大于其他所有值的和
我们使用 NewHistogram 初始化一个直方图类型的指标:
requestDurations := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "A histogram of the HTTP request durations in seconds.",
// Bucket 配置:第一个 bucket 包括所有在 0.05s 内完成的请求,最后一个包括所有在10s内完成的请求。
Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
})
Histogram 类型指标提供一个 Observe() 方法, 用于加入一个值到直方图中, 当然加入后 体现在直方图中的不是具体的值,而是值落入区间的统计,实际上每个 bucket 就是一个 Counter 指标
下面是一个完整测试用例
func TestHistogram(t *testing.T) {
requestDurations := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "A histogram of the HTTP request durations in seconds.",
// Bucket 配置:第一个 bucket 包括所有在 0.05s 内完成的请求,最后一个包括所有在10s内完成的请求。
Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
})
// 添加值
for _, v := range []float64{0.01, 0.02, 0.3, 0.4, 0.6, 0.7, 5.5, 11} {
requestDurations.Observe(v)
}
registry := prometheus.NewRegistry()
registry.MustRegister(requestDurations)
data, err := registry.Gather()
if err != nil {
panic(err)
}
enc := expfmt.NewEncoder(os.Stdout, expfmt.FmtText)
fmt.Println(enc.Encode(data[0]))
}
最后的结果
# HELP http_request_duration_seconds A histogram of the HTTP request durations in seconds.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 2
http_request_duration_seconds_bucket{le="0.1"} 2
http_request_duration_seconds_bucket{le="0.25"} 2
http_request_duration_seconds_bucket{le="0.5"} 4
http_request_duration_seconds_bucket{le="1"} 6
http_request_duration_seconds_bucket{le="2.5"} 6
http_request_duration_seconds_bucket{le="5"} 6
http_request_duration_seconds_bucket{le="10"} 7
http_request_duration_seconds_bucket{le="+Inf"} 8
http_request_duration_seconds_sum 18.53
http_request_duration_seconds_count 8
注意点:
- le="+Inf": 表示小于正无穷, 也就是统计所有的含义
- 后缀 _sum: 参加统计的值的求和
- 后缀 _count: 参加统计的值得总数
很多时候直接依赖直方图还是很难定位问题, 我们需要的是请求的一个概统分布, 比如百分之99的请求 落在了那个区间(比如99%请求都在500ms内完成的), 从而判断我们的访问 从整体上看 是良好的。
而像上面的概念分布问题有一个专门的名称叫: quantile, 翻译过来就分位数, 及百分之多少的请求 在那个范围下
基于直方图提供的数据, 可以计算出分位数, 但分位数的精度会受到分区设置精度的影响(bucket设置), 比如只设置了2个bucket, 0.001, 5, 那么你统计出来的100%这个分位数 就是5s, 因为所有的请求都会落到这个bucket中
如果bucket设置是合理的, 又想使用直方图来统计分位数喃? prometheus的QL, 提供了专门的函数histogram_quantile, 可以用于 基于直方图的统计数据,计算分位数
如果服务端压力很大, bucket也不确定, 我能不能直接在客户端计算分位数(quantile)?
答案是有的,就是第四种指标类型: Summaries
Summaries
这种类型的指标 就是用于计算分位数(quantile)的, 因此他需要配置一个核心参数: 你需要统计哪个(百)分位
用 NewSummary 来构建该类指标
requestDurations := prometheus.NewSummary(prometheus.SummaryOpts{
Name: "http_request_duration_seconds",
Help: "A summary of the HTTP request durations in seconds.",
Objectives: map[float64]float64{
0.5: 0.05, // 第50个百分位数,最大绝对误差为0.05。
0.9: 0.01, // 第90个百分位数,最大绝对误差为0.01。
0.99: 0.001, // 第90个百分位数,最大绝对误差为0.001。
},
},
)
和直方图一样, 他也近提供一个方法: Observe, 用于统计数据
下面是具体的测试用例:
func TestSummary(t *testing.T) {
requestDurations := prometheus.NewSummary(prometheus.SummaryOpts{
Name: "http_request_duration_seconds",
Help: "A summary of the HTTP request durations in seconds.",
Objectives: map[float64]float64{
0.5: 0.05, // 第50个百分位数,最大绝对误差为0.05。
0.9: 0.01, // 第90个百分位数,最大绝对误差为0.01。
0.99: 0.001, // 第99个百分位数,最大绝对误差为0.001。
},
})
for _, v := range []float64{0.01, 0.02, 0.3, 0.4, 0.6, 0.7, 5.5, 11} {
requestDurations.Observe(v)
}
registry := prometheus.NewRegistry()
registry.MustRegister(requestDurations)
data, err := registry.Gather()
if err != nil {
panic(err)
}
enc := expfmt.NewEncoder(os.Stdout, expfmt.FmtText)
fmt.Println(enc.Encode(data[0]))
}
最后的结果:
# HELP http_request_duration_seconds A summary of the HTTP request durations in seconds.
# TYPE http_request_duration_seconds summary
http_request_duration_seconds{quantile="0.5"} 0.4 # 中位数是0.4
http_request_duration_seconds{quantile="0.9"} 11 # 90%的位置是11
http_request_duration_seconds{quantile="0.99"} 11
http_request_duration_seconds_sum 18.53
http_request_duration_seconds_count 8
可以看出来 直接使用客户端计算分位数, 准确度不依赖我们设置bucket, 是比较推荐的做法
指标标签
Prometheus将指标的标签分为2类:
- 静态标签:
constLabels, 在指标创建时提前声明好, 采集过程中永不变动 - 动态标签:
variableLabels, 用于在指标的收集过程中动态补充标签, 比如kafka集群的exporter需要动态补充instance_id
静态标签我们在 NewGauge 之类时已经指明, 下面讨论下如何添加动态标签
要让你的指标支持动态标签 有专门的构造函数, 对应关系如下:
- NewGauge() 变成 NewGaugeVec()
- NewCounter() 变成 NewCounterVec()
- NewSummary() 变成 NewSummaryVec()
- NewHistogram() 变成 NewHistogramVec()
下面以NewGaugeVec为例进行讲解
NewGaugeVec相比于NewGauge只多出了一个labelNames的参数:
func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec
一定声明了labelNames, 我们在为指标设置值得时候就必须带上对应个数的标签
queueLength.WithLabelValues("rm_001", "kafka01").Set(100)
完整测试用例:
func TestGaugeVec(t *testing.T) {
ChengDuHot := prometheus.NewGauge(prometheus.GaugeOpts{
// Namespace, Subsystem, Name 会拼接成指标的名称: China_SiChuan_ChengDu
// 其中Name是必填参数
Namespace: "China",
Subsystem: "SiChuan",
Name: "ChengDu",
// 指标的描信息
Help: "成都的火热指数",
// 指标的标签
ConstLabels: map[string]string{
"module": "http-server",
},
}, []string{"instance_id", "instance_name"})
ChengDuHot.WithLabelValues("rm_001", "kafka01").Set(100)
registry := prometheus.NewRegistry()
registry.MustRegister(ChengDuHot)
data, err := registry.Gather()
if err != nil {
panic(err)
}
enc := expfmt.NewEncoder(os.Stdout, expfmt.FmtText)
fmt.Println(enc.Encode(data[0]))
}
最终我们看到的结果如下:
# HELP China_SiChuan_ChengDu 成都的火热指数
# TYPE China_SiChuan_ChengDu gauge
China_SiChuan_ChengDu{instance_id="rm_001",instance_name="kafka01",module="http-server"} 100
指标注册
指标采集完成后需要注册给 Prometheus 的 Http Handler 才能暴露出去, Prometheus客户端提供了对应的接口
// 指标注册接口
type Registerer interface {
// 注册采集器, 有异常会报错
Register(Collector) error
// 与上面相同,但有异常会panic
MustRegister(...Collector)
// 注销该采集器
Unregister(Collector) bool
}
默认注册表
Prometheus 实现了一个默认的 Registerer 对象, 也就是默认注册表
var (
defaultRegistry = NewRegistry()
DefaultRegisterer Registerer = defaultRegistry
DefaultGatherer Gatherer = defaultRegistry
)
我们通过prometheus提供的 MustRegister 可以将我们自定义指标注册进去
// 在默认的注册表中注册该指标
prometheus.MustRegister(temp)
prometheus.Register()
prometheus.Unregister()
下面时一个完整的例子
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main(){
ChengDuHot := prometheus.NewGauge(prometheus.GaugeOpts{
// Namespace, Subsystem, Name 会拼接成指标的名称: China_SiChuan_ChengDu
// 其中Name是必填参数
Namespace: "China",
Subsystem: "SiChuan",
Name: "ChengDu",
// 指标的描信息
Help: "成都的火热指数",
// 指标的标签
ConstLabels: map[string]string{
"module": "http-server",
},
})
prometheus.MustRegister(ChengDuHot)
ChengDuHot.Set(100)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8050", nil)
}
启动后重新访问指标接口,会发现多了一个名为China_SiChuan_ChengDu 的指标:
# HELP China_SiChuan_ChengDu 成都的火热指数
# TYPE China_SiChuan_ChengDu gauge
China_SiChuan_ChengDu{module="http-server"} 100
...
自定义注册表
Prometheus 默认的 Registerer , 会添加一些默认指标的采集, 比如上面的看到的go运行时和当前process相关信息, 如果不想采集指标, 那么最好的方式是 使用自定义的注册表
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main(){
registry := prometheus.NewRegistry()
ChengDuHot := prometheus.NewGauge(prometheus.GaugeOpts{
// Namespace, Subsystem, Name 会拼接成指标的名称: China_SiChuan_ChengDu
// 其中Name是必填参数
Namespace: "China",
Subsystem: "SiChuan",
Name: "ChengDu",
// 指标的描信息
Help: "成都的火热指数",
// 指标的标签
ConstLabels: map[string]string{
"module": "http-server",
},
})
registry.MustRegister(ChengDuHot)
ChengDuHot.Set(100)
http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry}))
http.ListenAndServe(":8050", nil)
}
- 使用
NewRegistry()创建一个全新的注册表 - 通过注册表对象的
MustRegister把指标注册到自定义的注册表中
暴露指标时必须调用 promhttp.HandleFor() 函数创建一个专门针对我们自定义注册表的 HTTP 处理器,还需在 promhttp.HandlerOpts 配置对象的 Registry 字段中传递我们的注册表对象
可以看到指标少了很多, 除了 promhttp_metric_handler 就只有我们自定义的指标了
那如果后面又想把go运行时和当前process相关加入到注册表中暴露出去怎么办?
其实Prometheus在客户端中默认有如下Collector供我们选择
只需把需要的添加到我们自定义的注册表中即可
// 添加 process 和 Go 运行时指标到我们自定义的注册表中
registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
registry.MustRegister(collectors.NewGoCollector())
再次访问http://localhost:8050/metrics, 之前少的指标又回来了
通过查看prometheus提供的 Collectors 我们发现, 直接把指标注册到registry中的方式不太优雅, 为了能更好的模块化, 我们需要把指标采集封装为一个Collector对象, 这也是很多第三方Collecotor的标准写法
采集器
下面是Collector接口声明:
type Collector interface {
// 指标的一些描述信息, 就是# 标识的那部分
// 注意这里使用的是指针, 因为描述信息 全局存储一份就可以了
Describe(chan<- *Desc)
// 指标的数据, 比如 promhttp_metric_handler_errors_total{cause="gathering"} 0
// 这里没有使用指针, 因为每次采集的值都是独立的
Collect(chan<- Metric)
}
下面我们就把之前的单个指标的采集, 改造成使用采集器的方式编写
demo采集器
实现demo采集器
func NewDemoCollector() *DemoCollector {
return &DemoCollector{
queueLengthDesc: prometheus.NewDesc(
"China_SiChuan_ChengDu",
"成都的火热指数",
// 动态标签的key列表
[]string{"instnace_id", "instnace_name"},
// 静态标签
prometheus.Labels{"module": "http-server"},
),
// 动态标的value列表, 这里必须与声明的动态标签的key一一对应
labelValues: []string{"mq_001", "kafka01"},
}
}
type DemoCollector struct {
queueLengthDesc *prometheus.Desc
labelValues []string
}
func (c *DemoCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.queueLengthDesc
}
func (c *DemoCollector) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(c.queueLengthDesc, prometheus.GaugeValue, 100, c.labelValues...)
}
重构后我们的代码将变得简洁优雅:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
registry := prometheus.NewRegistry()
registry.MustRegister(collectors.NewGoCollector())
registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
registry.MustRegister(NewCollctor())
http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry}))
http.ListenAndServe(":8050", nil)
}
最后我们看到的结果如下:
# HELP China_SiChuan_ChengDu 成都的火热指数
# TYPE China_SiChuan_ChengDu gauge
China_SiChuan_ChengDu{instnace_id="mq_001",instnace_name="kafka01",module="http-server"} 10