当前位置: 首页 > 后端技术 > Node.js

NodeJS运维:Prometheus+Grafana业务性能指标监控从0开始

时间:2023-04-03 18:26:55 Node.js

为什么需要指标监控和告警?一个复杂的应用往往是由很多模块组成的,往往会有各种千奇百怪的使用场景。谁也不能保证它维护的服务永远不会出错。用户投诉发现问题再处理就来不及了,损失是无法弥补的。因此,利用数据指标来衡量一个服务的稳定性和处理效率,是否正常运行,监控指标曲线的状态,在指标异常时及时主动报警就显得非常重要了。一些常见的指标包括但不限于:QPS请求处理耗时进程占用内存进程占用CPUgolang服务goroutinenodejs事件循环滞后前端应用性能耗时...例如如果一个服务:使用内存超时逐渐上升的CPU占用率越来越高,请求耗时越来越高,请求成功率越来越低。磁盘空间经常过度拥挤。或者前端单页应用:前端重定向到/error页面,/excption页面越来越多。某个页面的打开次数越来越少。某个系统/版??本的设备激活率越来越低……这是人性的扭曲还是道德的沦丧?一旦应用中的某些缺陷导致了这些问题,通过服务日志很难直观快速地检测到这些指标的变化和波动。至于前端,很可能根本无法感知用户的行为,只能通过埋点进行一定程度的监控。通过监控告警手段,有效覆盖“发现”和“定位”问题,更高效地排查和解决问题。指标监控系统:PrometheusPrometheus是一个开源的服务监控系统和时序数据库。工作流程可以简化为:客户端采集当前机器/服务/进程状态等相关指标数据。Prometheus服务器根据一定时间主动拉取客户端的指标数据,并存储在时序数据库中。发现指标异常后,告警管理器会向相关负责人发送告警通知的具体架构设计如下:为什么不用mysql存储?Prometheus使用的是自己设计的时序数据库(TSDB),那为什么不使用大家比较熟悉和常用的mysql,或者其他关系型数据库呢?假设需要监控WebServerA的各个API的请求量为例,需要监控的维度包括:服务名(job)、实例IP(instance)、API名(handler)、方法(method)、返回码(代码)和请求量(值)。以SQL为例,演示常见的查询操作:#查询method=put,code=200的请求量SELECT*fromhttp_requests_totalWHEREcode=”200”ANDmethod=”put”ANDcreated_atBETWEEN1495435700AND1495435710;#查询处理程序=prometheusandmethod=postrequestsSELECT*fromhttp_requests_totalWHEREhandler=”prometheus”ANDmethod=”post”ANDcreated_atBETWEEN1495435700AND1495435710;#Queryinstance=10.59.8.110andthehandlerstartswithquerySELECT*fromhttp_requests_totalWHEREhandler=”query”ANDinstance=”10.59.8.110”ANDcreated_atBETWEEN1495435700AND1495435710;从上面的例子可以看出,在常见的查询和统计方面,日常监控大多是根据监控的维度,结合查询和时间进行查询。如果监控100个服务,每个服务平均部署10个实例,每个服务有20个API,4个方法,每30秒采集一次数据,保存60天。那么数据条总数为:100(服务)*10(实例)*20(API)*4(方法)*86400(秒/天)*60(天)/30(秒)=138.24亿条数据,这种量级的数据在Mysql等关系型数据库上是不可能写入、存储和查询的。所以Prometheus使用TSDB作为存储引擎。TimeSeriesDatabase(TSDB)TimeSeriesDatabase主要用于处理带有时间标签的数据(时间顺序的变化,即时间序列化)。带有时间标签的数据也称为时间序列数据。对于prometheus来说,每个时序点的结构如下:metric:metric名称,当前数据的标识,在某些系统中也叫name。label:标签属性timestamp:数据点的时间,表示数据发生的时间。value:值,数据的价值每个指标都有多个时间序列图;多个时间序列数据点连接起来形成一个时间序列图。如果用传统的关系数据库来表示时序数据,它有如下结构:create_time__metric_name__pathvalue2020-10-0100:00:00http_request_total/home1002020-10-0100:00:00http_request_total/error02020-10-0100:00:15http_request_total/home1202020-10-0100:01:00http_request_total/home1602020-10-0100:01:00http_request_total/2020-10-0100:01:00qps=(160-100)/60=1,同样,指标request_total{path="/error"}在2020-10-0100:01:00时qps=1/60。与MySQL相比,时序数据库的核心在于时间序列。查询与时间相关的数据,资源消耗相对较低,效率较高。数据具有明显的时序特征,因此采用时序数据库作为存储层的数据类型。Change,diskcapacity,CPUusage,histogram:,聚合数据查询耗时分布【服务器端计算,模糊,不准确】总结:无法聚合查询的耗时分布【客户端计算,准确】nodejs索引集合和数据拉取并定义一个Counter数据类型,记录索引constreqCounter=newCounter({name:`credit_insight_spl_id_all_pv`,help:'requestcount',labelNames:['deviceBrand','systemType','appVersion','channel']})reqCounter.inc({deviceBrand:'Apple',ssystemType:'iOS',appVersion:'26014',channel:'mepage'},1)定义访问路径为/metrics的controller@Get('metrics')getMetrics(@Res()res){res.set('Content-Type',register.contentType)res.send(register.metrics())}Prometheus主动请求node客户端的/metrics接口获取当前数据快照promQLpromQL是prometheus的查询语言,语法很simple和basicquery查询指标都是最新的Value:{__name__="http_request_total",handler="/home"}#语法糖:http_request_total{handler="/home"}#相当于mysql:select*fromhttp_request_totalwherehandler="/home"ANDcreate_time=《now()》间隔时间段查询查询过去一分钟的数据#promQLhttp_request_total[1m]#相当于SELECT*fromhttp_requests_totalWHEREcreate_timeBETWEEN《now() - 1min》AND《now()》;时间偏移量查询PS:promQL不支持指定时间点查询,只能通过偏移量查询历史某个时间点的数据,查询一小时前的数据#promQLhttp_request_totaloffset1h#相当于SELECT*fromhttp_requests_totalWHEREcreate_time=《now() - 1 hour》;promQL查询函数根据上面的查询语法,我们可以简单的组合一些索引数据:比如查询最近的/home页面请求数dayhttp_request_total{handler="/home"}-http_request_total{handler="/home"}offset1d其实上面的写法显然不够简洁,我们可以使用内置的增加函数来代替:#相当于上面的写法increase(http_request_total{handler="/home"}[1d])除了increase之外,还有很多其他有用的功能,比如rate函数计算QPS//每秒平均请求数过去2分钟rate(http_request_total{code="400"}[2m])//相当于增加(http_request_total{code="400"}[2m])/120个索引聚合查询除了上面的基础查询,我们还可以还需要聚合查询如果我们有以下数据指标:,channel="none"}credit_insight_spl_id_all_pv{url="/error",channel="mepage"}当所有的索引数据在某个维度上聚合查询时,例如:查询url="/home"最后一天的访问量,channel是none或mepage的/homevisits都包含在里面。当然我们会写:increase(credit_insight_spl_id_all_pv{url="/home"}[1d])但实际上我们会得到两个这样的指标:credit_insight_spl_id_all_pv{url="/home",channel="none"}233credit_insight_spl_id_all_pv{url="/home",channel="mepage"}666不是我们期望的:credit_insight_spl_id_all_pv{url="/home"}899但是如果我们想要得到这样的聚合查询结果,我们需要使用sumby#聚合url="/home"的数据sum(increase(credit_insight_spl_id_all_pv{url="/home"}[1d]))by(url)#得到结果:credit_insight_spl_id_all_pv{url="/home"}899#all的累计值访问/homepageinthechannel#聚合所有url可以这样写:sum(increase(credit_insight_spl_id_all_pv{}[1d]))by(url)#得到结果:credit_insight_spl_id_all_pv{url="/home"}899credit_insight_spl_id_all_pv{url="/error"}7#相当于mysqlSELECTurl,COUNT(*)AStotalFROMcredit_insight_spl_id_all_pvWHEREcreate_timebetweenandGROUPBYurl;时序曲线上方的指标所有例子中的查询值其实都是最近一个时间点的值,我们更关注一个时间段内的值变化。实现这个原理也很简单。只需要在历史上的每个时间点进行一次索引查询,#如果今天7号#一天中从6号到7号的访问量sum(increase(credit_insight_spl_id_all_pv{}[1d]))by(url)#One-5号到6号一天的流量offset1dsum(increase(credit_insight_spl_id_all_pv{}[1d]offset1d))by(url)#4号到5号一天的流量sum(increase(credit_insight_spl_id_all_pv{}[1d]offset2d))by(url)Prometheus内置时间段查询功能,并进行了优化。可以通过/api/v1/query_range接口查询,得到grpah:Prometheus查询瓶颈数据存储:索引数据有“Writesarevertical,readsarehorizo??ntal”(垂直写入,水平读取)mode:"Writesarevertical,readsarehorizo??ntal”是指tsdb通常以固定的时间间隔采集和写入指标,将最近所有时间序列的数据“垂直”写入,而读操作往往面向某一时间范围内的一个或多个时间序列。查询”horizo??ntally”跨越时间。每个metric取决于metric的个数。有labelAlabelBlabelC*...时间序列图。每个时间序列图(timeseries)中一个点的时间序列为[timestamp,value],对于example[1605607257,233].[timestamp-value]可以确定图上的一个点,将一个时间区间内的所有点连接起来形成时间序列图,因为Prometheus每隔15s采集一次数据,所以计时点的时间间隔为15s,即1分钟有60/15=4个计时点,1小时有4*60=240个计时点。Prometheus默认查询样本上限为5000w。因此,如果指标的时序图数量过多,则允许查询的时间间隔会相对较小。影响一次图表查询的时间序列个数的因素有3个,分别是:查询条件的时间序列个数(n)、查询的时间间隔(time)、每个时间序列点之间的间隔图表曲线(步骤)。以credit_insight_spl_id_all_pv指标为例,指标中共有n=163698个时间序列。如果step=15s,如果搜索过去time=60m的所有指标的时间序列图,那么搜索的样本数为163698*60*(60/15)=39287520,接近4kw。可以找到。但是如果搜索过去90m的数据,163698*90*4=58931280,5000多w,会发现数据请求异常:Errorexecutingquery:queryprocessingwouldloadtoomanysamplesintomemoryinqueryexecution所以,目测可以得到一个图中查询时序点个数的公式为:total=n*time/step,time和step的时间单位必须一致,总计不得超过5000w。反向扣,时间<5000w/n*步。要扩大搜索时间范围,请增加步长或减小n。step不变,减少n【指定label值减少搜索条件的结果数】:credit_insight_spl_id_all_pv{systemType="Android",systemVersion="10"},n=18955增加step为30s,n不变同:当然,一般正常情况下,我们的n值只有几百,步长基本都大于60s,所以一般情况下,我们可以查询两个多月的数据图。可视化平台:Grafanagrafana是一个开源的、高度可配置的数据图表分析、监控、告警平台,也是前端可视化产品。CustomChartsGrafana提供了多种内置的图表模板,具体有以下几种:当使用Prometheus作为数据源时,通常使用的图表类型绘制的时序图较多。对于一些基本的数据监控,这些图表类型足以满足我们的需求。但是对于复杂的需求,当这些类型不能满足我们的需求时,我们安装pannel插件来更新可用的图表类型,或者我们也可以根据官方文档开发自己的前端图表面板构建面板插件。图表配置在时序图配置场景中,我们需要重点关注以下配置:promQL:查询语句Legend:格式图例文本step/interval:采集点间隔,每隔一段时间采集一次数据。曲线的数据点数=图表持续时间/采样间隔。例如查看最近24小时的数据,采样间隔为5分钟,数据点数=24*60/5=288。采集间隔越短,采样率越大,图表数据量越大,曲线越平滑。采集间隔默认自动计算生成,也可以自定义。metrictimerange:每个点的数据统计时间间隔的长度。以QPS为例,图表上每个时间点的数据含义是:在这个时间点,过去n秒的访问次数。从上图可以看出,如果采样间隔>统计间隔时长:数据采样率<100%。无法收集的数据将被丢弃,并且不会显示在图表上。如果采样率太小,异常数据指标可能会出错。.若采样间隔==统计间隔长度,则采样率为100%。如果采样间隔小于统计间隔长度,数据将被重复统计,意义不大。自定义变量为了实现一些常用的过滤场景,grafana提供了可变函数变量配置:变量配置有多种方式(Type),可以自定义options,也可以根据prometheus指标的标签动态拉取。变量用法:变量通过$xxx形式引用。Alarms除了Prometheus本身可以配置告警表达式:grafana也可以配置告警:数据源Prometheus通常用于后端应用的指标数据的实时上报,主要用于异常告警和排查,所以数据是时间紧迫,我们不会去关注一个指标的异常波动预警,几个月前就已经检查修复了。但是,如果我们使用Prometheus做业务指标监控,那么我们可能会关注更长期的数据。例如,我们可能希望看到上个季度的环比增长。使用Prometheus作为数据源并不合适,因为Prometheus是时序数据库,更注重实时数据,数据量大。当前的数据存储时限仅为3个月。.那么这个时候,我们可能需要维护一个长期的统计数据,这个数据可能需要存储在mysql或者其他存储方式中。Grafana不是Prometheus的专属产品,同时也支持多种数据源,包括但不限于:常用数据库MySqlSQLServerPostgreSQLOracle日志、文档数据库LokiElasticsearch时序数据库PrometheusgraphiteopenTSDBInfluxDB链接跟踪JaegerZipkin...如果你没有需要的数据源配置,也可以安装RESTAPIDatasourcePlugin,作为数据源通过http接口查询,了解了grafana的高度可配置设计后,有几点值得思考:注意其设计思路.如果你想自己实现一个类似的可视化网络应用程序,你将如何设计?如果你想自己做一个高度可配置的功能,你应该如何设计呢?深入业务,比如我们常用的admin管理系统,一些常用的业务功能是否可以实现高度可配置?如何实现业务关联性强的配置与业务的有机结合?诸如此类,这些其实都值得思考。另外,Prometheus和grafana都有一些高级的玩法,有兴趣的可以去探索。参考文章Prometheus数据存储实现【理论】prometheustsdb存储和索引查询处理会在查询执行时将过多的样本加载到内存中