client_golang 是Prometheus client的使用,基于golang语言。提供了prometheus的数据规范。
库结构
地址: https://github.com/prometheus/client_golang
Directories(描述)
api Package api provides clients for the HTTP APIs.
api/prometheus/v1 Package v1 provides bindings to the Prometheus HTTP API v1: http://prometheus.io/docs/querying/api/
examples/random A simple example exposing fictional RPC latencies with different types of random distributions (uniform, normal, and exponential) as Prometheus metrics.
examples/simple A minimal example of how to include Prometheus instrumentation.
prometheus Package prometheus is the core instrumentation package.
prometheus/graphite Package graphite provides a bridge to push Prometheus metrics to a Graphite server.
prometheus/promauto Package promauto provides constructors for the usual Prometheus metrics that return them already registered with the global registry (prometheus.DefaultRegisterer).
prometheus/promhttp Package promhttp provides tooling around HTTP servers and clients.
prometheus/push Package push provides functions to push metrics to a Pushgateway.
上面对client_golang库的结构和使用进行了总结。
原理
原理解析
下面是客户端的UML图
1、默认调用gocollect这个采集器,在引入package registry包的时候,就会调用init初始化
- registry会调用describe这个接口,实现就是gocollect这个对应的describe
- http.handle会调用registry的gather函数,然后函数调用collect接口,实现就是gocollect这个对应的collect
2、上面只是一种特殊类型,其实对应的四种类型分别都有对应的结构体继承vec的基本函数接口,也有对应的接口,会有对应的实现
然后这个四种类型就是就是四种collecter,同样的流程
3、可以新建一个struct作为一个collecter,实现describe,collect接口,就可以实现自己的逻辑,最后其实还是调用四种类型,结合使用
使用
1、四种类型有实现的函数赋值,常用
set()
WithLabelValues().set()
2、注册的几种方式
第一种
//statement
proxyTodayTrafficIn := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "",
Help: "The today trafficin of proxy.",
},[]string{"type","laststarttime","lastclosetime"})
//get value
proxyTodayTrafficIn.With(prometheus.Labels{"type":v.Type,"laststarttime":v.LastStartTime,"lastclosetime":v.LastCloseTime}).Set(float64(v.TodayTrafficIn))
//registry
prometheus.MustRegister(proxyTodayTrafficIn)
第二种
serverBindPort := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "frps_server_bind_port",
Help: "The port of server frps.",
})
serverBindPort.Set(float64(cfg.BindPort))
//registry
prometheus.MustRegister(serverBindPort)
上面两种可以归为一类,都是采用的默认的方式,下面就涉及到自定义结构体,根据上面的原理,我们需要重自定义的结构体中获取到两个结构体的值
func (s *结构体) Describe(ch chan<- *prometheus.Desc) {}----可见这个接口的实现需要将prometheus.Desc放倒channel中去
func (s *结构体) Collect(ch chan<- prometheus.Metric) {}----可见这个接口的实现需要将prometheus.Metric放倒channel中去
我们来看看这两个结构体
type Desc struct {
// fqName has been built from Namespace, Subsystem, and Name.
fqName string
// help provides some helpful information about this metric.
help string
// constLabelPairs contains precalculated DTO label pairs based on
// the constant labels.
constLabelPairs []*dto.LabelPair
// VariableLabels contains names of labels for which the metric
// maintains variable values.
variableLabels []string
// id is a hash of the values of the ConstLabels and fqName. This
// must be unique among all registered descriptors and can therefore be
// used as an identifier of the descriptor.
id uint64
// dimHash is a hash of the label names (preset and variable) and the
// Help string. Each Desc with the same fqName must have the same
// dimHash.
dimHash uint64
// err is an error that occured during construction. It is reported on
// registration time.
err error
}
type Metric interface {
// Desc returns the descriptor for the Metric. This method idempotently
// returns the same descriptor throughout the lifetime of the
// Metric. The returned descriptor is immutable by contract. A Metric
// unable to describe itself must return an invalid descriptor (created
// with NewInvalidDesc).
Desc() *Desc
// Write encodes the Metric into a "Metric" Protocol Buffer data
// transmission object.
//
// Metric implementations must observe concurrency safety as reads of
// this metric may occur at any time, and any blocking occurs at the
// expense of total performance of rendering all registered
// metrics. Ideally, Metric implementations should support concurrent
// readers.
//
// While populating dto.Metric, it is the responsibility of the
// implementation to ensure validity of the Metric protobuf (like valid
// UTF-8 strings or syntactically valid metric and label names). It is
// recommended to sort labels lexicographically. (Implementers may find
// LabelPairSorter useful for that.) Callers of Write should still make
// sure of sorting if they depend on it.
Write(*dto.Metric) error
// TODO(beorn7): The original rationale of passing in a pre-allocated
// dto.Metric protobuf to save allocations has disappeared. The
// signature of this method should be changed to "Write() (*dto.Metric,
// error)".
}
让我看看如何获取这两种值,首先desc,每种数据类型都有一个desc函数可以直接获取,如下:
name := fmt.Sprintf("%s_%s", namespace, metricMaps.Name)
gaugeDescription := prometheus.NewGauge(
prometheus.GaugeOpts{
Name: name,
Help: metricMaps.Description,
},
)
ch <- gaugeDescription.Desc()
还可以直接新建
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {}
//new desc
desc := prometheus.NewDesc(name, metricMaps.Description, constLabels, nil)
再来看看metrics这个接口,找到其相应的结构体实现
func MustNewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) Metric {}
//channel
ch <- prometheus.MustNewConstMetric(desc, vtype, value, labelValue...)
第三种
新建结构体,完成上面方法的使用,就可以了,如下:
//set var
vtype := prometheus.CounterValue
name := fmt.Sprintf("%s_%s", namespace, metricMaps.Name)
log.Debugf("counter name: %s", name)
//new desc
desc := prometheus.NewDesc(name, metricMaps.Description, constLabels, nil)
//deal Value
value, err := dealValue(res[i])
if err != nil {
log.Errorf("parse value error: %s",err)
break
}
log.Debugf("counter value: %s", value)
//channel
ch <- prometheus.MustNewConstMetric(desc, vtype, value, labelValue...)
3、Newregistry调用和直接调用prometheus的对应的MustRegister其实是一样的,都是默认new一个registry的结构体
4、duplicate metrics collector registration attempted—重复注册
Collector
Collector 中 Describe 和 Collect 方法都是无状态的函数,其中 Describe 暴露全部可能的 Metric 描述列表,在注册(Register)或注销(Unregister)Collector 时会调用 Describe 来获取完整的 Metric 列表,用以检测 Metric 定义的冲突,另外在 github.com/prometheus/client_golang/prometheus/promhttp 下的 Instrument Handler 中,也会通过 Describe 获取 Metric 列表,并检查 label 列表(InstrumentHandler 中只支持 code 和 method 两种自定义 label);而通过 Collect 可以获取采样数据,然后通过 HTTP 接口暴露给 Prom Server。另外,一些临时性的进程,如批处理任务,可以把数据 push 到 Push Gateway,由 Push Gateway 暴露 pull 接口,此处不赘述。
客户端对数据的收集大多是针对标准数据结构来进行的,如下:
- Counter:收集事件次数等单调递增的数据
- Gauge:收集当前的状态,可增可减,比如数据库连接数
- Histogram:收集随机正态分布数据,比如响应延迟
- Summary:收集随机正态分布数据,和 Histogram 是类似的
每种标准数据结构还对应了 Vec 结构,通过 Vec 可以简洁的定义一组相同性质的 Metric,在采集数据的时候传入一组自定义的 Label/Value 获取具体的 Metric(Counter/Gauge/Histogram/Summary),最终都会落实到基本的数据结构上,这里不再赘述。
Counter 和 Gauge
Gauge 和 Counter 基本实现上看是一个进程内共享的浮点数,基于 value 结构实现,而 Counter 和 Gauge 仅仅封装了对这个共享浮点数的各种操作和合法性检查逻辑。
看 Counter 中 Inc 函数的实现
value.Add 中修改共享数据时采用了“无锁”实现,相比“有锁 (Mutex)”实现可以更充分利用多核处理器的并行计算能力,性能相比加 Mutex 的实现会有很大提升。下图是 Go Benchmark 的测试结果,对比了“有锁”(用 defer 或不用 defer 来释放锁)和“无锁”实现在多核场景下对性能的影响。
Histogram
Histogram 实现了 Observer 接口,用来获取客户端状态初始化(重启)到某个时间点的采样点分布,监控数据常需要服从正态分布。
Summary
Summary 是标准数据结构中最复杂的一个,用来收集服从正态分布的采样数据。在 Go 客户端 Summary 结构和 Histogram 一样,都实现了 Observer 接口
这两个比较复杂,使用较少,可以先不研究,使用的时候研究
集成注意事项
Go 客户端为 HTTP 层的集成提供了方便的 API,但使用中需要注意不要使用 github.com/prometheus/client_golang/prometheus 下定义的已经 deprecated 的 Instrument 函数,除了会引入额外(通常不需要)的监控数据,不仅会对程序性能造成不利影响,而且可能存在危险的 race(如计算请求大小时存在 goroutine 并发地访问 Header 逻辑)。
Prometheus Exporter实例
Exporter是基于Prometheus实施的监控系统中重要的组成部分,承担数据指标的采集工作,官方的exporter列表中已经包含了常见的绝大多数的系统指标监控,比如用于机器性能监控的node_exporter, 用于网络设备监控的snmp_exporter等等。这些已有的exporter对于监控来说,仅仅需要很少的配置工作就能提供完善的数据指标采集。
有时我们需要自己去写一些与业务逻辑比较相关的指标监控,这些指标无法通过常见的exporter获取到。比如我们需要提供对于DNS解析情况的整体监控,了解如何编写exporter对于业务监控很重要,也是完善监控系统需要经历的一个阶段。接下来我们就介绍如何编写exporter, 本篇内容编写的语言为golang, 官方也提供了python, java等其他的语言实现的库,采集方式其实大同小异。编写exporter的方式也是大同小异,就是集成对应的prometheus库,我们使用golang语言,就是集成client_golang。
下面我们就使用golang语言集成cleint_golang来开发一个exporter。
搭建环境
首先确保机器上安装了go语言(1.7版本以上),并设置好了对应的GOPATH。接下来我们就可以开始编写代码了。以下是一个简单的exporter
下载对应的prometheus包
go get github.com/prometheus/client_golang/prometheus/promhttp
程序主函数:
package main
import (
"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"},
)
加一个counter的用法
totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Name: "exporter_scrapes_total",
Help: "Current total redis scrapes.",
})
这里还可以注册其他的参数,比如上面的磁盘失败次数统计上,我们可以同时传递一个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.3
hd_errors_total{"device"="/dev/sda"} 1
当然我们写在main函数中的方式是有问题的,这样这个指标仅仅改变了一次,不会随着我们下次采集数据的时候发生任何变化,我们希望的是每次执行采集的时候,程序都去自动的抓取指标并将数据通过http的方式传递给我们。
到这里,一套基本的采集流程也就完成了,这是最基本的使用方式,当然其中也还是封装了很多过程,比如采集器等,如果需要自定义一些东西,就要了解这些封装的过程,完成重写,下面我们自定义exporter。
自定义exporter
counter数据采集实例,重写collecter
下面是一个采集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"} 364
clustermanager_oom_crashes_total{host="bar.example.org",zone="db"} 90
clustermanager_oom_crashes_total{host="foo.example.org",zone="ca"} 844
clustermanager_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 gauge
clustermanager_ram_usage_bytes{host="bar.example.org",zone="ca"} 10.738111282075208
clustermanager_ram_usage_bytes{host="bar.example.org",zone="db"} 19.003276633920805
clustermanager_ram_usage_bytes{host="foo.example.org",zone="ca"} 79.72085409108028
clustermanager_ram_usage_bytes{host="foo.example.org",zone="db"} 13.041384617379178
每次刷新的时候,我们都会获得不同的数据,类似于实现了一个数值不断改变的采集器。当然,具体的指标和采集函数还需要按照需求进行修改,满足实际的业务需求。