Prometheus是一个开源系统监控和告警工具包,于2016年加入云原生计算基金会,成为继Kubernetes之后的第二个托管项目。 在以前的文章《深入理解prometheus:如何为表达式抽取和注入Labels》中介绍了如何修改PromQL的labels,但一直没有介绍有什么用途,本篇文章将结合告警信息来一起了解下如何实现在告警时附带指标趋势图,以便能更好的定位告警原因和发生时间。在告警时附带指标趋势图的难点 在Prometheus中提供了三种查看指标出图的方式,分别是EXPRESSIONBROWSERGrafanaConsoletemplates 我们通常会更推荐使用Grafana,拥有EXPRESSIONBROWSER的所有能力,同时还支持令人映像深刻的出图效果和友好的使用体验。 node内存使用指标趋势图 Prometheus支持基于PromQL的告警规则,通常如下所示groups:name:examplerules:alert:NodeHighMemoryUsedexpr:nodememoryusedpercent{jobmyjob}0。8for:10mlabels:severity:warnannotations:summary:nodehighmemoryused 其中最重要的就是expr字段,它的值是一个表达式PromQL表达式,如果表达式的结果为true,也即能过滤到数据,那么就会对每个时序metric生成一条告警,默认的告警信息中通常会包含labels和annotations内地信息,这些信息通常是预置的文本和文本模版(annotations内支持将labels作为变量的模版,最后告警时会渲染为实际的值,最后也是文本) 基于告警文本的消息只能获得告警当时的状态信息,告警只是第一步,如果要定位告警原因,通常需要告警发生前的历史累积状态信息,那就需要针对每个具体告警,生成对应的专有指标趋势图。 默认的告警表达式,如上图所示,会展示所有符合条件的数据,但有两个问题:每个告警其实只需要自己的趋势图,不需要展示别的对象的趋势图希望展示完整的趋势图,更便于发现趋势问题 如下图所示,只针对某个告警对象,且既有告警趋势,也有基线,这样的图更加方便定位。 通常我们的规则都是形式如下:nodecpuusage0rate(nodecputotal{noden1}〔1m〕)rate(nodecputotal{noden2}〔1m〕)containercpulimitusageavgovertime(containercpulimitusage〔1d〕offset1d)1。01containercpulimitusage0andcontainermemorylimitusage0containercpulimitusage0。5andcontainermemorylimitusage0。5orcontainercpulimitusage0。8sum(rate(apiserverrequestdurationsecondssum{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))without(instance,pod)sum(rate(apiserverrequestdurationsecondscount{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))without(instance,pod) 这些规则的基本规律就是基于metricname和labels筛选数据,并做一些函数处理,最后与某个阈值行比较,在PromQL中,比较操作其实会被转换为过滤操作,如果能过滤出符合的数据,就触发告警。 如果要查明白为啥告警,首先需要对每个表达式进行拆分,然后分别查询每个拆分后的表达式,看看到底是哪个数据有问题,同时也需要看看每个表达式在数据异常前后的一些情况,也就需要比告警的时间范围大一些。 另外一点是,通常告警规则是适用于一类对象的,比如都是node的cpu告警,或者内存告警,但实际告警时是会给出每个具体的node信息,其他告警也类似,如果要查询告警数据,也只需要查发生告警的对象的指标数据,其他对象不需要查。 为了实现上述的能力,通常需要解决两个问题:将Prometheusalertrules拆分成多个查询表达式在Prometheusalertrules中增加本次告警专属的用于区分告警对象的labels解决思路 上述两个功能,在prometheus中并没有提供直接可用的能力,但可以根据代码分析出可以使用和借鉴的能力。 在以前的文章《深入理解prometheus:如何为表达式抽取和注入Labels》中,我们先分析了Prometheus支持的每种类型和基本的四种类型,同时也说明了PromQL是一种嵌套函数语言,最后也介绍了如何增加和抽取labels的方法,也给出了基本代码,问题2自然就解了。funcInjectSelectors(exprExpr,selectors〔〕labels。Matcher)error{Inspect(expr,func(nodeNode,〔〕Node)error{vs,ok:node。(VectorSelector)ifok{vs。LabelMatchersappend(vs。LabelMatchers,selectors。。。)}returnnil})returnnil} 对于问题1,在上述文章的分析过程中,也发现了对应的解决办法,但当时没有意识到,直到后续debug一步一步测试中才发现规律,同时也加深了对PromQL设计的理解。 PromQL是一门函数嵌套语言,同时也支持基本的算数和逻辑运算,既然是这样,那么我们就可以像解数学计算题一样,一步一步的解,而且组成PromQL表达式的元素的固定的,有11种类型(详情见上面引用的文章,这里不再赘述),每个表达式都可以拆解为这些Type的实例的组合。 对于我们的告警表达式来说,绝大多数都是左右两个数据进行算数或者逻辑运算,这种都是BinaryExpr类型,也即由更下层的类型组合成的新的表达式类型。我们可以直接识别BinaryExpr类型,如果是BinaryExpr,那就进行拆分,就得到两个表达式用于绘图,如果不是,跳过即可。实战验证 拆分测试代码:funcTestSplitQuery(ttesting。T){testExprs:〔〕string{nodecpuusage0,rate(nodecputotal{noden1}〔1m〕)rate(nodecputotal{noden2}〔1m〕),containercpulimitusageavgovertime(containercpulimitusage〔1d〕offset1d)1。01,containercpulimitusage0andcontainermemorylimitusage0,containercpulimitusage0。5andcontainermemorylimitusage0。5orcontainercpulimitusage0。8,sum(rate(apiserverrequestdurationsecondssum{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))without(instance,pod)sum(rate(apiserverrequestdurationsecondscount{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))without(instance,pod),}for,qs:rangetestExprs{expr,err:parser。ParseExpr(string(qs))iferr!nil{t。Fatal(err)}parser。Inspect(expr,func(nodeparser。Node,〔〕parser。Node)error{switchT:node。(type){caseparser。VectorSelector:t。Log(VectorSelector,T。String())caseparser。BinaryExpr:t。Log(BinaryExpr,T。String(),T。Op。String(),T。LHS。String(),T。RHS。String())returnnil}returnnil})}} 结果:Runningtool:usrlocalgobingotesttimeout30srunTestSplitQueryxxxpkgxxxvRUNTestSplitQueryxxxpkgxxxtest。go:77:BinaryExprnodecpuusage0nodecpuusage0xxxpkgxxxtest。go:75:VectorSelectornodecpuusagexxxpkgxxxtest。go:77:BinaryExprrate(nodecputotal{noden1}〔1m〕)rate(nodecputotal{noden2}〔1m〕)rate(nodecputotal{noden1}〔1m〕)rate(nodecputotal{noden2}〔1m〕)xxxpkgxxxtest。go:75:VectorSelectornodecputotal{noden1}xxxpkgxxxtest。go:75:VectorSelectornodecputotal{noden2}xxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusageavgovertime(containercpulimitusage〔1d〕offset1d)1。01containercpulimitusageavgovertime(containercpulimitusage〔1d〕offset1d)1。01xxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusageavgovertime(containercpulimitusage〔1d〕offset1d)containercpulimitusageavgovertime(containercpulimitusage〔1d〕offset1d)xxxpkgxxxtest。go:75:VectorSelectorcontainercpulimitusagexxxpkgxxxtest。go:75:VectorSelectorcontainercpulimitusageoffset1dxxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusage0andcontainermemorylimitusage0andcontainercpulimitusage0containermemorylimitusage0xxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusage0containercpulimitusage0xxxpkgxxxtest。go:75:VectorSelectorcontainercpulimitusagexxxpkgxxxtest。go:77:BinaryExprcontainermemorylimitusage0containermemorylimitusage0xxxpkgxxxtest。go:75:VectorSelectorcontainermemorylimitusagexxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusage0。5andcontainermemorylimitusage0。5orcontainercpulimitusage0。8orcontainercpulimitusage0。5andcontainermemorylimitusage0。5containercpulimitusage0。8xxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusage0。5andcontainermemorylimitusage0。5andcontainercpulimitusage0。5containermemorylimitusage0。5xxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusage0。5containercpulimitusage0。5xxxpkgxxxtest。go:75:VectorSelectorcontainercpulimitusagexxxpkgxxxtest。go:77:BinaryExprcontainermemorylimitusage0。5containermemorylimitusage0。5xxxpkgxxxtest。go:75:VectorSelectorcontainermemorylimitusagexxxpkgxxxtest。go:77:BinaryExprcontainercpulimitusage0。8containercpulimitusage0。8xxxpkgxxxtest。go:75:VectorSelectorcontainercpulimitusagexxxpkgxxxtest。go:77:BinaryExprsumwithout(instance,pod)(rate(apiserverrequestdurationsecondssum{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))sumwithout(instance,pod)(rate(apiserverrequestdurationsecondscount{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))sumwithout(instance,pod)(rate(apiserverrequestdurationsecondssum{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))sumwithout(instance,pod)(rate(apiserverrequestdurationsecondscount{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}〔5m〕))xxxpkgxxxtest。go:75:VectorSelectorapiserverrequestdurationsecondssum{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}xxxpkgxxxtest。go:75:VectorSelectorapiserverrequestdurationsecondscount{subresource!log,verb!LISTWATCHWATCHLISTDELETECOLLECTIONPROXYCONNECT}PASS:TestSplitQuery(0。00s)PASSokxxxpkgxxx0。025s总结 本文主要探讨如何在告警信息中,加入拆分后的、专属告警本身的指标趋势图,来更加方便的定位告警原因。主要解决两个问题:如何拆分告警表达式和对表达式加入新的labelmatchers。在文章《深入理解prometheus:如何为表达式抽取和注入Labels》中我们给出了如何对已有表达式加入新的labelmatchers的详细分析和代码;而本篇继续深入分析,并给出了如何拆分告警表达式的详细分析和代码示例;这样结合两者我们就能完整的实现我们的需求了。