以下内容来自腾讯工程师 kevin
导语 | Prometheus是一款开源的全面监控解决方案,本文将对其不同方面进行介绍,包括数据收集、查询和可视化展示以及监控告警,以便更好地理解该工具。
Prometheus is a deity from ancient Greek mythology, belonging to the Titan clan. His name, "Prometheus," translates to "foresight." In the depicted image, Prometheus is depicted as being punished by Zeus, enduring the torment of having his liver devoured by an eagle every day, as the night prolongs his suffering.
作为一名IT工程师,我对Prometheus有一定的了解。Prometheus是一种开源的解决方案,用于收集和监控系统的指标数据,以帮助我们洞察系统的运行状态,并能快速识别和解决问题。从其官网封面图引导语可知,Prometheus的目标是从指标数据中获得深入的洞察力,从而为我们的系统提供高效的指标收集和监控。利用Prometheus,我们能够随时监控系统的运行状态,并能迅速定位和解决故障。
Prometheus的发展非常迅速。该项目于2012年开发完成,于2016年加入了Cloud Native Computing Foundation(CNCF),成为继Kubernetes之后的第二个由CNCF托管的项目。目前,该项目在GitHub上有42k个星标,并且拥有一个非常活跃的社区。维护频率非常高,基本上每个月都会发布一个小版本以进行迭代,这使得项目的稳定性得到了保证。
Prometheus提供了从指标暴露,到指标抓取、存储和可视化,以及最后的监控告警等一系列组件。
每个受Prometheus监控的服务都可以被视为一个作业,并且Prometheus提供了官方的软件开发工具包(SDK)来支持这些作业。通过使用此SDK,用户可以自定义和导出自己的业务指标,并且还可以使用Prometheus官方提供的各种常见中间件和组件的导出器(如MySQL、Consul等)。对于执行时间较短的脚本任务或无法直接拉取指标的服务,Prometheus还提供了PushGateway网关,允许这些任务主动将服务指标推送到网关中,再由Prometheus从该网关中拉取这些指标。
上面提到了Push和Pull,其实这是两种指标抓取模型。
指标抓取完成后,会将数据存储在内置的时序数据库中。为了进行指标查询,Prometheus提供了PromQL查询语言。我们可以通过PromQL在Prometheus的WebUI上进行可视化查询指标,并且还可以方便地接入第三方可视化工具,如grafana。
Prometheus为我们提供了Alertmanager,它基于PromQL来进行系统监控告警。当PromQL查询所得的指标超过我们设定的阈值时,Prometheus将生成一条告警信息并发送给Alertmanager。Alertmanager会将这个告警消息下发到我们预先配置好的邮箱或微信。
Prometheus的从被监控服务的注册到指标抓取到指标查询的流程分为五个步骤:
在Prometheus中,被监控服务是通过Job的方式存在的。每个被监控服务的实例在Prometheus中被视为一个target。因此,被监控服务的注册过程实际上就是在Prometheus中注册一个Job和该Job所包含的所有target。这个注册过程可以分为以下几个步骤:
静态注册是一种通过在Prometheus yaml文件的scrape_configs配置下设置服务的IP和抓取指标的端口号的方式。
scrape_configs: - job_name: "prometheus" static_configs: - targets: ["localhost:9090"]
以下是关于注册服务 "prometheus" 的更为正式和专业的描述: 所述服务 "prometheus" 在一个实例下暴露了一个抓取地址 "localhost:9090"。 动态注册是指在 Prometheus 的 YAML 文件的 "scrape_configs" 配置下配置服务发现的地址和服务名称。Prometheus 将会根据提供的服务名称动态发现实例列表,并支持多种服务发现机制,包括但不限于使用 consul、DNS、文件、K8s 等。其中基于 consul 的服务发现是一种选择。
- job_name: "node_export_consul" metrics_path: /node_metrics scheme: http consul_sd_configs: - server: localhost:8500 services: - node_exporter
我们的Consul地址为localhost:8500。在这个地址下,我们有一个服务名为node_exporter,其中包含一个exporter实例:localhost:9600。
如果您使用动态注册,建议添加以下两个配置项。静态注册会默认指定指标拉取路径为/metrics,所以如果暴露的指标拉取路径不同或者是在动态注册的情况下,请务必添加这两个配置项。否则系统可能会报错"INVALID" is not a valid start token。可以通过以下示例进行演示,也可以在百度上搜索相关内容,这个问题可能是由于数据格式不统一所导致的。
metrics_path: /node_metrics scheme: http
最后可以在webUI中查看发现的实例:
目前,Prometheus支持多达二十多种服务发现协议:
<azure_sd_config> <consul_sd_config> <digitalocean_sd_config> <docker_sd_config> <dockerswarm_sd_config> <dns_sd_config> <ec2_sd_config> <openstack_sd_config> <file_sd_config> <gce_sd_config> <hetzner_sd_config> <http_sd_config> <kubernetes_sd_config> <kuma_sd_config> <lightsail_sd_config> <linode_sd_config> <marathon_sd_config> <nerve_sd_config> <serverset_sd_config> <triton_sd_config> <eureka_sd_config> <scaleway_sd_config> <static_config>
在更新完Prometheus的配置文件之后,我们需要将配置更新到程序的内存中。有两种方式可以实现此更新:一种是通过重启Prometheus来实现,这种方式比较简单而直接;另一种是通过动态更新的方式来实现。以下是实现动态更新Prometheus配置的步骤: 第一步:首先,确保在启动Prometheus时带上启动参数--web.enable-lifecycle。
prometheus --config.file=/usr/local/etc/prometheus.yml --web.enable-lifecycle
第二步:去更新我们的Prometheus配置 第三步:更新完配置后,我们可以通过Post请求的方式,动态更新配置:
curl -v --request POST 'http://localhost:9090/-/reload'
原理: Prometheus在web模块中,注册了一个handler
if o.EnableLifecycle { router.Post("/-/quit", h.quit) router.Put("/-/quit", h.quit) router.Post("/-/reload", h.reload) // reload配置 router.Put("/-/reload", h.reload) }
通过h.reload这个handler方法实现:这个handler就是往一个channle中发送一个信号:
func (h *Handler) reload(w http.ResponseWriter, r *http.Request) { rc := make(chan error) h.reloadCh <- rc // 发送一个信号到channe了中 if err := <-rc; err != nil { http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError) } }
在main函数中会去监听这个channel,只要有监听到信号,就会做配置的reload,重新将新配置加载到内存中
case rc := <-webHandler.Reload(): if err := reloadConfig(cfg.configFile, cfg.enableExpandExternalLabels, cfg.tsdb.EnableExemplarStorage, logger, noStepSubqueryInterval, reloaders...); err != nil { level.Error(logger).Log("msg", "Error reloading config", "err", err) rc <- err } else { rc <- nil }
Prometheus通过主动的拉取方式对指标进行抓取。该方式是周期性地向被监控服务暴露的度量接口或PushGateway发出请求,以获取度量指标。默认情况下,每15秒进行一次抓取,并可通过以下配置项进行调整:
global: scrape_interval: 15s
指标捕获将以时间序列形式存储在内存中,并定期刷新到磁盘上,刷新频率默认为每两个小时一次。为了防止Prometheus崩溃或重启时数据丢失,Prometheus还提供了类似于MySQL中的binlog的预写日志。当Prometheus崩溃重启时,会使用该预写日志来恢复数据。
Prometheus采集的所有指标都是以时间序列的形式进行存储,每一个时间序列有三部分组成:
可以通过查看Prometheus的metrics接口查看所有上报的指标:
所有的指标也都是通过如下所示的格式来标识的:
# HELP // HELP:这里描述的指标的信息,表示这个是一个什么指标,统计什么的 # TYPE // TYPE:这个指标是什么类型的 <metric name>{<label name>=<label value>, ...} value // 指标的具体格式,<指标名>{标签集合} 指标值
Prometheus底层存储不区分指标类型,以时间序列形式存储。为了便于用户使用和理解不同监控指标之间的差异,Prometheus定义了四种指标类型:计数器(counter)、仪表盘(gauge)、直方图(histogram)和摘要(summary)。
Counter计数器是一种类型,类似于redis的自增命令,它只能增加,不能减少。通过Counter指标,我们可以统计单调递增的数据,例如Http请求数量、请求错误数、接口调用次数等。另外,我们还可以结合内置函数increase和rate等,来统计数据的变化速率。在接下来的内容中,我们将详细介绍这些函数。
Gauge仪表盘是一种可动态增减的指标,用于反映一些动态变化的数据,例如当前内存占用、CPU利用率、Gc次数等可上升可下降的数据。在Prometheus中,我们可以使用Gauge指标来直观地表示数据的变化情况,而无需使用内置函数。下图显示了表示堆可分配空间大小的Gauge指标示例。
上述文本中提到的指标分为两类:数值指标和统计指标。其中,Histogram和Summary是属于统计指标的一种。这些指标用于描述数据的变化情况和分布情况。Histogram是一种直方图类型的统计指标,用来观察指标在不同区间范围的数据分布情况。例如,可以通过Histogram来观察请求耗时在各个桶(区间)的数据分布情况。
请注意,直方图是一种表示数据分布的图表,其中每个条形表示一个数据范围的数量或频率。在直方图中,每个桶都只有一个上边界值。例如,以下图表显示了小于0.1毫秒的请求数量为18173个,小于0.2毫秒的请求数量为18182个。在le="0.2"这个桶中,包含了le="0.1"这个桶的数据。如果要计算0.1毫秒到0.2毫秒的请求数量,可以通过两个桶的差值来计算。
在直方图中,还可以通过histogram_quantile函数求出百分位数,比如P50,P90,P99等数据
Summary是一种用于统计分析的数据结构,与直方图的区别在于它直接存储百分位数。通过使用Summary,我们可以直观地观察样本数据的中位数、百分之90和百分之99等指标。
每个百分位数的计算是由客户端处理的,然后直接获取给Prometheus,而不需要Prometheus执行计算操作。与此不同的是,直方图通过Prometheus服务端上的内置函数histogram_quantile来计算和求解。
在指标导出方面,我们有两种选择。其一是使用Prometheus社区提供的定制化Exporter,它可以导出MySQL、Kafka等组件的指标。另一种选择是利用社区提供的Client来自定义指标导出。
github.com/prometheus/client_golang/prometheus/promhttp
自定义Prometheus exporter:
package main import ( "net/http" "github.com/prometheus/client_golang/prometheus/promhttp" ) func main() { http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(":8080", nil) }
访问:http://localhost:8080/metrics,即可看到导出的指标,这里我们没有自定义任何的指标,但是能看到一些内置的Go的运行时指标和promhttp相关的指标,这个Client默认为我们暴露的指标,go_:以 go_ 为前缀的指标是关于 Go 运行时相关的指标,比如垃圾回收时间、goroutine 数量等,这些都是 Go 客户端库特有的,其他语言的客户端库可能会暴露各自语言的其他运行时指标。promhttp_:来自 promhttp 工具包的相关指标,用于跟踪对指标请求的处理。
添加自定义指标:
package main import ( "net/http" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) func main() { // 1.定义指标(类型,名字,帮助信息) myCounter := prometheus.NewCounter(prometheus.CounterOpts{ Name: "my_counter_total", Help: "自定义counter", }) // 2.注册指标 prometheus.MustRegister(myCounter) // 3.设置指标值 myCounter.Add(23) http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(":8080", nil) }
运行:
模拟下在业务中上报接口请求量
package main import ( "fmt" "net/http" "github.com/prometheus/client_golang/prometheus" ) var ( MyCounter prometheus.Counter ) // init 注册指标 func init() { // 1.定义指标(类型,名字,帮助信息) MyCounter = prometheus.NewCounter(prometheus.CounterOpts{ Name: "my_counter_total", Help: "自定义counter", }) // 2.注册指标 prometheus.MustRegister(MyCounter) } // Sayhello func Sayhello(w http.ResponseWriter, r *http.Request) { // 接口请求量递增 MyCounter.Inc() fmt.Fprintf(w, "Hello Wrold!") }
main.go:
package main import ( "net/http" "github.com/prometheus/client_golang/prometheus/promhttp" ) func main() { http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/counter",Sayhello) http.ListenAndServe(":8080", nil) }
一开始启动时,指标counter是0
调用:/counter接口后,指标数据发生了变化,这样就可以简单实现了接口请求数的统计
对于其他指标定义方式是一样的:
var ( MyCounter prometheus.Counter MyGauge prometheus.Gauge MyHistogram prometheus.Histogram MySummary prometheus.Summary ) // init 注册指标 func init() { // 1.定义指标(类型,名字,帮助信息) MyCounter = prometheus.NewCounter(prometheus.CounterOpts{ Name: "my_counter_total", Help: "自定义counter", }) // 定义gauge类型指标 MyGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "my_gauge_num", Help: "自定义gauge", }) // 定义histogram MyHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "my_histogram_bucket", Help: "自定义histogram", Buckets: []float64{0.1,0.2,0.3,0.4,0.5}, // 需要指定桶 }) // 定义Summary MySummary = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "my_summary_bucket", Help: "自定义summary", // 这部分可以算好后在set Objectives: map[float64]float64{ 0.5: 0.05, 0.9: 0.01, 0.99: 0.001, }, }) // 2.注册指标 prometheus.MustRegister(MyCounter) prometheus.MustRegister(MyGauge) prometheus.MustRegister(MyHistogram) prometheus.MustRegister(MySummary) }
上述指标目前并未应用标签,然而,通常我们都会使用带标签的指标。要如何为指标设置标签呢?对于需要具备标签的计数器类型指标,只需将现有的NewCounter方法替换为NewCounterVec方法,并传入标签集合作为参数即可。
MyCounter *prometheus.CounterVec // 1.定义指标(类型,名字,帮助信息) MyCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "my_counter_total", Help: "自定义counter", }, // 标签集合 []string{"label1","label2"}, ) // 带标签的set指标值 MyCounter.With(prometheus.Labels{"label1":"1","label2":"2"}).Inc()
其他同理
前面我们提及了在Prometheus中有多种类型的指标以及如何将指标导出到Prometheus。现在,我们可以利用Prometheus提供的PromQL查询语言来查询我们导出的指标了。PromQL是Prometheus为我们提供的一种函数式的查询语言,它支持四种类型的查询表达式:
直接通过指标名即可进行查询,查询结果是当前指标最新的时间序列,比如查询Gc累积消耗的时间:
go_gc_duration_seconds_count
我们可以看到查询出来有多个同名指标结果 可以用{}做标签过滤查询:比如我们想查指定实例的指标
go_gc_duration_seconds_count{instance="127.0.0.1:9600"}
而且也支持则表达式,通过=~指定正则表达式,如下所示:查询所有instance是localhost开头的指标
go_gc_duration_seconds_count{instance=~"localhost.*"}
范围查询的结果集就是区间向量,可以通过[]指定时间来做范围查询
查询5分钟内的Gc累积消耗时间
go_gc_duration_seconds_count{}[5m]
在范围查询中,第一个点并不一定是恰好距离查询时间5分钟前的时序样本点。实际上,范围查询以5分钟为区间,从该区间内的第一个点开始,一直到最后一个样本点。
时间单位:
在 IT 工程师的角度,我们能够支持以更专业的方式表达这段文字。下方的缩写表示的含义:d代表天,h代表小时,m代表分钟,ms代表毫秒,s代表秒,w代表周,y代表年。除此之外,我们也支持像 SQL 中的 offset 查询,以下面的需求为例:查询标准时间之前一天当时五分钟之前的时序数据集。
go_gc_duration_seconds_count{}[5m] offset 1d
作为一名IT工程师,我将为您提供更加专业的叙述方式。Prometheus提供了多个内置函数,以下将重点介绍几个常用函数的用法:其中包括rate和irate函数。rate函数可用于计算指标的平均变化速率。
rate函数=时间区间前后两个点的差 / 时间范围
一般rate函数可以用来求某个时间区间内的请求速率,也就是我们常说的QPS
但是rate函数只能计算某个时间区间内的平均速率,无法反映突发变化。举例来说,在一分钟的时间区间内,前50秒的请求量都维持在0到10之间,但是最后10秒的请求量突然增加到100以上。这种情况下,使用rate函数计算出来的值可能无法准确反映出这个峰值变化。为了解决这个问题,可以使用irate函数来计算瞬时变化率。irate函数能够准确求出瞬时的变化率。
时间区间内最后两个样本点的差 / 最后两个样本点的时间差
可以通过图像看下两者的区别:irate函数的图像峰值变化大,rate函数变化较为平缓 rate函数:
irate函数:
作为一位IT工程师,我想用更专业的方式对下面的文本进行重新表达。在上面的示例中,我们使用了聚合函数Sum() by() without()来计算特定接口的QPS。这可能会导致多个实例的QPS计算结果,下面是一个具体的案例,其中涉及多个接口和三个服务的QPS。
rate(demo_api_request_duration_seconds_count{job="demo", method="GET", status="200"}[5m])
利用sum函数可以将三个QPS聚合,即可得到整个服务该接口的QPS:其实Sum就是将指标值做相加
然而,简单地进行加法运算太过模糊和抽象。可以在求和时结合by和without函数,基于一些标签进行分组,类似于SQL中的group by。例如,可以按照请求接口标签进行分组,这样得到的是各个具体接口的每秒请求数(QPS)。
sum(rate(demo_api_request_duration_seconds_count{job="demo", method="GET", status="200"}[5m])) by(path)
也可以不根据接口路径分组:通过without指定
sum(rate(demo_api_request_duration_seconds_count{job="demo", method="GET", status="200"}[5m])) without(path)
通过使用histogram_quantile函数可以进行数据统计,该函数适用于计算百分位数。函数的第一个参数表示百分位,第二个参数是直方图指标,通过计算可以得到中位数,也就是P50。
histogram_quantile(0.5,go_gc_pauses_seconds_total_bucket)
分享之前和同事一起发现的坑: 在刚刚写的自定义exporter上新增几个histogram的样本点:
MyHistogram.Observe(0.3) MyHistogram.Observe(0.4) MyHistogram.Observe(0.5)
histogram的桶设置:
MyHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "my_histogram_bucket", Help: "自定义histogram", Buckets: []float64{0,2.5,5,7.5,10}, // 需要指定桶 })
在此情况下,所有指标值将集中在第一个桶(范围为0到2.5)中。如果要计算中位数,根据数学公式,中位数肯定在0到2.5之间,并且准确来说应该在0.3到0.5之间。我使用了histogram_quantile函数来计算中位数,结果是1.25,并且事实上这个结果是错误的。
histogram_quantile(0.5,my_histogram_bucket_bucket)
我在计算下P99,等于2.475
histogram_quantile(0.99,my_histogram_bucket_bucket)
我的指标都是不大于1的,为啥算出来的P50和P99都这么离谱呢? 这是因为Prometheus他是不保存你具体的指标数值的,他会帮你把指标放到具体的桶,但是他不会保存你指标的值,计算的分位数是一个预估的值,怎么预估呢?就是假设每个桶内的样本分布是均匀的,线性分布来计算的,比如刚刚的P50,其实就是算排在第50%位置的样本值,因为刚刚所有的数据都落在了第一个桶,那么他在计算的时候就会假定这个50%值在第一个桶的中点,他就会假定这个数就是0.52.5,P99就是第一个桶的99%的位置,他就会假定这个数就是0.992.5 导致这个误差较大的原因就是我们的bucket设置的不合理。 重新定义桶:
// 定义histogram MyHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "my_histogram_bucket", Help: "自定义histogram", Buckets: []float64{0.1,0.2,0.3,0.4,0.5}, // 需要指定桶 })
上报数据:
MyHistogram.Observe(0.1) MyHistogram.Observe(0.3) MyHistogram.Observe(0.4)
重新计算P50,P99
桶设置的越合理,计算的误差越小
在实现指标可视化方面,我们不仅可以使用Prometheus的webUI,还可以使用Grafana。首先,我们需要与数据源进行对接。
配置好prometheus的地址:
第二步:创建仪表盘
编辑仪表盘
在metrics处编写PromQL即可完成查询和可视化
仪表盘编辑完后,可以导出对应的json文件,方便下次导入同样的仪表盘
以上是我之前搭建的仪表盘:
AlertManager是由Prometheus提供的一个用于下发告警信息的组件,它可以根据配置来对告警信息进行分组、下发和静默处理等策略。一旦配置完成,可以在Web界面上查看对应的告警策略信息。告警规则是基于PromQL语言定制的。对于编写告警配置来说,当Http_srv服务宕机时,导致Prometheus无法收集到相关指标并持续1分钟以上,就会触发告警。
groups: - name: simulator-alert-rule rules: - alert: HttpSimulatorDown expr: sum(up{job="http_srv"}) == 0 for: 1m labels: severity: critical
在prometheus.yml中配置告警配置文件,需要配置上alertmanager的地址和告警文件的地址
# Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: ['localhost:9093'] # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. rule_files: - "alert_rules.yml" #- "first_rules.yml"
配置告警信息,例如告警发送地址,告警内容模版,分组策略等都在alertmanager的配置文件中配置:
global: smtp_smarthost: 'smtp.qq.com:465' smtp_from: 'xxxx@qq.com' smtp_auth_username: 'xxxx@qq.com' smtp_auth_password: 'xxxx' smtp_require_tls: false route: group_interval: 1m repeat_interval: 1m receiver: 'mail-receiver' # group_by //采用哪个标签作为分组 # group_wait //分组等待的时间,收到报警不是立马发送出去,而是等待一段时间,看看同一组中是否有其他报警,如果有一并发送 # group_interval //告警时间间隔 # repeat_interval //重复告警时间间隔,可以减少发送告警的频率 # receiver //接收者是谁 # routes //子路由配置 receivers: - name: 'mail-receiver' email_configs: - to: 'xxxx@qq.com'
当我kill进程:
prometheus已经触发告警:
在等待1分钟,如果持续还是符合告警策略,则状态为从pending变为 FIRING会发送邮件到我的邮箱
此时我的邮箱收到了一条告警消息
alertmanager也支持对告警进行静默,在alertmanager的WEBUI中配置即可
间隔了4分钟,没有收到告警,静默生效
一个小时没有收到告警信息
欢迎点赞分享,搜索关注【鹅厂架构师】公众号,一起探索更多业界领先产品技术。
参考链接:https://zhuanlan.zhihu.com/p/644124551
江苏纵目信息科技有限公司是一家专注于运维监控软件产品研发与销售的高科技企业。覆盖全链路应用性能监控、IT基础设施监控、物联网数据采集数据观测等场景,基于Skywalking、Zabbix、ThingsBoard等开源体系构建了ArgusAPM、ArgusOMS、ZeusIoT等产品,致力于帮助各行业客户构建集聚可观测性的统一运维平台、物联网大数据平台。
一体化运维观测
全链路应用性能监控
©2023 江苏纵目科技有限公司 苏ICP备18050677号-1 版权与免责申明 版权申述