2

视频教程

项目地址

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  

ning1875
167 声望67 粉丝

k8s/prometheus/cicd运维开发专家,想进阶的dy搜 小乙运维杂货铺