文中摘自微信公众平台「运维管理开发设计小故事」,创作者没有文案的夏老师。转截文中请联络运维管理开发设计小故事微信公众号。
Exporter详细介绍
Exporter 是一个收集监控数据并根据 Prometheus 监控标准对外开放给予数据的部件,它承担从总体目标系统软件(Your 服务项目)收集数据,并将其转换为 Prometheus 适用的格式。Prometheus 会周期性地启用 Exporter 给予的 metrics 数据插口来获得数据。那麼应用 Exporter 的优势是啥?举例来说,假如要监控 Mysql/Redis 等数据库,大家务必要启用他们的插口来获得信息(前提条件要有),那样各家都是有一套插口,那样十分不通用性。因此 Prometheus 作法是每一个手机软件做一个 Exporter,Prometheus 的 Http 载入 Exporter 的信息内容(将监控指标值开展统一的格式化并暴露出去)。简易对比,Exporter 便是个翻泽,把各种各样文字翻译成一种统一的语言表达。
针对Exporter来讲,它的基本功能关键是将数据周期性地从监控目标中拿出来完成生产加工,随后将数据规范化后根据节点暴露给Prometheus,因此关键包括如下所示3个作用。
- 封装形式程序模块获得监控系统软件内部结构的统计数据。
- 将回到数据开展规范化投射,使其变成合乎Prometheus规定的格式化数据。
- Collect控制模块承担储存规范化后的数据,最终当Prometheus按时从Exporter获取数据时,Exporter就将Collector搜集的数据根据HTTP的方式在/metrics节点开展暴露。
详细介绍Primetheus client
文中将详细介绍Primetheus client的应用,根据golang语言,golang client 是当pro搜集所监控的系統的数据时,用以回应pro的要求,依照一定的格式给pro回到数据,简言之便是一个http server, 源代码参照github,有关的文本文档参照GoDoc,阅读者可以立即阅读文章文本文档开展开发设计,文中仅仅协助了解。下列是简单化流程表:
四种数据种类
prometheus将全部数据储存为timeseries data,用metric name和label区别,label是在metric name上的更细层面的区划,在其中的每一个案例是由一个float64和timestamp构成,只不过是timestamp是隐式再加上去的,有时不容易表明出去。如下边所显示(数据来自prometheus暴露的监控数据,浏览http://localhost:9090/metrics 可获得),在其中go_gc_duration_seconds是metrics name,quantile="0.5"是key-value pair的label,而后边的值是float64 value。
#HELPgo_gc_duration_secondsAsummaryoftheGCinvocationdurations.#TYPEgo_gc_duration_secondssummarygo_gc_duration_seconds{quantile="0.5"}0.000107458go_gc_duration_seconds{quantile="0.75"}0.000200112go_gc_duration_seconds{quantile="1"}0.000299278go_gc_duration_seconds_sum0.002341738go_gc_duration_seconds_count18#HELPgo_goroutinesNumberofgoroutinesthatcurrentlyexist.#TYPEgo_goroutinesgaugego_goroutines107
这种信息内容有一个相同点,便是运用了有别于JSON或是Protocol Buffers的数据组织结构——文字方式。在文字方式中,每一个指数都占有一行,#HELP意味着标准的注解信息内容,#TYPE用以界定样版的种类注解信息内容,略逊一筹的句子便是详细的监控指标值(即样版)。#HELP的內容格式如下所示所显示,必须填写指标值名字及对应的详细说明信息内容。
HELP<metrics_name><doc_string>
#TYPE的內容格式如下所示所显示,必须填写指标值名字和指标值种类(要是没有确定的指标值种类,必须回到untyped)。
TYPE<metrics_name><metrics_type>
监控样版一部分必须达到如下所示格式标准。
metric_name["{"label_name"=""label_value"{","label_name"=""label_value"}[","]"}"]value[timestamp]
在其中,metric_name和label_name务必遵循PromQL的格式标准。value是一个f loat格式的数据,timestamp的种类为int64(从1970-01-01 00:00:00逐渐迄今的总ms数),可设定其默认设置为现在时间。具备同样metric_name的样版务必依照一个组的方式排序,而且每一行务必是唯一的标准名字和标识键值对组成。Prometheus为了更好地便捷client library的应用带来了四种数据种类:
Counter:Counter是一个累积的数据种类。一个Counter类型的指标值只能伴随着時间慢慢增长(当系统软件重新启动的情况下,Counter指标值会被重设为0)。纪录系统软件成功的总任务总数、系统软件从近期一次运行到现在为止产生的总不正确等数情景都适宜应用Counter种类的指标值。
- Gauge:Gauge指标适用于纪录一个瞬时值,这一指标值可以提升还可以降低,例如CPU的运用状况、运行内存需求量及其电脑硬盘现阶段的室内空间容积等。
- Histogram:Histogram表明柱形图,适用于统计分析一些数据遍布的状况,可以测算在一定区域内的数据遍布状况,与此同时还带来了指标的总数。在大部分情形下,客户会采用一些指标值的平均数做为参照,例如,应用系统软件的均值反应时间来考量系统软件的反应工作能力。这类方法有一个突出的问题——假如大部分要求的反应时间都保持在100ms内,而某些要求的反应时间必须1s乃至更久,那麼反应时间的均值反映出不来反应时间中的硬刺,这就是所说的“长尾关键词问题”。为了更好地更为现实地体现系统软件回应工作能力,常见的形式是依照要求延迟时间的标准开展分类,例如在以上实例中,可以各自统计分析反应时间在[0,100ms]、[100,1s]和[1s,∞]这3个区段的要求数,根据查询这3个系统分区中要求量的遍布,就可以较为理性地剖析出系统软件的反应工作能力。
- Summary:Summary与Histogram相近,也会数量指标的数量(以_count做为后缀名)及其sum值(以_sum做为后缀名)。二者的关键差别取决于,Histogram指标值立即纪录了在不一样区段内样版的数量,而Summary种类则由手机客户端测算相匹配的分位数。例如下边展现了一个Summary种类的指标值,在其中quantile=”0.5”表明平均数,quantile=”0.9”表明九分位数。
理论上讲,全部可以向Prometheus给予监控样版数据的应用程序都能够被称作一个Exporter,Exporter的一个案例被称作target,Prometheus会根据轮询的方式按时从这种target中获得样版数据。
亲自动手撰写一个Exporter
一般来说,绝大部分Exporter全是根据Go语言撰写的,一小部分是根据Python语言表达撰写的,也有不大一部分是应用Java语言撰写的。例如官方网给予的Consul Metrics自定采集器Exporter,如果是在Go语言的运行环境下,必须依照如下所示所示代码运行这一Exporter。
packagemainimport("log""net/http""github.com/prometheus/client_golang/prometheus""github.com/prometheus/client_golang/prometheus/promhttp")var(cpuTemp=prometheus.NewGauge(prometheus.GaugeOpts{NameSpace:"our_idc",Subsystem:"k8s"Name:"cpu_temperature_celsius",Help:"CurrenttemperatureoftheCPU.",})hdFailures=prometheus.NewCounterVec(prometheus.CounterOpts{NameSpace:"our_idc",Subsystem:"k8s"Name:"hd_errors_total",Help:"Numberofhard-diskerrors.",},[]string{"device"},))funcinit(){//Metricshavetoberegisteredtobeexposed:prometheus.MustRegister(cpuTemp)prometheus.MustRegister(hdFailures)}funcmain(){cpuTemp.Set(65.3)hdFailures.With(prometheus.Labels{"device":"/dev/sda"}).Inc()//TheHandlerfunctionprovidesadefaulthandlertoexposemetrics//viaanHTTPserver."/metrics"istheusualendpointforthat.http.Handle("/metrics",promhttp.Handler())log.Fatal(http.ListenAndServe(":8888",nil))
在其中建立了一个gauge和CounterVec目标,并各自特定了metric name和help信息内容,在其中CounterVec是用于管理方法同样metric下不一样label的一组Counter,同样存有GaugeVec,能够看见上边编码中申明了一个lable的key为“device”,应用的过程中也必须规定一个lable: hdFailures.With(prometheus.Labels{"device":"/dev/sda"}).Inc()。变量定义后开展申请注册,最终再打开一个http服务项目的8888端口号就完成了全部程序流程,Prometheus采集数据是根据按时要求该服务项目http端口号来完成的。运行程序流程以后可以在web电脑浏览器里键入http://localhost:8888/metrics 就可以获得client曝露的数据信息,在其中有精彩片段表明为:
#HELPour_idc_k8s_cpu_temperature_celsiusCurrenttemperatureoftheCPU.#TYPEour_idc_k8s_cpu_temperature_celsiusgaugeour_idc_k8s_cpu_temperature_celsius65.3#HELPour_idc_k8s_hd_errors_totalNumberofhard-diskerrors.#TYPEour_idc_k8s_hd_errors_totalcounterour_idc_k8s_hd_errors_total{device="/dev/sda"}1
图中便是实例程序流程所泄露出的数据信息,而且能够看见counterVec是有label的,而纯粹的gauage目标却无需lable标志,这就是基本数据类型和相匹配Vec版本号的区别。这时再查询http://localhost:9090/graph 便会发觉服务项目情况早已变成UP了。上边的案例仅仅一个简便的demo,由于在prometheus.yml环境变量中大家特定收集网络服务器信息内容的间隔时间为60s,每过60s Prometheus会根据http要求一次自身泄露的数据信息,而在编码中大家只设定了一次gauge变量cupTemp的值,假如在60s的取样间距里将该值设定多次,前边的值便会被遮盖,仅有Prometheus采集数据那一刻的值能被见到,而且假如不会再更改这一值,Prometheus就自始至终能看见这一稳定的自变量,除非是客户显式根据Delete函数删掉这一自变量。应用Counter,Gauage等这种构造非常简单,可是假如不会再应用这种自变量必须大家手动式删,我们可以启用resetfunction来消除以前的metrics。
自定义Collector
立即应用Collector,go client Colletor只能在每一次回应Prometheus要求的过程中才收集数据。必须每一次显式传送变量的值,不然就不可能再保持该变量,在Prometheus也将看不见这一变量。Collector是一个插口,全部收集metrics数据的另一半都必须完成这些插口,Counter和Gauage等不除外。它内部结构给予了2个函数公式,Collector用以收集客户数据,将收集好的数据传送给传到主要参数Channel就可;Descirbe函数用以叙述这一Collector。
当收集系统软件收集的数据过多时刻,就可以自定义Collector收集的方法,提升步骤,而且在某种情形下假如已拥有一个完善的metrics,就不用应用Counter,Gauage等这种数据构造,立即在Collector内部结构完成一个代理的作用就可以。
大部分任何的export全是根据自定义Collector完成。一个简便的Collector的完成export的源代码如下所示:
packagemainimport("github.com/prometheus/client_golang/prometheus""github.com/prometheus/client_golang/prometheus/promhttp""net/http""sync")typeClusterManagerstruct{sync.MutexZonestringmetricMapCountersmap[string]stringmetricMapGaugesmap[string]string}//Simulatepreparethedatafunc(c*ClusterManager)ReallyExpensiveAssessmentOfTheSystemState()(metricsmap[string]float64,){metrics=map[string]float64{"oom_crashes_total":42.00,"ram_usage":6.023e23,}return}//根据NewClusterManager方法建立结构体及相匹配的标准信息内容,编码如下所示所显示。//NewClusterManagercreatesthetwoDescsOOMCountDescandRAMUsageDesc.Note//thatthezoneissetasaConstLabel.(It'sdifferentineachinstanceofthe//ClusterManager,butconstantoverthelifetimeofaninstance.)Thenthereis//avariablelabel"host",sincewewanttopartitionthecollectedmetricsby//host.SinceallDescscreatedinthiswayareconsistentacrossinstances,//withaguaranteeddistinctionbythe"zone"label,wecanregisterdifferent//ClusterManagerinstanceswiththesameregistry.funcNewClusterManager(zonestring)*ClusterManager{return&ClusterManager{Zone:zone,metricMapGauges:map[string]string{"ram_usage":"ram_usage_bytes",},metricMapCounters:map[string]string{"oom_crashes":"oom_crashes_total",},}}//最先,数据采集器务必完成prometheus.Collector插口,也务必完成Describe和Collect方式。完成数据接口的源代码如下所示所显示。//DescribesimplysendsthetwoDescsinthestructtothechannel.//Prometheus的注册器启用Collect来爬取主要参数//将收集的数据传送到Channel中并回到//收集的标准信息内容来源于Describe,可以高并发地实行爬取工作中,可是一定要确保进程的安全性func(c*ClusterManager)Describe(chchan<-*prometheus.Desc){//prometheus.NewDesc(prometheus.BuildFQName(namespace,"",metricName),docString,labels,nil)for_,v:=rangec.metricMapGauges{ch<-prometheus.NewDesc(prometheus.BuildFQName(c.Zone,"",v),v,nil,nil)}for_,v:=rangec.metricMapCounters{ch<-prometheus.NewDesc(prometheus.BuildFQName(c.Zone,"",v),v,nil,nil)}}//Collect方式是关键,它会爬取你需要的全部数据信息,依据要求对它进行剖析,随后将指标值推送回手机客户端库。//用以传送全部很有可能指标值的界定描述符//可以在程序执行期内加上新的叙述,搜集新的标准信息内容//反复的描述符将被忽视。2个不一样的Collector不必设定同样的描述符func(c*ClusterManager)Collect(chchan<-prometheus.Metric){c.Lock()deferc.Unlock()m:=c.ReallyExpensiveAssessmentOfTheSystemState()fork,v:=rangem{t:=prometheus.GaugeValueifc.metricMapCounters[k]!=""{t=prometheus.CounterValue}c.registerConstMetric(ch,k,v,t)}}//用以传送全部很有可能指标值的界定描述符给指标值func(c*ClusterManager)registerConstMetric(chchan<-prometheus.Metric,metricstring,valfloat64,valTypeprometheus.ValueType,labelValues...string){descr:=prometheus.NewDesc(prometheus.BuildFQName(c.Zone,"",metric),metric,nil,nil)ifm,err:=prometheus.NewConstMetric(descr,valType,val,labelValues...);err==nil{ch<-m}}funcmain(){workerCA:=NewClusterManager("xiaodian")reg:=prometheus.NewPedanticRegistry()reg.MustRegister(workerCA)//当promhttp.Handler()强制执行时,全部metric被实例化輸出。题外话,实际上輸出的文件格式既可以是plaintext,还可以是protocolBuffers。http.Handle("/metrics",promhttp.HandlerFor(reg,promhttp.HandlerOpts{}))http.ListenAndServe(":8888",nil)}
这时就可以去http://localhost:8888/metrics 见到传送以往的数据信息了。
高品质Exporter的撰写标准与方式
关键方式
参照连接:https://prometheus.io/docs/instrumenting/writing_exporters/。1.在浏览Exporter的首页(即http://yourExporter/那样的根途径)时,它会回到一个简便的网页页面,这就是Exporter的商品详情页(Landing Page)。落地页中可以放文本文档和协助信息内容,包含监管指标值项的表明。商品详情页上还包含近期运行的查验目录、列表的状况及其调试信息,这对常见故障清查十分有协助。
2.一台网络服务器或是器皿上也许会出现很多Exporter和Prometheus部件,他们都是有自身的端口。因而,在写Exporter和公布Exporter以前,必须查验新加入的端口号是不是早已被应用[1],提议应用默认设置端口号分派范畴以外的端口号。
3.大家需要依据业务类型设计方案好指标值的#HELP#TYPE的文件格式。这种指标值通常是可配备的,包含默认设置打开的技术指标和默认设置关掉的指标值。这主要是因为绝大多数指标值并不会真真正正被使用,设计方案太多的指标值不但会耗费多余的資源,还会继续危害总体的特性。
别的方式
针对要怎么写高品质Exporter,除开有效分派端口、设计方案商品详情页、整理指标值这3个层面外,也有一些其它的标准。
- 纪录Exporter自身的运转情况指标值。
- 可配备化开展作用的开启和关掉。
- 强烈推荐应用YAML做为配备文件格式。
- 遵循衡量规范取名的最佳实践[2],尤其是_count、_sum、_total、_bucket和info等问题。
- 为衡量给予合理的企业。
- 标识的唯一性、易读性及必需的沉余信息设计。
- 根据Docker等方法一键配备Exporter。
- 尽可能应用Collectors方法搜集指标值,如Go语言中的MustNewConstMetric。
- 给予scrapes刮擦不成功的异常设计方案,这有利于特性调节。
- 最好不要反复给予现有的指标值,如Node Exporter早已给予的CPU、硬盘等信息内容。
- 向Prometheus公布初始的衡量数据信息,不建议自主测算,Exporter的核心内容是收集初始指标值。
Redis Exporter源代码分析
在本文中,阅读者可以发觉开源系统行业拥有数不胜数的Exporter,阿里开源系统的Exporter就会有RocketMQ Exporter、Sentinel Exporter、Sentry Exporter、Alibaba Cloud Exporter等多种多样。撰写Exporter和编写Spring Boot Starter一样,可以多参照别的出色的开源项目的编码。这节就来简洁明了剖析一下运维管理工作上使用到的Redis Exporter源代码。在应用Redis Exporter时,可以根据redis_exporter--help指令查询详细的主要参数目录。默认设置状况下,它在端口号9192上运作,并在途径/metrics上曝露指标值。可以根据--web.listen-addres和--web.telemetry-path指令来设定端口号和途径,编码如下所示所显示。
redis_exporter-web.listen-address=":8888"-web.telemetry-path="/node_metrics"
以上编码将改动redis Exporter关联到端口号8888并在途径/node_metrics上曝露指标值。这一逻辑性是在源代码redis_exporter.go中保持的.Redis Exporter[3]关键根据Redis原生态的指令获得Redis全部的信息内容,它适用2.x、3.x、4.x、5.x和6.x版本号。在源代码中,能够看见好几处应用了doRedisCmd方式推送指令以获得性能参数,编码如下所示所显示。主要是根据原生态的INFO指令获得全部特性信息内容。该指令的回到結果详细信息参考[4]。
infoAll,err:=redis.String(doRedisCmd(c,"INFO","ALL"))
转化成的infoAll信息内容根据func (e *Exporter) extractInfoMetrics(ch chan<- prometheus.Metric, info string, dbCount int)再次解决。它的具体目标是解析xml查看到的結果,依据指标值转化成一个hash值。源码如下所示所显示:
func(e*Exporter)extractInfoMetrics(chchan<-prometheus.Metric,infostring,dbCountint){keyValues:=map[string]string{}handledDBs:=map[string]bool{}fieldClass:=""//以回车符开展切分lines:=strings.Split(info,"\n")masterHost:=""masterPort:=""//解析xml查看到的結果,依据指标值转化成一个hash值for_,line:=rangelines{line=strings.TrimSpace(line)log.Debugf("info:%s",line)//除去带#的注解文档iflen(line)>0&&strings.HasPrefix(line,"#"){fieldClass=line[2:]log.Debugf("setfieldClass:%s",fieldClass)continue}//除去没有:的或是标识符低于2的if(len(line)<2)||(!strings.Contains(line,":")){continue}//以冒号开展切分split:=strings.SplitN(line,":",2)fieldKey:=split[0]fieldValue:=split[1]//将指标值名字与值存进hash中keyValues[fieldKey]=fieldValueiffieldKey=="master_host"{masterHost=fieldValue}iffieldKey=="master_port"{masterPort=fieldValue}//依照群集和团本和哨兵模式开展解决switchfieldClass{case"Replication":ifok:=e.handleMetricsReplication(ch,masterHost,masterPort,fieldKey,fieldValue);ok{continue}case"Server":e.handleMetricsServer(ch,fieldKey,fieldValue)case"Commandstats":e.handleMetricsCommandStats(ch,fieldKey,fieldValue)continuecase"Keyspace":ifkeysTotal,keysEx,avgTTL,ok:=parseDBKeyspaceString(fieldKey,fieldValue);ok{dbName:=fieldKeye.registerConstMetricGauge(ch,"db_keys",keysTotal,dbName)e.registerConstMetricGauge(ch,"db_keys_expiring",keysEx,dbName)ifavgTTL>-1{e.registerConstMetricGauge(ch,"db_avg_ttl_seconds",avgTTL,dbName)}handledDBs[dbName]=truecontinue}case"Sentinel":e.handleMetricsSentinel(ch,fieldKey,fieldValue)}if!e.includeMetric(fieldKey){continue}//将采集到数据开展依照一定标准开展解决e.parseAndRegisterConstMetric(ch,fieldKey,fieldValue)}fordbIndex:=0;dbIndex<dbCount;dbIndex {dbName:="db" strconv.Itoa(dbIndex)if_,exists:=handledDBs[dbName];!exists{e.registerConstMetricGauge(ch,"db_keys",0,dbName)e.registerConstMetricGauge(ch,"db_keys_expiring",0,dbName)}}e.registerConstMetricGauge(ch,"instance_info",1,keyValues["role"],keyValues["redis_version"],keyValues["redis_build_id"],keyValues["redis_mode"],keyValues["os"],keyValues["maxmemory_policy"],keyValues["tcp_port"],keyValues["run_id"],keyValues["process_id"],)ifkeyValues["role"]=="slave"{e.registerConstMetricGauge(ch,"slave_info",1,keyValues["master_host"],keyValues["master_port"],keyValues["slave_read_only"])}}
随后根据e.parseAndRegisterConstMetric(ch, fieldKey, fieldValue)方式,将采集到hash中的信息内容,依照一定的标准转化成prometheus.Metric。关键编码如下所示:
func(e*Exporter)registerConstMetric(chchan<-prometheus.Metric,metricstring,valfloat64,valTypeprometheus.ValueType,labelValues...string){descr:=e.metricDescriptions[metric]ifdescr==nil{descr=newMetricDescr(e.options.Namespace,metric,metric "metric",labelValues)}ifm,err:=prometheus.NewConstMetric(descr,valType,val,labelValues...);err==nil{ch<-m}}
最终*Exporter.Collect的方式启用registerConstMetric方式,就完成了redis的info指标值的搜集。别的指数的搜集原先也是一致的,有感兴趣的用户可以自主阅读文章。
汇总
文中详细介绍了Exporter的定义。Exporter的由来关键有两个:一个是小区给予的,一个是客户定制的。在现实制造中,官方网给予的Exporter关键包含数据库查询、硬件配置、问题追踪及持续交付、信息系统软件、储存、HTTP、API、日志、别的监控系统等,这种已经有的Exporter可以达到绝大部分开发者及运维管理员工的要求。针对系统软件、手机软件沒有Exporter的状况,此章也从数据信息标准、数据采集方法、编码实例编写等层面领着阅读者感受了Exporter的制定与实践活动,一步步具体指导阅读者打造出订制化Exporter。为了更好地协助阅读者产生较好的编码设计风格并能真真正正撰写高品质Exporter,此章还得出了撰写高品质Exporter的提议,并融合 Redis Exporter的工作原理实现了实战演练分析。根据对此章的学习培训,阅读者可以把握应用和订制Exporter的工作能力。
[1] Exporter端口号目录:https://github.com/prometheus/prometheus/wiki/Default-port-allocations。
[2] 规范取名最佳实践:https://prometheus.io/docs/practices/naming。
[3] Redis Exporter详细地址:https://github.com/oliver006/redis_Exporter。
[4] Redis INFO指令详细地址:https://redis.io/commands/info。