开源项目: pre_query: 给prometheus 重(heavy_query)查询提速

视频教程

项目地址

pre_query https://github.com/ning1875/pre_query

术语解释

什么是heavy_query

顾名思义 就是查询表现出来返回时间较长,对应调用服务端资源较多的查询
一般我们定义在1小时内的range_query 响应时间超过3秒则认为较重了

instance_query

当前点查询,用作报警查询或展示当前分布情况,对应调用的是prometheus 的/api/v1/query接口
image.png

range_query

查询一段时间的曲线,,对应调用的是prometheus 的/api/v1/query_range接口
image.png

series/datapoint

  • series代表不同label匹配到的项目,可以理解为单一一条曲线
  • datapoint 每个series中具体到某一时刻的值
  • 其余概念请看我之前的文章:prometheus 本地存储解析及其使用的那些"黑科技"

    heavy_query产生原因

  • 这就是一个典型的heavy_query
    image.png
  • 可以看到去掉histogram_quantile/rate等agg方法后查询一小时的metric传输数据达到12.8MB,可见数据量之大
    image.png
  • 查询instance_query可以看到命中了1.8w个series
    image.png

    prometheus range_query过程

    请看这篇文章,写的很清楚了详解Prometheus range query中的step参数

prometheus 查询limit限制参数

  • --storage.remote.read-sample-limit=5e7 remote_read时单一query的最大加载点数
  • --storage.remote.read-concurrent-limit remote_read并发query数目
  • --storage.remote.read-max-bytes-in-frame=1048576 remote_read时单一返回字节大小
  • --query.max-concurrency=20 prometheus 本身并发读请求
  • --query.max-samples=50000000 prometheus 单一query的最大加载点数

heavy_query原因总结

资源原因

  • 因为tsdb都有压缩算法对datapoint压缩,比如dod 和xor
  • 那么当查询时数据必然涉及到解压放大的问题
  • 比如压缩正常一个datapoint大小为16byte
  • 一个heavy_query加载1万个series,查询时间24小时,30秒一个点来算,所需要的内存大小为 439MB,所以同时多个heavy_query会将prometheus内存打爆,prometheus也加了上面一堆参数去限制
    image.png
  • 当然除了上面说的queryPreparation过程外,查询时还涉及sort和eval等也需要耗时

prometheus原生不支持downsample

  • 还有个原因是prometheus原生不支持downsample,所以无论grafana上面的step随时间如何变化,涉及到到查询都是将指定的block解压再按step切割
  • 所以查询时间跨度大对应消耗的cpu和内存就会报增,同时原始点的存储也浪费了,因为grafana的step会随时间跨度变大变大

    实时查询/聚合 VS 预查询/聚合

    prometheus的query都是实时查询的/聚合
    实时查询的优点很明显

  • 查询/聚合条件随意组合,比如 rate后再sum然后再叠加一个histogram_quantile

实时查询的缺点也很明显

  • 那就是慢,或者说资源消耗大
    实时查询的优缺点反过来就是预查询/聚合的
    一个预聚合的例子请看我写的falcon组件 监控聚合器系列之: open-falcon新聚合器polymetric
  • 所有的聚合方法提前定义好,并定时被计算出结果
  • 查询时不涉及任何的聚合,直接查询结果
  • 比如实时聚合需要每次加载10万个series,预聚合则只需要查询几个结果集
    那么问题来了prometheus有没有预查询/聚合呢
    答案是有的

    prometheus的预查询/聚合

    prometheus record

Recording rules allow you to precompute frequently needed or computationally expensive expressions and save their result as a new set of time series. Querying the precomputed result will then often be much faster than executing the original expression every time it is needed. This is especially useful for dashboards, which need to query the same expression repeatedly every time they refresh.

原理分析

record配置样例

groups:
- name: my_record
  interval: 30s
  rules:
  - record: hke:heavy_expr:0211d8a2fcdefee8e626c86ba3916281
    expr: sum(delta(kafka_topic_partition_current_offset{instance=~'1.1.1.1:9308', topic=~".+"}[5m])/5) by (topic)
  • 查看代码我们知道,prometheus把record记录当做和alert一样处理
  • 进行instance_query查询当前点,如果是alert则走报警的流程
  • 如果是record,那么将查询到的结果做tsdb的add app.Add(s.Metric, s.T, s.V),新的metric_name使用配置中设置的,同时保留原有结果的label
  • E:\go_path\src\github.com\prometheus\prometheus\rules\manager.go

    // Eval runs a single evaluation cycle in which all rules are evaluated sequentially.  
    func (g *Group) Eval(ctx context.Context, ts time.Time) {  
     for i, rule := range g.rules {  
        select {  
        case <-g.done:  
           return  
     default:  
        }  
    
        func(i int, rule Rule) {  
           sp, ctx := opentracing.StartSpanFromContext(ctx, "rule")  
           sp.SetTag("name", rule.Name())  
           defer func(t time.Time) {  
              sp.Finish()  
    
              since := time.Since(t)  
              g.metrics.evalDuration.Observe(since.Seconds())  
              rule.SetEvaluationDuration(since)  
              rule.SetEvaluationTimestamp(t)  
           }(time.Now())  
    
           g.metrics.evalTotal.WithLabelValues(groupKey(g.File(), g.Name())).Inc()  
    
           vector, err := rule.Eval(ctx, ts, g.opts.QueryFunc, g.opts.ExternalURL)  
           if err != nil {  
              // Canceled queries are intentional termination of queries. This normally  
     // happens on shutdown and thus we skip logging of any errors here. if _, ok := err.(promql.ErrQueryCanceled); !ok {  
                 level.Warn(g.logger).Log("msg", "Evaluating rule failed", "rule", rule, "err", err)  
              }  
              g.metrics.evalFailures.WithLabelValues(groupKey(g.File(), g.Name())).Inc()  
              return  
     }  
    
           if ar, ok := rule.(*AlertingRule); ok {  
              ar.sendAlerts(ctx, ts, g.opts.ResendDelay, g.interval, g.opts.NotifyFunc)  
           }  
           var (  
              numOutOfOrder = 0  
     numDuplicates = 0  
     )  
    
           app := g.opts.Appendable.Appender()  
           seriesReturned := make(map[string]labels.Labels, len(g.seriesInPreviousEval[i]))  
           defer func() {  
              if err := app.Commit(); err != nil {  
                 level.Warn(g.logger).Log("msg", "Rule sample appending failed", "err", err)  
                 return  
     }  
              g.seriesInPreviousEval[i] = seriesReturned  
           }()  
           for _, s := range vector {  
              if _, err := app.Add(s.Metric, s.T, s.V); err != nil {  
                 switch errors.Cause(err) {  
                 case storage.ErrOutOfOrderSample:  
                    numOutOfOrder++  
                    level.Debug(g.logger).Log("msg", "Rule evaluation result discarded", "err", err, "sample", s)  
                 case storage.ErrDuplicateSampleForTimestamp:  
                    numDuplicates++  
                    level.Debug(g.logger).Log("msg", "Rule evaluation result discarded", "err", err, "sample", s)  
                 default:  
                    level.Warn(g.logger).Log("msg", "Rule evaluation result discarded", "err", err, "sample", s)  
                 }  
              } else {  
                 seriesReturned[s.Metric.String()] = s.Metric  
              }  
           }

    pre_query项目介绍

    效果图 heavy_query时间对比.png

    image

    解决方案说明

  • heavy_query对用户侧表现为查询速度慢
  • 在服务端会导致资源占用过多甚至打挂后端存储
  • 查询如果命中heavy_query策略(目前为查询返回时间超过2秒)则会被替换为预先计算好的轻量查询结果返回,两种方式查询的结果一致
  • 未命中的查询按原始查询返回
  • 替换后的metrics_name 会变成 hke:heavy_expr:xxxx 字样,而对应的tag不变。对于大分部panel中已经设置了曲线的Legend,所以展示没有区别
  • 现在每晚23:30增量更新heavy_query策略。对于大部分设定好的dashboard没有影响(因为已经存量heavy_query已经跑7天以上了),对于新增策略会从策略生效后开始展示数据,对于查询高峰的白天来说至少保证有10+小时的数据

    代码架构说明

  • parse组件根据prometheus的query log分析heavy_query记录
  • 把记录算哈希后增量写入consul,和redis集群中
  • prometheus 根据confd拉取属于自己分片的consul数据生成record.yml
  • 根据record做预查询聚合写入tsdb
  • query前面的lua会将grafana传过来的查询expr算哈希
  • 和redis中的记录匹配,匹配中说明这条是heavy_query
  • 那么替换其expr到后端查询
  • 下面是架构图
    image.png

使用指南

prometheus 和confd组件

# 安装prometheus 和confd  
# 将confd下的配置文件放置好,启动服务  
# prometheus开启query_log   
  
global:  
  query_log_file: /App/logs/prometheus_query.log  

openresty和lua组件

#1. 安装openresty ,准备lua环境  
yum install yum-utils -y  
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo  
yum install openresty openresty-resty -y  
  
#2.  
# 修改lua文件中的redis地址为你自己的  
# 修改ngx_prome_redirect.conf文件中 真实real_prometheus后端,使用前请修改  
  
mkdir -pv /usr/local/openresty/nginx/conf/conf.d/  
mkdir -pv /usr/local/openresty/nginx/lua_files/  
  
#3.  
# 将nginx配置和lua文件放到指定目录  
/bin/cp -f  ngx_prome_redirect.conf /usr/local/openresty/nginx/conf/conf.d/  
/bin/cp -f  nginx.conf /usr/local/openresty/nginx/conf/  
/bin/cp -f prome_redirect.lua /usr/local/openresty/nginx/lua_files/  
  
  
#4.  
# 启动openresty  
systemctl enable openresty  
systemctl start openresty  
#5.  
# 修改grafana数据源,将原来的指向真实prometheus地址改为指向openresty的9992端口  
  

parse和ansible组件 在python3.6+中运行

# 安装依赖  
pip3 install -r  requirements.txt  
  
# 修改config.yaml中各个配置  
# 准备真实prometheus地址写入all_prome_query  
# 运行添加crontab 每晚11:30定时运行一次即可  
ansible-playbook -i all_prome_query  prome_heavy_expr_parse.yaml  

运维指南

# 查看redis中的heavy_query记录  
redis-cli -h $redis_host   keys hke:heavy_expr*  
# 查看consul中的heavy_query记录  
curl http://$consul_addr:8500/v1/kv/prometheus/record?recurse= |python -m json.tool  
# 根据一个heavy_record文件恢复记录  
python3 recovery_by_local_yaml.py local_record_yml/record_to_keep.yml  
# 根据一个metric_name前缀删除record记录  
bash -x recovery_heavy_metrics.sh  $metric_name  

监控系统和运维开发
监控系统的源码解析,运维开发经验交流
158 声望
55 粉丝
0 条评论
推荐阅读
k8s默认调度器关于pod申请资源过滤的源码细节
思考 Q1 k8s的默认调度器是在哪个环节过滤满足这个pod资源的节点的?如果问你是否了解k8s的调度原理,大家估计都会滔滔不绝说一通但是是否真正的了解其中的细节估计就不好说了下面是我阅读k8s调度器的源码分析的...

ning1875阅读 1k

不对全文内容进行索引的Loki到底优秀在哪里,可以占据一部分日志监控领域
k8s零基础入门运维课程k8s零基础入门运维课程,计算存储网络和常见的集群相关操作k8s纯源码解读教程(3个课程内容合成一个大课程)k8s底层原理和源码讲解之精华篇k8s底层原理和源码讲解之进阶篇k8s纯源码解读课程,...

ning1875阅读 8.3k评论 1

prometheus指南:采集k8s的原理和高可用存储实践
k8s零基础入门运维课程k8s零基础入门运维课程,计算存储网络和常见的集群相关操作k8s纯源码解读教程(3个课程内容合成一个大课程)k8s底层原理和源码讲解之精华篇k8s底层原理和源码讲解之进阶篇k8s纯源码解读课程,...

ning1875阅读 2.6k评论 1

李峋同款会动的爱心Python代码版
最近看到不少关于李峋同款爱心的视频、文章,今天我们也分享一下李峋同款爱心 Python 代码版。要问李峋是谁?我也不太清楚,大家可自行百度,这个是我百度的结果,仅供参考。

Python小二阅读 1.2k评论 1

Prometheus的使用
在Prometheus的架构设计中,Prometheus Server 并不直接服务监控特定的目标,其主要任务负责数据的收集,存储并且对外提供数据查询支持。因此为了能够能够监控到某些东西,如主机的CPU使用率,我们需要使用到Expo...

代码的路2阅读 252

Prometheus 性能调优-水平分片
简介之前笔者有连续 2 篇文章:Prometheus 性能调优 - 什么是高基数问题以及如何解决?如何精简 Prometheus 的指标和存储占用陆续介绍了一些 Prometheus 的性能调优技巧,包括高基数问题的解决以及精简 Prometheu...

东风微鸣云原生阅读 964

封面图
自定义数据采集export到prometheus使用 Flask实现
如图 想要取到 url get请求的值,使用prometheus blackbox 无法获取,所以考虑使用flask自定义exporter 获取

台湾省委书记阅读 795

封面图
158 声望
55 粉丝
宣传栏