本文发布于Cylon的收藏册,转载请著名原文链接~


背景

对于整个 Kubernetes 集群来说,随着业务不断地打磨,新增指标,那么对于 Prometheus 特性来说,那么内存 与 存储的使用势必是增加。这是对于存储压力是很重的,通常情况下,使用 Prometheus,都会是用于 Kubernetes 集群中,而 应用于 Kubernetes 集中的存储势必是 PVC 之类的网络存储。

这种场景中,我将尝试拆解如何分析和配置 Prometheus 以显著的减少其资源使用并解决高基数问题

高基数

基数 (cardinality) 通俗来说是一个集合中的元素数量 [1] 基数的来源通常为:

  • label 的数量
  • series(指标) 的数量
  • 时间:label 或者 series 随时间而流失或增加,通常是增加

那么这么看来高基数就是,label, series, 时间这三个集合的笛卡尔积,那么高基数的情况就很正常了。

而高基数带来的则是 Prometheus 资源使用,以及监控的性能。下图是 Grafana Lab 提到的一张图,很好的阐述了高基数这个问题

image-20220704002227865

图:Prometheus中的基数
Source:https://grafana.com/blog/2022/02/15/what-are-cardinality-spikes-and-why-do-they-matter

如图所示:一个指标 server_responses 他的 label 存在两个 status_codeenvironment ,这代表了一个集合,那他的 label value 是 1~5xx,这个指标的笛卡尔积就是10。

那么此时存在一个问题,如何能定位 基数高不高,Grafana Lab 给出了下面的数据 [1],但是我不清楚具体的来源或者如何得到的这些值。也就是 label:value

  • 低基数:1: 5
  • 标准基数:1: 80
  • 高基数:1: 10000

为什么指标会指数级增长

在以 Kubernetes 为基础的架构中,随着抽象级别的提高(通常为Pod, Label, 以及更多抽象的拓扑),指标的时间序列也越来越多。因为在这种基础架构中,在传统架构中运行的一个应用的单个裸机,被许多运行分散在许多不同节点上的许多不同微服务的 Pod 所取代。在这些抽象层中的每一个都需要一个标签,以便可以唯一地标识它们,并且这些组件中的每一个都会生成自己的指标,从而创建其独特的时间序列集。

此外,在 Kubernetes 中的工作负载的短暂性最终也会创建更多的时间序列。例如 JAVA的 http_request_duration_seconds_bucket 指标,它会每次 pod 更改状态时生成一个新的时间序列,比如从“状态200" 或者 “状态 404” 在到 “每个URL” 再到 “每个请求的时间”,这样大量短时间请求,对一个 Pod 状态可能会生成大量指标。

这是就要考虑到 Prometheus 兼容的格式,而非传统监控的监控指标的格式问题,就例如上面的例子,通过对 URI,请求时长,请求状态码几个维度去监控,那么此时的 exporter 导出的数据势必是非常杂乱的,而这种可能相同的指标就会放大到无穷。

在这种环境中的 Label,就是两组集合的笛卡尔积的选择,就是次优标签 sub-optimal labels ,对付这类高基数的指标,控制基数,以及如何避免使用这类错误,就是解决高基数的根本。

高基数是一个非常重要的问题

高基数的问题,带来的就是基于 Prometheus 的监控带来的是更多的可观测性,反之,随着时间序列的基数增加,那么为了维持某几个特别的指标的观测性,就必须要付出更多的硬件资源,以及影响本身监控系统的性能。比较明显的表现,就是监控的相应下降,极大的拖慢了整个系统的运行速度(包含仪表盘,promQL等)。还会延长系统故障排除时的MTTR (Mean Time to Repair)。

Notes: 其实这里还有一类型错误,就是这会导致时间序列的乱序,怎么说呢,就是当指标无线放大时,在某一个点 scrap 的指标存储时间,大于了抓取周期,导致新指标存储早于旧指标,这种很容易出现在例如 Prometheus 的从内存到存储的那个点。

如何控制控制指标的高基数增长

指标的无序扩张(高基数)是不可避免对监控系统产生非常大的影响(存储和性能),而为此引出了一个如何优化不断增长的指标就是控制高基数增长的关键部分,下面将从几个维度来阐释控制“高基数”问题的步骤

第一步:高基数指标是否有价值?

在任何优化方法的第一步都是去了解哪些指标给系统带来负面影响(这里指高基数),并且还需要确定这些指标中哪些指标是有价值的;所谓的有价值既,在仪表板、告警中是否有被使用。

基于这些信息,我将根据基数问题与监控指标的价值分为四个象限:

  • 高价值,低成本:闲置、陈旧、很长时间没有新数据的
  • 低价值,低成本:基本上没有什么影响,但是需要去考虑优化
  • 低价值,高成本:可以考虑删除掉 Label 和 metric
  • 高价值,高成本:你的指标是否过细化,是否需要重新设计 Label 或者聚合数据;或这类指标是否适合使用 Prometheus 这类时间序列

第二步:如何确定高基数指标

确定高基数指标包含3种方式

  • Prometheus WEB UI 分析,2.14 版本之后
  • PromQL 分析
  • Prometheus API 分析

通常情况下,WEB UI 就可以满足需求了,通过路径 Prometheus UI -> Status -> TSDB Status -> Head Cardinality Stats。

image-20230617175638751

图:Prometheus WEB UI TOP 10 series

查看上图可见,http_server_requests_seconds_bucket 的信息,就这个 bucket 指标占总指标的 25% 左右,在加上其他的 几个 bucket,10个基本上占用了60%;在这里体现的高基数问题的指标,通常都是以 bucket 结尾的指标,而这些指标通常包含2个维度,会无线拉长成为高基数指标。如下面指标所示,通常由 le (标识每个 bucket 的上限,这可以确保可以定位到在一个时间范围内相应的请求指标有哪些)

http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/map",le="+Inf",} 71.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.001",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.001048576",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.001398101",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.001747626",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.002097151",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.002446676",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.002796201",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.003145726",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.003495251",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.003844776",} 0.0
http_server_requests_seconds_bucket{application="map-common-api",exception="None",method="POST",outcome="SUCCESS",status="200",uri="/api/v1/maplist",le="0.004194304",} 0.0

例如下面是生产环境中的一个高基数TOP10

DC01 Top 10 series count by metric names

Name Count
http_server_requests_seconds_bucket 1282158
lettuce_command_completion_seconds_bucket 782680
lettuce_command_firstresponse_seconds_bucket 782680
nginx_ingress_controller_response_size_bucket 99840
nginx_ingress_controller_request_duration_seconds_bucket 99840
http_server_requests_seconds 92695
nginx_ingress_controller_request_size_bucket 91520
nginx_ingress_controller_response_duration_seconds_bucket 74580
nginx_ingress_controller_bytes_sent_bucket 66560
node_ipvs_backend_weight 51173

DC02 Top 10 series count by metric names

Name Count
http_server_requests_seconds_bucket 1073571
lettuce_command_firstresponse_seconds_bucket 755650
lettuce_command_completion_seconds_bucket 755650
http_server_requests_seconds 77775
nginx_ingress_controller_request_duration_seconds_bucket 67440
nginx_ingress_controller_response_size_bucket 67440
nginx_ingress_controller_request_size_bucket 61820
nginx_ingress_controller_response_duration_seconds_bucket 53052
node_ipvs_backend_connections_inactive 48796
node_ipvs_backend_connections_active 48796

至此可以看到实际上 http_server_requests_seconds_bucket 这一个指标占据了 prometheus 总指标的50%+;这种指标就会存在多个维度的扩张,URI, outcome, status,uri,le;假设我们有 100个 接口,

  • 在什么都不做的情况下,就多出了100个 series
  • 如果状态码是存在变数的,假设为5,此时series 为500
  • 在基于 outcome 的数量,此时一个指标的基数已经为1000
  • 那么 le 将无限放大这个 metric 到 无穷

这样就可以定位,影响到 prometheus 存储于性能最大点在哪里,那么此时就需要考虑 le=“0.001048576” 和 le=“0.001” 有什么区别,以及此类的指标是否有必要存在?

  • 基于我们现有的监控报表来看
  • 结合业务日志将访问信息的监控从 Prometheus 提到 ELK 这种日志层面监控
  • 再通过 将 kube-apiserver 的 bucket 以及负载均衡在线/不在线Pod 这两个 指标清空,基本上可以满足70%左右的监控项删减 通常会删掉 le 这个标签,而不是 http_server_requests_seconds_bucket 的数据,但是也需要考虑,http_server_requests_seconds_bucket 指标是否有用到,如果没有用到, 又是高基数,那么可以删除

而其他的一些分析,可以很有效的定位到你需要优化的标签

  • Top 10 label names with high memory usage
  • Top 10 series count by label value pairs

通过 promQL 定位 job

  • 查询 top 10 的 series topk(10, count by (__name__)({__name__=~".+"}))
  • sum(scrape_series_added) by (job) 通过 job Label 分析 series 增长
  • sum(scrape_samples_scraped) by (job) 通过 job Label 分析 series 总量

可以通过指标属于哪个 job

第三步:发现那些指标没有在使用

Grafana Mimirtool 是一个开源的命令行工具, 它可以识别 Mimir、Prometheus 或 Prometheus 的存储中未在Dashboard、Alert 或 recording 中使用的指标。通过 Mimirtool 可以快速发现未使用的指标,并且做出操作

优化监控指标

优化监控指标来解决高基数问题主要从以下维度进行

增加采集间隔

Prometheus 的默认值为 scrape_interval: 15s,或 DPM (Data points Per minute) 4个,但是如果查询语句为 scrape_samples_scraped[1m] 那么可以考虑将这个 job 的 scrape_interval 增加为1m,这样15~60 可以减少近75%的存储成本。

kube-state-metrics:
  namespaceOverride: ""
  rbac:
    create: true
  releaseLabel: true
  prometheus:
    monitor:
      enabled: true

      ## Scrape interval. If not set, the Prometheus default scrape interval is used.
      ##
      interval: ""

优化 histogram

histrogram 是 Prometheus中一种更具有更复杂类型的监控指标,通常用于决定数据的精度,典型的例子就是上面提到的 http_server_requests_seconds_bucket 中的 le ,此时假设 le 代表请求毫秒,那么我们只需要决定你所需要的精度是哪些?例如,如果仅仅需要 1ms, 5ms, 10ms,那么指标 le 标签就控制为3,这样结合 URI 指标,那么这个 histogram 是有限的

# drop all metric series ending with _bucket and where le="0.1xxx"
- source_labels: [__name__, le]
  separator: _
  regex: ".+_bucket_(0.1+)"
  action: "drop"
  
# Object labels:
__name__: http_server_requests_seconds_bucket
le: 0.114421

这里可以通过 promlabs 来测试你的规则是否是成功的 [2]

删除不需要的标签

对于一些指标,删除了未使用的标签后,反而会使这个指标变得没有意义,并且使这个指标变得序列重复,这个时候可以完整删除这个指标

例如在下面的示例中,第一个示例可以安全地删除 ip 标签,因为其余系列都是唯一的。但在第二个示例中,如果删除 ip 标签将产生重复的时间序列,Prometheus 将删除这些时间序列。my_metric_total在此示例中,Prometheus 将接收具有相同时间戳的值 1、3 和 7,并将丢弃其中的 2 个数据点。

# You can drop ip label, remaining series are still unique
my_metric_total{env=“dev”, ip=“1.1.1.1"} 12
my_metric_total{env=“tst”, ip=“1.1.1.1"} 14
my_metric_total{env=“prd”, ip=“1.1.1.1"} 18

#Remaining values after dropping ip label
my_metric_total{env=“dev”} 12
my_metric_total{env=“tst”} 14
my_metric_total{env=“prd”} 18

# You can not drop ip label, remaining series are not unique
my_metric_total{env=“dev”, ip=“1.1.1.1"} 1
my_metric_total{env=“dev”, ip=“3.3.3.3"} 3
my_metric_total{env=“dev”, ip=“5.5.5.5"} 7

#Remaining values after dropping ip label are not unique
my_metric_total{env=“dev”} 1
my_metric_total{env=“dev”} 3
my_metric_total{env=“dev”} 7

如果无法控制删除标签将导致重复序列,通过 Prometheus sum、avg、min、max等函数可以保留聚合数据,同时删除单个系列。在下面的示例中,我们使用 sum 函数来存储聚合指标,从而允许我们删除单个时间序列。

# sum by env
my_metric_total{env="dev", ip="1.1.1.1"} 1
my_metric_total{env="dev", ip="3.3.3.3"} 3
my_metric_total{env="dev", ip="5.5.5.5"} 7

# Recording rule
sum by(env) (my_metric_total{})

my_metric_total{env="dev"} 11

使用聚合组

例如对于 *_seconds_bucket 类的指标, 通常需要的是一些高纬度的指标,那么这些指标可以通过 recording rules 进行记录和存储

groups:
  - interval: 3m
    name: kube-apiserver-availability.rules
    rules:
      - expr: >-
          avg_over_time(code_verb:apiserver_request_total:increase1h[30d]) *
          24 * 30
        record: code_verb:apiserver_request_total:increase30d
      - expr: >-
          sum by (cluster, code, verb)
          (increase(apiserver_request_total{job="apiserver",verb=~"LIST|GET|POST|PUT|PATCH|DELETE",code=~"2.."}[1h]))
        record: code_verb:apiserver_request_total:increase1h
      - expr: >-
          sum by (cluster, code, verb)
          (increase(apiserver_request_total{job="apiserver",verb=~"LIST|GET|POST|PUT|PATCH|DELETE",code=~"5.."}[1h]))
        record: code_verb:apiserver_request_total:increase1h

最后 drop 掉指标

write_relabel_configs:
  - source_labels: [__name__]
    regex: "apiserver_request_duration_seconds_bucket"
    action: drop

recording rules 是允许预先将经常计算的表达式的结果保存为一组新的时间序列的,这种情况下查询的成本会比每次直接查询原始的表达式要快许多,并且在聚合后,可以将原来的指标删掉

优化后每个块的大小

image-20240915210841954

优化后的每个块的大小

image-20240915210918296

Reference

[1] What are cardinality spikes and why do they matter?

[2] How to manage high cardinality metrics in Prometheus and Kubernetes

[3] 精简Prometheus指标减少资源占用

[4] What are cardinality spikes and why do they matter?

[5] Containing your Cardinality

本文发布于Cylon的收藏册,转载请著名原文链接~

链接:https://www.oomkill.com/2023/06/impove-prometheus-performance/

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」 许可协议进行许可。