文章目录
Prometheus
Prometheus:不仅是一款时间序列数据库,在整个生态上还是一套完整的监控系统,可以用作对k8s的资源进行监控。
特点:
- 通过PromQL实现多维度数据模型的灵活查询。
- 定义了开放指标数据的标准,自定义探针(如Exporter等),编写简单方便。
- PushGateway组件让这款监控系统可以接收监控数据。
- 提供了VM和容器化的版本。
尤其是第一点,这是很多监控系统望尘莫及的。多维度的数据模型和灵活的查询方式,使监控指标可以关联到多个标签,并对时间序列进行切片和切块,以支持各种图形、表格和告警场景。
Prometheus github 地址:https://github.com/coreos/kube-prometheus
架构

Prometheus主要由Prometheus Server、Pushgateway、Job/Exporter、Service Discovery、Alertmanager、Dashboard这6个核心模块构成。
Prometheus通过Service Discovery(服务发现机制)发现target,这些目标可以是长时间执行的Job,也可以是短时间执行的Job,还可以是通过Exporter监控的第三方应用程序。被抓取的数据会存储起来,通过PromQL语句在dashboard等可视化系统中供查询,或者向Alertmanager发送告警信息,告警会通过页面、电子邮件、钉钉信息或者其他形式呈现。
从上述架构图中可以看到,Prometheus不仅是一款时间序列数据库,在整个生态上还是一套完整的监控系统。对于时间序列数据库,在进行技术选型的时候,往往需要从宽列模型存储、类SQL查询支持、水平扩容、读写分离、高性能等角度进行分析。而监控系统的架构,往往还需要考虑通过减少组件、服务来降低成本和复杂性以及水平扩容等因素。
监控系统使用MQ通信的问题
很多企业自己研发的监控系统中往往会使用消息队列Kafka和Metrics parser、Metrics process server等Metrics解析处理模块,再辅以Spark等流式处理方式。应用程序将Metric推到消息队列(如Kafaka),然后经过Exposer中转,再被Prometheus拉取。之所以会产生这种方案,是因为考虑到有历史包袱、复用现有组件、通过MQ(消息队列)来提高扩展性等因素。这个方案会有如下几个问题。
- 增加了查询组件,比如基础的sum、count、average函数都需要额外进行计算。这一方面多了一层依赖,在查询模块连接失败的情况下会多提供一层故障风险;另一方面,很多基本的查询功能的实现都需要消耗资源。而在Prometheus的架构里,上述这些功能都是得到支持的。
- 抓取时间可能会不同步,延迟的数据将会被标记为陈旧数据。如果通过添加时间戳来标识数据,就会失去对陈旧数据的处理逻辑。
- Prometheus适用于监控大量小目标的场景,而不是监控一个大目标,如果将所有数据都放在Exposer中,那么Prometheus的单个Job拉取就会成为CPU的瓶颈。这个架构设计和Pushgateway类似,因此如果不是特别必要的场景,官方都不建议使用。
- 缺少服务发现和拉取控制机制,Prometheus只能识别Exposer模块,不知道具体是哪些target,也不知道每个target的UP时间,所以无法使用Scrape_*等指标做查询,也无法用scrape_limit做限制。
对于上述这些重度依赖,可以考虑将其优化掉,而Prometheus这种采用以拉模式为主的架构,在这方面的实现是一个很好的参考方向。同理,很多企业的监控系统对于cmdb具有强依赖,通过Prometheus这种架构也可以消除标签对cmdb的依赖。
组件介绍
Job/Exporter
Exporter将监控数据采集的端点通过HTTP服务的形式暴露给Prometheus Server,Prometheus Server通过访问该Exporter提供的Endpoint端点,即可获取到需要采集的监控数据。因此Promethenux是一种以拉为主的监控系统。
一般来说可以将Exporter分为2类:
- 直接采集:这一类Exporter直接内置了对Prometheus监控的支持,比如cAdvisor,Kubernetes,Etcd,Gokit等,都直接内置了用于向Prometheus暴露监控数据的端点。
- 间接采集:间接采集,原有监控目标并不直接支持Prometheus,因此我们需要通过Prometheus提供的Client Library编写该监控目标的监控采集程序。例如: Mysql Exporter,JMX Exporter,Consul Exporter等。
Pushgateway
Prometheus是拉模式为主的监控系统,它的推模式就是通过Pushgateway组件实现的。Pushgateway是支持临时性Job主动推送指标的中间网关,它本质上是一种用于监控Prometheus服务器无法抓取的资源的解决方案。它也是用Go语言编写的,在Apache 2.0许可证下开源。
Pushgateway作为一个独立的服务,位于被采集监控指标的应用程序和Prometheus服务器之间。应用程序主动推送指标到Pushgateway,Pushgateway接收指标,然后Pushgateway也作为target被Prometheus服务器抓取。它的使用场景主要有如下几种:
- 临时/短作业。
- 批处理作业。
- 应用程序与Prometheus服务器之间有网络隔离,如安全性(防火墙)、连接性(不在一个网段,服务器或应用程序仅允许特定端口或路径访问)。
Pushgateway与网关类似,在Prometheus中被建议作为临时性解决方案,主要用于监控不太方便访问到的资源。它会丢失很多Prometheus服务器提供的功能,比如UP指标和指标过期时进行实例状态监控。
Service Discovery
Prometheus通过服务发现机制对云以及容器环境下的监控场景提供了完善的支持。
服务发现方式:
配置文件:
Prometheus会周期性地从文件中读取最新的target信息。
对于支持文件的服务发现,实践场景下可以衍生为与自动化配置管理工具(Ansible、Cron Job、Puppet、SaltStack等)结合使用。
通过服务动态感知:
Prometheus还支持多种常见的服务发现组件,如Kubernetes、DNS、Zookeeper、Azure、EC2和GCE等。例如,Prometheus可以使用Kubernetes的API获取容器信息的变化(如容器的创建和删除)来动态更新监控对象。
通过服务发现的方式,管理员可以在不重启Prometheus服务的情况下动态发现需要监控的target实例信息。
Prometheus Server

Prometheus服务器是Prometheus最核心的模块。它主要包含抓取、存储和查询这3个功能:
(1)抓取:Prometheus Server通过服务发现组件,周期性地从上面介绍的Job、Exporter、Pushgateway这3个组件中通过HTTP轮询的形式拉取监控指标数据;
(2)存储:抓取到的监控数据通过一定的规则清理和数据整理(抓取前使用服务发现提供的relabel_configs方法,抓取后使用作业内的metrics_relabel_configs方法),把得到的结果存储到新的时间序列中进行持久化。多年来,存储模块经历了多次重新设计,Prometheus 2.0版的存储系统是第三次迭代。该存储系统每秒可以处理数百万个样品的摄入,使得使用一台Prometheus服务器监控数千台机器成为可能。使用的压缩算法可以在真实数据上实现每个样本1.3B。建议使用SSD,但不是严格要求。
Prometheus的存储分为本地存储和远程存储
- 本地存储:会直接保留到本地磁盘,性能上建议使用SSD且不要保存超过一个月的数据。记住,任何版本的Prometheus都不支持NFS。一些实际生产案例告诉我们,Prometheus存储文件如果使用NFS,则有损坏或丢失历史数据的可能。
- 远程存储:适用于存储大量的监控数据。Prometheus支持的远程存储包括OpenTSDB、InfluxDB、Elasticsearch、Graphite、CrateDB、Kakfa、PostgreSQL、TimescaleDB、TiKV等。远程存储需要配合中间层的适配器进行转换,主要涉及Prometheus中的remote_write和remote_read接口。在实际生产中,远程存储会出现各种各样的问题,需要不断地进行优化、压测、架构改造甚至重写上传数据逻辑的模块等工作。
(3)查询:Prometheus持久化数据以后,客户端就可以通过PromQL语句对数据进行查询了。
Dashboard
在Prometheus架构图中提到,Web UI、Grafana、API client可以统一理解为Prometheus的Dashboard。Prometheus服务器除了内置查询语言PromQL以外,还支持表达式浏览器及表达式浏览器上的数据图形界面。实际工作中使用Grafana等作为前端展示界面,用户也可以直接使用Client向Prometheus Server发送请求以获取数据。
Alertmanager
Alertmanager是独立于Prometheus的一个告警组件,需要单独安装部署。Prometheus可以将多个Alertmanager配置为一个集群,通过服务发现动态发现告警集群中节点的上下线从而避免单点问题,Alertmanager也支持集群内多个实例之间的通信。

Alertmanager接收Prometheus推送过来的告警,用于管理、整合和分发告警到不同的目的地。Alertmanager提供了多种内置的第三方告警通知方式,同时还提供了对Webhook通知的支持,通过Webhook用户可以完成对告警的更多个性化的扩展。Alertmanager除了提供基本的告警通知能力以外,还提供了如分组、抑制以及静默等告警特性。
Prometheus的3大局限性
Prometheus固然强大,但它还是具有一定局限性的。
- 更多地展示的是趋势性的监控。Prometheus作为一个基于度量的系统,不适合存储事件或者日志等,它更多地展示的是趋势性的监控。如果用户需要数据的精准性,可以考虑ELK或其他日志架构。另外,APM更适用于链路追踪的场景;
- Prometheus本地存储不适合大量历史数据存储。Prometheus认为只有最近的监控数据才有查询的需要,所有Prometheus本地存储的设计初衷只是保存短期(如一个月)的数据,不会针对大量的历史数据进行存储。如果需要历史数据,则建议使用Prometheus的远端存储,如OpenTSDB、M3DB等;
- 成熟度没有InfluxDB高。Prometheus在集群上不论是采用联邦集群还是采用Improbable开源的Thanos等方案,都没有InfluxDB成熟度高,需要解决很多细节上的技术问题(如耗尽CPU、消耗机器资源等问题),这也是开头提到的InfluxDB在时序数据库中排名第一的原因之一。部分互联网公司拥有海量业务,出于集群的原因会考虑对单机免费但是集群收费的InfluxDB进行自主研发。
总之,使用Prometheus一定要了解它的设计理念:它并不是为了解决大容量存储问题,TB级以上数据建议保存到远端TSDB中;它是为运行时正确的监控数据准备的,无法做到100%精准,存在由内核故障、刮擦故障等因素造成的微小误差。
PromQL
时间序列
通过Node Exporter暴露的HTTP服务,Prometheus可以采集到当前主机所有监控指标的样本数据。例如:
# HELP node_cpu Seconds the cpus spent in each mode.
# TYPE node_cpu counter
node_cpu{cpu="cpu0",mode="idle"} 362812.7890625
# HELP node_load1 1m load average.
# TYPE node_load1 gauge
node_load1 3.0703125
其中非#开头的每一行表示当前Node Exporter采集到的一个监控样本:
- node_cpu和node_load1表明了当前指标的名称;
- 大括号中的标签则反映了当前样本的一些特征和维度;
- 浮点数则是该监控样本的具体值。
Prometheus会将所有采集到的样本数据以时间序列(time-series)的方式保存在内存数据库中,并且定时保存到硬盘上。time-series是按照时间戳和值的序列顺序存放的,我们称之为向量(vector)。每条time-series通过指标名称(metrics name)和一组标签集(labelset)命名。如下所示,可以将time-series理解为一个以时间为Y轴的数字矩阵:

在time-series中的每一个点称为一个样本(sample),样本由以下三部分组成:
- 指标(metric):metric name和描述当前样本特征的labelsets;
- 时间戳(timestamp):一个精确到毫秒的时间戳;
- 样本值(value): 一个float64的浮点型数据表示当前样本的值。

在形式上,所有的指标(Metric)都通过如下格式标示:
<metric name>{<label name>=<label value>, ...}
指标的名称(metric name)可以反映被监控样本的含义(比如,http_request_total
- 表示当前系统接收到的HTTP请求总量)。指标名称只能由ASCII字符、数字、下划线以及冒号组成并必须符合正则表达式[a-zA-Z_:][a-zA-Z0-9_:]*
。
标签(label)反映了当前样本的特征维度,通过这些维度Prometheus可以对样本数据进行过滤,聚合等。标签的名称只能由ASCII字符、数字以及下划线组成并满足正则表达式[a-zA-Z_][a-zA-Z0-9_]*
。
其中以__
作为前缀的标签,是系统保留的关键字,只能在系统内部使用。标签的值则可以包含任何Unicode编码的字符。在Prometheus的底层实现中指标名称实际上是以__name__=<metric name>
的形式保存在数据库中的,因此以下两种方式均表示的同一条time-series:
api_http_requests_total{method="POST", handler="/messages"}
等同于:
{__name__="api_http_requests_total",method="POST", handler="/messages"}
metric类型
在Prometheus的存储实现上所有的监控样本都是以time-series的形式保存在Prometheus内存的TSDB(时序数据库)中,而time-series所对应的监控指标(metric)也是通过labelset进行唯一命名的。
从存储上来讲所有的监控指标metric都是相同的,但是在不同的场景下这些metric又有一些细微的差异。 例如,在Node Exporter返回的样本中指标node_load1反应的是当前系统的负载状态,随着时间的变化这个指标返回的样本数据是在不断变化的。而指标node_cpu所获取到的样本数据却不同,它是一个持续增大的值,因为其反应的是CPU的累积使用时间,从理论上讲只要系统不关机,这个值是会无限变大的。
为了能够帮助用户理解和区分这些不同监控指标之间的差异,Prometheus定义了4中不同的指标类型(metric type):Counter(计数器)、Gauge(仪表盘)、Histogram(直方图)、Summary(摘要)。
在Exporter返回的样本数据中,其注释中也包含了该样本的类型。例如:
# HELP node_cpu Seconds the cpus spent in each mode.
# TYPE node_cpu counter
node_cpu{cpu="cpu0",mode="idle"} 362812.7890625
# HELP node_load1 1m load average.
# TYPE node_load1 gauge
node_load1 3.0703125
Counter(只增不减的计数器)
Counter类型的指标其工作方式和计数器一样,只增不减(除非系统发生重置)。常见的监控指标,如http_requests_total,node_cpu都是Counter类型的监控指标。 一般在定义Counter类型指标的名称时推荐使用**_total**作为后缀。
Counter是一个简单但有强大的工具,例如我们可以在应用程序中记录某些事件发生的次数,通过以时序的形式存储这些数据,我们可以轻松的了解该事件产生速率的变化。 PromQL内置的聚合操作和函数可以让用户对这些数据进行进一步的分析:
例如:
increase函数获取区间向量中的第一个后最后一个样本并返回其增长量。因此,可以通过以下表达式Counter类型指标的增长率:
increase(node_cpu[2m])
通过rate()函数获取HTTP请求量的增长率:
rate(http_requests_total[5m])
等同于:
increase(node_cpu[5m]) / 300
需要注意的是使用rate或者increase函数去计算样本的平均增长速率,容易陷入“长尾问题”当中,其无法反应在时间窗口内样本数据的突发变化。 例如,对于主机而言在2分钟的时间窗口内,可能在某一个由于访问量或者其它问题导致CPU占用100%的情况,但是通过计算在时间窗口内的平均增长率却无法反应出该问题。
irate同样用于计算区间向量的计算率,但是其反应出的是瞬时增长率。irate函数是通过区间向量中最后两个样本数据来计算区间向量的增长速率。这种方式可以避免在时间窗口范围内的“长尾问题”,并且体现出更好的灵敏度,通过irate函数绘制的图标能够更好的反应样本数据的瞬时变化状态。
irate函数相比于rate函数提供了更高的灵敏度,不过当需要分析长期趋势或者在告警规则中,irate的这种灵敏度反而容易造成干扰。因此在长期趋势分析或者告警中更推荐使用rate函数。
irate(node_cpu[2m])
查询当前系统中,访问量前10的HTTP地址:
topk(10, http_requests_total)
Gauge(可增可减的仪表盘)
与Counter不同,Gauge类型的指标侧重于反应系统的当前状态。因此这类指标的样本数据可增可减。常见指标如:node_memory_MemFree(主机当前空闲的内容大小)、node_memory_MemAvailable(可用内存大小)都是Gauge类型的监控指标。
通过Gauge指标,用户可以直接查看系统的当前状态:
node_memory_MemFree
对于Gauge类型的监控指标,通过PromQL内置函数delta()可以获取样本在一段时间返回内的变化情况。例如,计算CPU温度在两个小时内的差异:
delta(cpu_temp_celsius{host="zeus"}[2h])
还可以使用deriv()计算样本的线性回归模型,甚至是直接使用predict_linear()对数据的变化趋势进行预测。例如,预测系统磁盘空间在4个小时之后的剩余情况:
predict_linear(node_filesystem_free{job="node"}[1h], 4 * 3600)
Histogram和Summary(分析数据分布情况)
除了Counter和Gauge类型的监控指标以外,Prometheus还定义了Histogram和Summary的指标类型。Histogram和Summary主用用于统计和分析样本的分布情况。
在大多数情况下人们都倾向于使用某些量化指标的平均值,例如CPU的平均使用率、页面的平均响应时间。这种方式的问题很明显,以系统API调用的平均响应时间为例:如果大多数API请求都维持在100ms的响应时间范围内,而个别请求的响应时间需要5s,那么就会导致某些WEB页面的响应时间落到中位数的情况,而这种现象被称为长尾问题。
为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在0~10ms之间的请求数有多少而10~20ms之间的请求数又有多少。通过这种方式可以快速分析系统慢的原因。Histogram和Summary都是为了能够解决这样问题的存在,通过Histogram和Summary类型的监控指标,我们可以快速了解监控样本的分布情况。
例如,指标prometheus_tsdb_wal_fsync_duration_seconds的指标类型为Summary。 它记录了Prometheus Server中wal_fsync处理的处理时间,通过访问Prometheus Server的/metrics地址,可以获取到以下监控样本数据:
# HELP prometheus_tsdb_wal_fsync_duration_seconds Duration of WAL fsync.
# TYPE prometheus_tsdb_wal_fsync_duration_seconds summary
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.5"} 0.012352463
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.9"} 0.014458005
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.99"} 0.017316173
prometheus_tsdb_wal_fsync_duration_seconds_sum 2.888716127000002
prometheus_tsdb_wal_fsync_duration_seconds_count 216
从上面的样本中可以得知当前Prometheus Server进行wal_fsync操作的总次数为216次,耗时2.888716127000002s。其中中位数(quantile=0.5)的耗时为0.012352463,9分位数(quantile=0.9)的耗时为0.014458005s。
在Prometheus Server自身返回的样本数据中,我们还能找到类型为Histogram的监控指标prometheus_tsdb_compaction_chunk_range_bucket。
# HELP prometheus_tsdb_compaction_chunk_range Final time range of chunks on their first compaction
# TYPE prometheus_tsdb_compaction_chunk_range histogram
prometheus_tsdb_compaction_chunk_range_bucket{le="100"} 0
prometheus_tsdb_compaction_chunk_range_bucket{le="400"} 0
prometheus_tsdb_compaction_chunk_range_bucket{le="1600"} 0
prometheus_tsdb_compaction_chunk_range_bucket{le="6400"} 0
prometheus_tsdb_compaction_chunk_range_bucket{le="25600"} 0
prometheus_tsdb_compaction_chunk_range_bucket{le="102400"} 0
prometheus_tsdb_compaction_chunk_range_bucket{le="409600"} 0
prometheus_tsdb_compaction_chunk_range_bucket{le="1.6384e+06"} 260
prometheus_tsdb_compaction_chunk_range_bucket{le="6.5536e+06"} 780
prometheus_tsdb_compaction_chunk_range_bucket{le="2.62144e+07"} 780
prometheus_tsdb_compaction_chunk_range_bucket{le="+Inf"} 780
prometheus_tsdb_compaction_chunk_range_sum 1.1540798e+09
prometheus_tsdb_compaction_chunk_range_count 780
与Summary类型的指标相似之处在于Histogram类型的样本同样会反应当前指标的记录的总数(以_count作为后缀)以及其值的总量(以_sum作为后缀)。不同在于Histogram指标直接反应了在不同区间内样本的个数,区间通过标签len进行定义。
同时对于Histogram的指标,我们还可以通过histogram_quantile()函数计算出其值的分位数。不同在于Histogram通过histogram_quantile函数是在服务器端计算的分位数。 而Sumamry的分位数则是直接在客户端计算完成。因此对于分位数的计算而言,Summary在通过PromQL进行查询时有更好的性能表现,而Histogram则会消耗更多的资源。反之对于客户端而言Histogram消耗的资源更少。在选择这两种方式时用户应该按照自己的实际场景进行选择。
比如计算中位数:
histogram_quantile(0.5, prometheus_tsdb_compaction_chunk_range_bucket)
查询操作
Prometheus通过指标名称(metrics name)以及对应的一组标签(labelset)唯一定义一条时间序列。指标名称反映了监控样本的基本标识,而label则在这个基本特征上为采集到的数据提供了多种特征维度。用户可以基于这些特征维度过滤,聚合,统计从而产生新的计算后的一条时间序列。
查询时间序列
当Prometheus通过Exporter采集到相应的监控指标样本数据后,我们就可以通过PromQL对监控样本数据进行查询。
当我们直接使用监控指标名称查询时,可以查询该指标下的所有时间序列。如:
http_requests_total
等同于:
http_requests_total{}
该表达式会返回指标名称为http_requests_total的所有时间序列:
http_requests_total{code="200",handler="alerts",instance="localhost:9090",job="prometheus",method="get"}=(20889@1518096812.326)
http_requests_total{code="200",handler="graph",instance="localhost:9090",job="prometheus",method="get"}=(21287@1518096812.326)
PromQL还支持用户根据时间序列的标签匹配模式来对时间序列进行过滤,目前主要支持两种匹配模式:完全匹配和正则匹配。
PromQL支持使用=
和!=
两种完全匹配模式:
- 通过使用
label=value
可以选择那些标签满足表达式定义的时间序列; - 反之使用
label!=value
则可以根据标签匹配排除时间序列;
例如,如果我们只需要查询所有http_requests_total时间序列中满足标签instance为localhost:9090的时间序列,则可以使用如下表达式:
http_requests_total{instance="localhost:9090"}
反之使用instance!="localhost:9090"
则可以排除这些时间序列:
http_requests_total{instance!="localhost:9090"}
除了使用完全匹配的方式对时间序列进行过滤以外,PromQL还可以支持使用正则表达式作为匹配条件,多个表达式之间使用|
进行分离:
- 使用
label=~regx
表示选择那些标签符合正则表达式定义的时间序列; - 反之使用
label!~regx
进行排除;
例如,如果想查询多个环节下的时间序列序列可以使用如下表达式:
http_requests_total{environment=~"staging|testing|development",method!="GET"}
合法的表达式:
所有的PromQL表达式都必须至少包含一个指标名称(例如http_request_total),或者一个不会匹配到空字符串的标签过滤器(例如{code=“200”})。
因此以下两种方式,均为合法的表达式:
http_request_total # 合法
http_request_total{} # 合法
{method="get"} # 合法
同时,除了使用<metric name>{label=value}
的形式以外,我们还可以使用内置的__name__
标签来指定监控指标名称:
{__name__=~"http_request_total"} # 合法
{__name__=~"node_disk_bytes_read|node_disk_bytes_written"} # 合法
时间范围查询
直接通过类似于PromQL表达式http*requests*total查询时间序列时,返回值中只会包含该时间序列中的最新的一个样本值,这样的返回结果我们称之为瞬时向量。而相应的这样的表达式称之为__瞬时向量表达式。
而如果我们想过去一段时间范围内的样本数据时,我们则需要使用区间向量表达式。区间向量表达式和瞬时向量表达式之间的差异在于在区间向量表达式中我们需要定义时间选择的范围,时间范围通过时间范围选择器[]
进行定义。例如,通过以下表达式可以选择最近5分钟内的所有样本数据:
http_request_total{}[5m]
该表达式将会返回查询到的时间序列中最近5分钟的所有样本数据:
http_requests_total{code="200",handler="alerts",instance="localhost:9090",job="prometheus",method="get"}=[
1@1518096812.326
1@1518096817.326
1@1518096822.326
1@1518096827.326
1@1518096832.326
1@1518096837.325
]
http_requests_total{code="200",handler="graph",instance="localhost:9090",job="prometheus",method="get"}=[
4 @1518096812.326
4@1518096817.326
4@1518096822.326
4@1518096827.326
4@1518096832.326
4@1518096837.325
]
通过区间向量表达式查询到的结果我们称为区间向量。
除了使用m表示分钟以外,PromQL的时间范围选择器支持其它时间单位:
- s - 秒
- m - 分钟
- h - 小时
- d - 天
- w - 周
- y - 年
时间位移操作
在瞬时向量表达式或者区间向量表达式中,都是以当前时间为基准:
http_request_total{} # 瞬时向量表达式,选择当前最新的数据
http_request_total{}[5m] # 区间向量表达式,选择以当前时间为基准,5分钟内的数据
而如果我们想查询,5分钟前的瞬时样本数据,或昨天一天的区间内的样本数据呢? 这个时候我们就可以使用位移操作,位移操作的关键字为offset。
可以使用offset时间位移操作:
http_request_total{} offset 5m
http_request_total{}[1d] offset 1d
聚合操作
一般来说,如果描述样本特征的标签(label)在并非唯一的情况下,通过PromQL查询数据,会返回多条满足这些特征维度的时间序列。而PromQL提供的聚合操作可以用来对这些时间序列进行处理,形成一条新的时间序列。
常用聚合操作:
sum
(求和)min
(最小值)max
(最大值)avg
(平均值)stddev
(标准差)stdvar
(标准差异)count
(计数)count_values
(对value进行计数)bottomk
(后n条时序)topk
(前n条时序)quantile
(分布统计)
使用聚合操作的语法如下:
<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]
其中只有count_values
, quantile
, topk
, bottomk
支持参数(parameter)。
without用于从计算结果中移除列举的标签,而保留其它标签。by则正好相反,结果向量中只保留列出的标签,其余标签则移除。通过without和by可以按照样本的问题对数据进行聚合。
sum(http_requests_total) without (instance)
等价于:
sum(http_requests_total) by (code,handler,job,method)
其他例子:
# 查询系统所有http请求的总量
sum(http_request_total)
# 按照mode计算主机CPU的平均使用时间
avg(node_cpu) by (mode)
# 按照主机查询各个主机的CPU使用率
sum(sum(irate(node_cpu{mode!='idle'}[5m])) / sum(irate(node_cpu[5m]))) by (instance)
# count_values用于时间序列中每一个样本值出现的次数。count_values会为每一个唯一的样本值输出一个时间序列,并且每一个时间序列包含一个额外的标签。
count_values("count", http_requests_total)
# topk和bottomk则用于对样本值进行排序,返回当前样本值前n位,或者后n位的时间序列。
# 获取HTTP请求数前5位的时序样本数据
topk(5, http_requests_total)
# quantile用于计算当前样本数据值的分布情况quantile(φ, express)其中0 ≤ φ ≤ 1。
# 当φ为0.5时,即表示找到当前样本数据中的中位数:
quantile(0.5, http_requests_total)
操作符
(1)PromQL支持的所有数学运算符如下所示:
+
(加法)-
(减法)*
(乘法)/
(除法)%
(求余)^
(幂运算)
# 我们可以通过指标node_memory_free_bytes_total获取当前主机可用的内存空间大小,其样本单位为Bytes。这是如果客户端要求使用MB作为单位响应数据,那只需要将查询到的时间序列的样本值进行单位换算即可:
node_memory_free_bytes_total / (1024 * 1024)
# 如果我们想根据node_disk_bytes_written和node_disk_bytes_read获取主机磁盘IO的总量,可以使用如下表达式:
node_disk_bytes_written + node_disk_bytes_read
(2)Prometheus支持以下布尔运算符如下:
==
(相等)!=
(不相等)>
(大于)<
(小于)>=
(大于等于)<=
(小于等于)
# 通过数学运算符我们可以很方便的计算出,当前所有主机节点的内存使用率:
(node_memory_bytes_total - node_memory_free_bytes_total) / node_memory_bytes_total
# 系统管理员在排查问题的时候可能只想知道当前内存使用率超过95%的主机
(node_memory_bytes_total - node_memory_free_bytes_total) / node_memory_bytes_total > 0.95
(3)使用bool修饰符改变布尔运算符的行为:
# 当前模块的HTTP请求量是否>=1000,如果大于等于1000则返回1(true)否则返回0
http_requests_total > bool 1000
使用bool修改符后,布尔运算不会对时间序列进行过滤,而是直接依次瞬时向量中的各个样本数据与标量的比较结果0或者1。从而形成一条新的时间序列。
http_requests_total{code="200",handler="query",instance="localhost:9090",job="prometheus",method="get"} 1
http_requests_total{code="200",handler="query_range",instance="localhost:9090",job="prometheus",method="get"} 0
同时需要注意的是,如果是在两个标量之间使用布尔运算,则必须使用bool修饰符:
2 == bool 2 # 结果为1
(4)Prometheus支持以下集合运算符:
and
(并且)or
(或者)unless
(排除)
*vector1 and vector2* 会产生一个由vector1的元素组成的新的向量。该向量包含vector1中完全匹配vector2中的元素组成。
*vector1 or vector2* 会产生一个新的向量,该向量包含vector1中所有的样本数据,以及vector2中没有与vector1匹配到的样本数据。
*vector1 unless vector2* 会产生一个新的向量,新向量中的元素由vector1中没有与vector2匹配的元素组成。
(5)操作符优先级:
在PromQL操作符中优先级由高到低依次为:
^
*, /, %
+, -
==, !=, <=, <, >=, >
and, unless
or
匹配模式
向量与向量之间进行运算操作时会基于默认的匹配规则:依次找到与左边向量元素匹配(标签完全一致)的右边向量元素进行运算,如果没找到匹配元素,则直接丢弃。
接下来将介绍在PromQL中有两种典型的匹配模式:一对一(one-to-one),多对一(many-to-one)或一对多(one-to-many)。
(1)一对一
在操作符两边表达式标签不一致的情况下,可以使用on(label list)或者ignoring(label list)来修改便签的匹配行为。使用ignoreing可以在匹配时忽略某些便签。而on则用于将匹配行为限定在某些便签之内。
<vector expr> <bin-op> ignoring(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) <vector expr>
例如当存在样本:
method_code:http_errors:rate5m{method="get", code="500"} 24
method_code:http_errors:rate5m{method="get", code="404"} 30
method_code:http_errors:rate5m{method="put", code="501"} 3
method_code:http_errors:rate5m{method="post", code="500"} 6
method_code:http_errors:rate5m{method="post", code="404"} 21
method:http_requests:rate5m{method="get"} 600
method:http_requests:rate5m{method="del"} 34
method:http_requests:rate5m{method="post"} 120
使用PromQL表达式:
method_code:http_errors:rate5m{code="500"} / ignoring(code) method:http_requests:rate5m
该表达式会返回在过去5分钟内,HTTP请求状态码为500的在所有请求中的比例。如果没有使用ignoring(code),操作符两边表达式返回的瞬时向量中将找不到任何一个标签完全相同的匹配项。
因此结果如下:
{method="get"} 0.04 // 24 / 600
{method="post"} 0.05 // 6 / 120
同时由于method为put和del的样本找不到匹配项,因此不会出现在结果当中。
(2)一对多/多对一
多对一和一对多两种匹配模式指的是“一”侧的每一个向量元素可以与"多"侧的多个元素匹配的情况。在这种情况下,必须使用group修饰符:group_left或者group_right来确定哪一个向量具有更高的基数(充当“多”的角色)。
<vector expr> <bin-op> ignoring(<label list>) group_left(<label list>) <vector expr>
<vector expr> <bin-op> ignoring(<label list>) group_right(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) group_left(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) group_right(<label list>) <vector expr>
多对一和一对多两种模式一定是出现在操作符两侧表达式返回的向量标签不一致的情况。因此需要使用ignoring和on修饰符来排除或者限定匹配的标签列表。
例如,使用表达式:
method_code:http_errors:rate5m / ignoring(code) group_left method:http_requests:rate5m
该表达式中,左向量method_code:http_errors:rate5m
包含两个标签method和code。而右向量method:http_requests:rate5m
中只包含一个标签method,因此匹配时需要使用ignoring限定匹配的标签为code。 在限定匹配标签后,右向量中的元素可能匹配到多个左向量中的元素 因此该表达式的匹配模式为多对一,需要使用group修饰符group_left指定左向量具有更好的基数。
最终的运算结果如下:
{method="get", code="500"} 0.04 // 24 / 600
{method="get", code="404"} 0.05 // 30 / 600
{method="post", code="500"} 0.05 // 6 / 120
{method="post", code="404"} 0.175 // 21 / 120
提醒:group修饰符只能在比较和数学运算符中使用。在逻辑运算and,unless和or才注意操作中默认与右向量中的所有元素进行匹配。
安装
(1)安装 kube-prometheus:
这里要注意一下版本兼容问题,在官网下载对应的版本即可
kube-prometheus stack | Kubernetes 1.16 | Kubernetes 1.17 | Kubernetes 1.18 | Kubernetes 1.19 | Kubernetes 1.20 |
---|---|---|---|---|---|
release-0.4 | ✔ (v1.16.5+) | ✔ | ✗ | ✗ | ✗ |
release-0.5 | ✗ | ✗ | ✔ | ✗ | ✗ |
release-0.6 | ✗ | ✗ | ✔ | ✔ | ✗ |
release-0.7 | ✗ | ✗ | ✗ | ✔ | ✔ |
HEAD | ✗ | ✗ | ✗ | ✔ | ✔ |
# 克隆相应的分支
git clone -b release-0.3 https://github.com/coreos/kube-prometheus.git
cd kube-prometheus/manifest
里面包含很多的资源清单:
~/gi/kube-prometheus/manifests release-0.3 > ls py base 16:20:01
alertmanager-alertmanager.yaml prometheus-adapter-clusterRoleAggregatedMetricsReader.yaml
alertmanager-secret.yaml prometheus-adapter-clusterRoleBinding.yaml
alertmanager-service.yaml prometheus-adapter-clusterRoleBindingDelegator.yaml
alertmanager-serviceAccount.yaml prometheus-adapter-clusterRoleServerResources.yaml
alertmanager-serviceMonitor.yaml prometheus-adapter-configMap.yaml
grafana-dashboardDatasources.yaml prometheus-adapter-deployment.yaml
grafana-dashboardDefinitions.yaml prometheus-adapter-roleBindingAuthReader.yaml
grafana-dashboardSources.yaml prometheus-adapter-service.yaml
grafana-deployment.yaml prometheus-adapter-serviceAccount.yaml
grafana-service.yaml prometheus-clusterRole.yaml
grafana-serviceAccount.yaml prometheus-clusterRoleBinding.yaml
grafana-serviceMonitor.yaml prometheus-operator-serviceMonitor.yaml
kube-state-metrics-clusterRole.yaml prometheus-prometheus.yaml
kube-state-metrics-clusterRoleBinding.yaml prometheus-roleBindingConfig.yaml
kube-state-metrics-deployment.yaml prometheus-roleBindingSpecificNamespaces.yaml
kube-state-metrics-role.yaml prometheus-roleConfig.yaml
kube-state-metrics-roleBinding.yaml prometheus-roleSpecificNamespaces.yaml
kube-state-metrics-service.yaml prometheus-rules.yaml
kube-state-metrics-serviceAccount.yaml prometheus-service.yaml
kube-state-metrics-serviceMonitor.yaml prometheus-serviceAccount.yaml
node-exporter-clusterRole.yaml prometheus-serviceMonitor.yaml
node-exporter-clusterRoleBinding.yaml prometheus-serviceMonitorApiserver.yaml
node-exporter-daemonset.yaml prometheus-serviceMonitorCoreDNS.yaml
node-exporter-service.yaml prometheus-serviceMonitorKubeControllerManager.yaml
node-exporter-serviceAccount.yaml prometheus-serviceMonitorKubeScheduler.yaml
node-exporter-serviceMonitor.yaml prometheus-serviceMonitorKubelet.yaml
prometheus-adapter-apiService.yaml setup
prometheus-adapter-clusterRole.yaml
(2)修改 grafana-service.yaml
:
$ vim grafana-service.yaml
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
spec:
type: NodePort # 添加内容
ports:
- name: http
port: 3000
targetPort: http
nodePort: 30100 #添加内容
selector:
app: grafana
(3)修改 prometheus-service.yaml
:
$ vim prometheus-service.yaml
apiVersion: v1
kind: Service
metadata:
labels:
prometheus: k8s
name: prometheus-k8s
namespace: monitoring
spec:
type: NodePort # 添加
ports:
- name: web
port: 9090
targetPort: web
nodePort: 30200 # 添加
selector:
app: prometheus
prometheus: k8s
sessionAffinity: ClientIP
(4)安装
# 在 kube-prometheus/manifests 目录下执行
# 创建名称空间,prometheus 组件都在这个名称空间下
kubectl create namespace monitoring
# 这两个目录下的 yaml 都要创建
kubectl apply -f ./setup
kubectl apply -f .
如果报了下面的错:
unable to recognize "../manifests/alertmanager-alertmanager.yaml": no matches for kind "Alertmanager" in version "monitoring.coreos.com/v1"
unable to recognize "../manifests/alertmanager-serviceMonitor.yaml": no matches for kind "ServiceMonitor" in version "monitoring.coreos.com/v1"
unable to recognize "../manifests/grafana-serviceMonitor.yaml": no matches for kind "ServiceMonitor" in version "monitoring.coreos.com/v1"
...
就多执行几遍下面的命令(每次执行前要确保上次执行的 pod 已经 Running):
kubectl apply -f ./setup
kubectl apply -f .
(5)kubectl top
命令查看集群资源
$ kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
k8s-master01 93m 4% 1183Mi 62%
k8s-node01 72m 3% 1116Mi 58%
k8s-node02 78m 3% 1151Mi 60%
$ kubectl top pod -n monitoring
NAME CPU(cores) MEMORY(bytes)
alertmanager-main-0 2m 21Mi
alertmanager-main-1 2m 22Mi
访问 prometheus
查看 service
:
$ kubectl get svc -o wide -n monitoring
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alertmanager-main NodePort 10.97.154.146 <none> 9093:30300/TCP 98m alertmanager=main,app=alertmanager
grafana NodePort 10.104.110.200 <none> 3000:30100/TCP 98m app=grafana
prometheus-k8s NodePort 10.104.85.240 <none> 9090:30200/TCP 98m app=prometheus,prometheus=k8s
根据前面安装时候的配置,prometheus
对应的 nodeport
端口为 30200
,访问 http://MasterIP:30200
。
查看 prometheus 的节点状态:

prometheus 的 WEB 界面上提供了基本的查询 K8S 集群中每个 Pod 的 CPU 使用情况,查询条件如下:
sum by (pod_name)( rate(container_cpu_usage_seconds_total{image!="", pod_name!=""}[1m] ) )

上述的查询有出现数据,说明 node-exporter 往 prometheus 中写入数据正常,接下来我们就可以部署 grafana 组件,实现更友好的 webui 展示数据了
注意:prometheus 对系统时间的要求比较高,要确保 k8s 每个节点的时间都同步。通过阿里云服务器同步中国上海时间:ntpdate ntp1.aliyun.com
访问 grafana
查看 grafana 服务暴露的端口号:
$ kubectl get service -n monitoring | grep grafana
grafana NodePort 10.98.154.100 <none> 3000:30100/TCP 11h
如上可以看到 grafana
的端口号是 30100
,浏览器访问:http://MasterIP:30100
,用户名密码默认 admin/admin
,第一次登录后会要求修改密码。

添加数据源:


查看apiserver:

也可以查看其它资源:

这里选择 Nodes:

Horizontal Pod Autoscaling(HPA)创建
HPA 的全称为(Horizontal Pod Autoscaling)它可以根据当前 pod 资源的使用率(如 CPU、磁盘、内存等),进行副本数的动态的扩容与缩容,以便减轻各个 pod 的压力,可以用于Replication Controller、Deployment 或者Replica Set。当 pod 负载达到一定的阈值后,会根据扩缩容的策略生成更多新的 pod 来分担压力,当 pod 的使用比较空闲时,在稳定空闲一段时间后,还会自动减少 pod 的副本数量。
若要实现自动扩缩容的功能,还需要部署 heapster 服务,用来收集及统计资源的利用率,支持 kubectl top 命令,heapster 服务集成在 prometheus(普罗米修斯) Mertric Server 服务中,所以说,要先安装 prometheus。
(1)创建 php-apache
为了测试 HPA,这里将使用 php-apache
。 php-apache
主要是一个 之后将通过请求访问该 Pod ,用来模拟请求的负载增加和减少,查看 Pod 的数量变化。
kubectl run php-apache --image=gcr.io/google_containers/hpa-example --requests=cpu=200m --expose --port=80
或者自己创建一个deployment和service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-apache
spec:
selector:
matchLabels:
run: php-apache
replicas: 1
template:
metadata:
labels:
run: php-apache
spec:
containers:
- name: php-apache
image: "registry.cn-shenzhen.aliyuncs.com/cookcodeblog/hpa-example:latest"
ports:
- containerPort: 80
resources:
limits:
cpu: 500m
requests:
cpu: 200m
---
apiVersion: v1
kind: Service
metadata:
name: php-apache
labels:
run: php-apache
spec:
ports:
- port: 80
selector:
run: php-apache
创建 deployment 和 servcie :
# 1. 创建
kubectl apply -f php-apache.yaml
# 2. 查看
kubectl get deployment php-apache
kubectl get svc php-apache
kubectl get pods -l run=php-apache -o wide
(2)创建HPA
为上面创建的 deployment php-apache
创建 HPA,其中最小副本数为 1
,最大副本数为 10
,保持该 deployment
的所有 Pod
的平均 CPU 使用率不超过 50%
。
# 默认创建的HPA名称和需要自动伸缩的对象名一致
# 可以通过--name来指定HPA名称
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
在本例中,deployment 的 pod 的 resources.request.cpu 为 200m (200 milli-cores vCPU),所以 HPA 将保持所有 Pod 的平均 CPU 使用率不超过 100m。可以通过 kubectl top pods 查看 pod 的 CPU 使用情况。
查看HPA:
$ kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
php-apache Deployment/php-apache 0%/50% 1 10 1 25s
如果 TARGETS
列值格式为 /
,如果 actual
值总是为 unkown
,则表示无法从 Metrics Server
中获取指标值。
HPA 默认每15秒从 Metrics Server 取一下指标来判断是否要自动伸缩。通过 --horizontal-pod-autoscaler-sync-period 来设置
Metrics Server 采集指标的默认间隔为60秒。可以使用 metrics-resolution 来修改,但不建议设置低于15s的值,因为这是 Kubelet 中计算的度量的分辨率。
(3)增加负载
打开一个新的 Terminal
,创建一个临时的 pod load-generator
,并在该 pod 中向 php-apache
服务发起不间断循环请求,模拟增加 php-apache
的负载(CPU使用率)。
kubectl run -i --tty load-generator --rm --image=busybox --restart=Never \
-- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"
查看 HPA 及 Pod:
# 监控 HPA
$ kubectl get hpa -w
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
php-apache Deployment/php-apache 196%/50% 1 10 4 17m
# 监控 Pod
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
load-generator 1/1 Running 0 16m 10.244.2.82 k8s-node02 <none> <none>
php-apache-5d8c7bfcd5-bsnnj 1/1 Running 0 13m 10.244.1.109 k8s-node01 <none> <none>
php-apache-5d8c7bfcd5-hs6gd 1/1 Running 0 14m 10.244.2.83 k8s-node02 <none> <none>
php-apache-5d8c7bfcd5-jmmpj 1/1 Running 0 53m 10.244.1.108 k8s-node01 <none> <none>
php-apache-5d8c7bfcd5-rdglr 1/1 Running 0 12m 10.244.2.84 k8s-node02 <none> <none>
# 当四个副本都运行之后,CPU 实际使用率已降低到49%
$ kubectl get hpa -w
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
php-apache Deployment/php-apache 49%/50% 1 10 4 30m
可以看到 HPA TARGETS(CPU使用率)的 acutal 值逐渐升高到196% (超过了期望值50%),副本数 REPLICAS 也从 1 自动扩容到了 4。当4个副本都 RUNNING 之后,CPU 使用率降低到了 50% 左右。
HPA 通过自动扩容到 4 个副本,来分摊了负载,使得所有 Pod 的平均 CPU 使用率保持(近似)在目标值。
查看 HPA 自动伸缩事件:
kubectl describe hpa php-apache
(4)减少负载
在运行 load-generator
的 Terminal
,按下 Ctrl + C
来终止进程。
过一段时间后来查看 HPA,副本数已经降到了1个:
$ kubectl get hpa php-apache
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
php-apache Deployment/php-apache 0%/50% 1 10 1 49m
如果观察HPA没有 scale down,需要再等待一段时间。Kuberntes 为了保证缩容时业务不中断,和防止频繁伸缩导致系统抖动,scale down 一次后需要等待一段时间才能再次 scale down,也叫伸缩冷却(cooldown)。默认伸缩冷却时间为5分钟。可以通过参数 --horizontal-pod-autoscaler-downscale-stabilization 修改
资源限制
Kubernetes 对资源的限制实际上是通过 cgroup 来控制的,cgroup 是容器的一组用来控制内核如何运行进程的相关属性集合。针对内存、CPU 和各种设备都有对应的 cgroup。
Pod资源限制
默认情况下,Pod 运行没有 CPU 和内存的限额。 这意味着系统中的任何 Pod 将能够像执行该 Pod 所在的节点一样,消耗足够多的 CPU 和内存 。一般会针对某些应用的 pod 资源进行资源限制,这个资源限制是通过 resources 的 requests 和 limits 来实现
- requests:要分分配的资源,可以简单理解为初始值;
- limits:为最高请求的资源值,可以简单理解为最大值
spec:
containers: # 在容器模板下进行设置
- image: xxxx
imagePullPolicy: Always
name: auth
ports:
- containerPort: 8080
protocol: TCP
resources:
limits:
cpu: "4" # 4个cpu
memory: 2Gi
requests:
cpu: 250m #250MHz
memory: 250Mi
名称空间资源限制
当 pod 没有设置资源限制的话,pod 会使用当前名称空间下的最大资源,如果名称空间也没有设置资源限制的话,pod 就可以使用集群的最大资源,就很有可能出现 OOM。
(1)计算资源配额
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-resources
namespace: spark-cluster
spec:
hard:
pods: "20" # 创建pod的数量
requests.cpu: "20" # 所有pod加在一起总共20个
requests.memory: 100Gi # 所有pod加在一起总共100G
limits.cpu: "40"
limits.memory: 200Gi
(2)配置对象数量配额限制
apiVersion: v1
kind: ResourceQuota
metadata:
name: object-counts
namespace: spark-cluster
spec:
hard:
configmaps: "10" # configmap 最多创建10个
persistentvolumeclaims: "4"
replicationcontrollers: "20"
secrets: "10"
services: "10"
services.loadbalancers: "2"
(3)配置 CPU 和 内存 LimitRange
LimitRange
是对指定名称空间下 pod
和 container
进行设置:
apiVersion: v1
kind: LimitRange
metadata:
name: mem-limit-range
spec:
limits:
- default: # 默认最大值
memory: 50Gi # 最大可以用到 50G
cpu: 5 # 最多可以使用 5 个cpu
defaultRequest: # 默认初始值
memory: 1Gi # 内存 1G
cpu: 1 # 一个cpu
type: Container # 类型