资讯详情

资讯详情

Prometheus监控系统实战:架构、PromQL查询与告警配置详解

说起监控系统这几年不管在哪个团队聊答案大概率都绕不开同一个名字Prometheus普罗米修斯。不夸张地说它是目前云原生领域监控的事实标准Kubernetes生态里默认的监控采集方案就是它。如果你做后端、运维或者SRE相关工作简历上不写Prometheus面试官基本会默认你只是“听说过”。这项目从2012年在某家公司内部诞生到2016年正式对外发布并加入云原生基金会再到今天成为CNCF里少数几个“毕业级”项目之一靠的不是概念包装而是设计上确实能打。这篇文章不打算给你念官方文档我按自己这些年实际搭监控系统、排查告警、优化存储的经验把Prometheus从架构设计、指标采集、查询语法到告警配置、常见坑位逐一拆开讲。适合两类人看一是刚接触监控、打算选型的新手可以借此建立整体认知二是已经在用但老觉得“哪里不对劲”的朋友比如内存时不时飙高、告警总是重复轰炸、查询反应慢这些我都会把原因和解决办法列出来。文章里所有实操步骤和参数都是我实际验证过的写法你基本可以照着抄。1. 核心架构与设计思路为什么偏偏是“pull”1.1 拉取模型监控系统里的一股清流Prometheus和传统监控系统最大的分水岭就是数据采集方式。像Zabbix那类监控系统一般是Agent主动把数据推到Server端也就是push模型。而Prometheus反着来——统一由Prometheus Server主动去各个目标拉数据也就是pull模型。别小看这个方向上的差别它直接决定了一个监控系统的稳定性和可调试性。pull模型最大的好处是你不需要在监控目标上维护一个单独的推送进程与Server之间的保活状态。Prometheus只要定期按配置去抓一遍HTTP接口就行目标挂了、接口超时了、数据不返回了Prometheus会在Targets面板里标红告诉你这个目标现在是down还是up。这相当于把健康状态探测和数据采集合并成了一件事省掉了一套额外的“心跳机制”。另一个隐性优势是可控的流量姿态。在push模型下业务方如果代码写得激进可能一下往Server灌大量数据把监控系统本身打挂这就是典型的“监控拖垮生产”。而Prometheus是Server端按固定周期去抓取抓多快、超时多久、并发几个目标都由运维说了算。我之前给一个业务团队搭监控栈时他们每天会跑一批大数据批处理如果按push模型这批作业一启动就会集中上报成千上万条数据监控Server很容易被冲垮。换成pull之后Prometheus一轮一轮按节奏拉压力完全在自己的掌控范围内。当然pull模型也有搞不定的场景比如短生命周期任务。像Kubernetes里的CronJob、离线计算容器跑几秒钟就结束了Prometheus还没轮到抓取它就已经消失根本拉不到数据。这种场景Prometheus官方给出的方案是Pushgateway——一个中间代理组件。业务方在任务启动时把指标push到PushgatewayPrometheus再去Pushgateway上拉取。但老实说Pushgateway用起来要小心它会成为单个故障点和指标滞后的来源能不用就尽量不用那种几十秒以上的常驻任务还是直接暴露/metrics接口让Prometheus来抓更省心。1.2 时序数据模型与存储原理Prometheus存储的每一条数据都叫一个样本sample它由三部分组成指标名、一组标签、一个浮点值加上时间戳。这里最关键的设计是标签体系。比如http_requests_total{methodGET, endpoint/api/users, status200}通过不同标签的组合你可以把同一个指标拆分成无数个维度的时间序列然后随时用PromQL按标签做过滤、聚合、分组。理解这个数据模型后最需要记住的一点是序列的标识是由“指标名标签组合”唯一决定的而不只是指标名。这带来一个操作层面的深刻影响——你一旦在一台机器上采集了带某些标签的数据比如实例IP、机房、应用名那任何一张新的标签组合都会形成一条新的时间序列。如果标签集合膨胀序列数量就会爆炸直接推高内存和磁盘占用。这就是业界常说的“高基数问题”我在后面第五章会具体讲。存储方面Prometheus自带的TSDB时序数据库其实是很多人容易低估的一块。它内部把时间序列数据按每2小时切成一个blockblock内部再做压缩和索引。写入的数据先进内存Head Block在一定条件下刷到磁盘查询时会合并多个block的结果。这个2小时的切割粒度是官方调试了多年后确定的兼顾了写入吞吐和查询性能。数据落盘后会做压缩压缩算法对浮点时间序列的效果非常好我们在一个中等规模集群上看过原始未压缩数据的磁盘占用大约是每秒样本数乘2字节左右压缩后差不多能降到原来的十分之一。再给一个可以直接用的磁盘与内存估算公式。如果你每秒采集约10万个样本每样本按2字节算压缩前一天的原始数据量是100000 * 86400 * 2 / 1024 / 1024 / 1024 ≈ 16GB。Prometheus压缩率按10倍算一天实际落盘大概是1.6GB。内存的话官方给出的经验线是活跃序列数乘以约2KB到8KB。假设你有100万条活跃序列内存就要预留4GB到8GB。这个估算方法我用了很久偏差基本在可接受范围内。配置机器时把系统内存的20%~30%留给操作系统的页缓存对查询性能帮助也很大很多人忽略这点。2. 采集层的细节与核心配置2.1 Node Exporter与常用采集器选型Prometheus本身不负责采集具体业务指标它只按配置去抓取HTTP接口。真正干脏活累活的是Exporter——每个Exporter负责把某一类系统或组件的状态转换成Prometheus能理解的指标格式。最常见的当然是Node Exporter它采集的是Linux主机层面的指标CPU、内存、磁盘、网络、文件系统、负载等。它的安装非常轻量官方提供二进制包解压即可运行。我一般习惯直接下载tar包放到/opt/node_exporter/下然后用systemd托管。需要注意的一个细节是Node Exporter默认开启的采集器已经覆盖监控主机所需的绝大多数指标比如node_cpu_seconds_total、node_memory_MemTotal_bytes、node_filesystem_avail_bytes。但如果你的机器上有特殊的硬件或文件系统可能需要手动开启对应collector比如--collector.diskstats默认就是开启的而--collector.supervisord这种就需要显式指定。启动命令里我强烈建议加上--collector.systemd --collector.tcpstat这两个一个能监控systemd服务状态一个能看TCP连接数在排障时非常有用。另外一个不太多人知道但值得加的参数是--collector.filesystem.mount-points-exclude可以排除掉那些重复或者无意义的挂载点比如^/(dev|proc|sys|run|var/lib/docker/.)($|/)不然你的磁盘指标面板上会多出一堆无用序列既难看又费存储。对于Java应用官方推荐的JMX Exporter有坑我见过很多团队在这里踩翻车。它的原理是通过JVM Agent在启动时挂一个HTTP服务来暴露JMX指标但配置复杂指标粒度又粗。相比之下如果你用的是Spring Boot 2.x以上的版本直接用Spring Boot Actuator把/actuator/prometheus端点暴露出来再在Prometheus的抓取配置里加上这个path体验会顺畅得多——指标命名规范而且自动带上了业务标签。另一次在某公司帮他们做服务治理时线上Java服务频繁Full GC用JVM Exporter配合Grafana面板一眼就看出新生代内存设置不合理这工具比看服务器指标直观多了。2.2 文件服务发现与Kubernetes服务发现这是Prometheus配置里最提升使用体验的一部分。早期大家写监控配置都是用static_configs硬编码目标地址比如scrape_configs: - job_name: node static_configs: - targets: [192.168.1.10:9100, 192.168.1.11:9100]但线上环境不可能一直手动维护这个列表主机扩容、缩容、IP变动都是常态每改一次要reload一次配置运维成本高还容易漏。Prometheus对此提供了服务发现机制其中两个最常用文件发现file_sd和Kubernetes发现kubernetes_sd。文件发现的做法是Prometheus定期读取某个目录下的JSON或YAML文件文件内容就是目标列表。发布系统或CMDB只要负责更新这些文件Prometheus自动感知变化不用人工reload。示例scrape_configs: - job_name: node file_sd_configs: - files: - /etc/prometheus/targets/node/*.yml refresh_interval: 30s对应目标文件/etc/prometheus/targets/node/node1.yml- targets: - 192.168.1.10:9100 - 192.168.1.11:9100 labels: env: prod region: cn-east-1通过给每个target打上env、region这样的标签后续在查询和告警里就能按业务维度做区分等于在采集层就把数据的“归属权”定了。Kubernetes服务发现类似只不过目标列表是从K8s API动态拉取的Pod一创建Prometheus就能自动发现Pod销毁后自动下线全程零人工介入。可以这样写scrape_configs: - job_name: kubernetes-pods kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: true - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] action: replace target_label: __metrics_path__ regex: (.)这四行relabel规则几乎是K8s监控的标配它的含义是只抓取打了prometheus.io/scrapetrue注解的Pod并且如果这个Pod还标了prometheus.io/path就用它覆盖抓取路径。这样业务方只要在Deployment的Pod模板里加几个注解就能决定自己的服务要不要被监控权限完全下放给应用团队运维不需要为每个新服务单独改一份抓取配置。这也是Prometheus能做到大规模监控而不被配置拖垮的关键设计。3. 从零搭建一套可用的监控栈PromQL与可视化3.1 最省心的部署方式与参数选择安装Prometheus本体其实很简单我通常直接在官方GitHub的release页面下载对应平台的tar包丢到服务器上就完事。目录结构大致是这样的/opt/prometheus/ ├── prometheus # 主程序 ├── promtool # 配置校验工具 ├── console_libraries/ ├── consoles/ ├── data/ # TSDB数据目录 └── prometheus.yml # 主配置文件启动时至少需要关心的启动参数有这几个--config.file指定配置文件路径--storage.tsdb.pathTSDB数据存放目录--storage.tsdb.retention.time数据保留时间这个必须提前规划。如果只关心最近一周的监控设成7d就行想查30天趋势就30d。不设置的话默认保留15天磁盘不够时会有数据被提前删除的风险。--web.listen-address默认监听9090端口按需修改--storage.tsdb.wal-compression开启后预写日志会压缩能省一些磁盘官方建议开启。用systemd托管的服务文件我贴一份你可以直接改路径使用[Unit] DescriptionPrometheus Server Afternetwork.target [Service] Userprometheus Groupprometheus Typesimple ExecStart/opt/prometheus/prometheus \ --config.file/etc/prometheus/prometheus.yml \ --storage.tsdb.path/var/lib/prometheus \ --storage.tsdb.retention.time30d \ --storage.tsdb.wal-compression Restarton-failure RestartSec5s [Install] WantedBymulti-user.target配置文件的抓取部分像下面这样规划就比较合理global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: prometheus static_configs: - targets: [localhost:9090] - job_name: node file_sd_configs: - files: - /etc/prometheus/targets/node/*.ymlscrape_interval是所有抓取任务的默认周期线上我建议设成15秒不要小于10秒。抓太勤对业务方接口是有压力的而且会产生大量重复数据查询又不一定用得到这么高的精度。evaluation_interval是告警规则评估周期默认15秒也就是每15秒检查一次当前指标是否触发了告警条件这已经很快了。如果你的告警阈值本身是针对长时间持续状态的10秒和15秒的评估差别完全可以忽略。配置改完后用./promtool check config prometheus.yml做一次校验再reload能避免一堆低级错误这是官方推荐的流程别嫌麻烦跳过它。3.2 PromQL查询那些天天用的表达式PromQL是Prometheus的查询语言也是很多人觉得有学习门槛的地方。但如果你只是想日常看监控、写告警需要掌握的其实不超过十个核心函数和语法。先记住两个最基础的逻辑rate()和irate()。它们都用来计算Counter类型指标的变化速率区别在于rate()是计算指定时间窗口内的平均速率适合画长期趋势irate()算的是最近两个样本之间的瞬时速率适合观察突然的流量抖动。日常查CPU使用率我几乎固定用下面这条100 - (avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m])) * 100)这个表达式意思是先取过去5分钟CPU空闲时间的平均速率再按实例分组求平均值最后用100减去它得到的就是CPU使用率。这里之所以先取空闲再减是因为CPU的idle值是一个累计Counter直接取绝对值看不出使用率必须先转成速率才行。这也是Prometheus监控Counter指标时最容易犯错的地方——拿Counter原始值去画图出来的全是锯齿状的递增曲线毫无参考价值。内存使用率是另一种典型写法(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100这条不需要rate因为内存是Gauge类型取的是瞬时值直接做除法就行。从这里你能直观体会到Counter与Gauge两类指标在查询上的根本区别。接口级别的QPS和错误率在业务侧如果暴露了http_requests_total{status200}这样的指标可以这么查sum by (endpoint) (rate(http_requests_total[5m]))错误率就是做一次除法sum(rate(http_requests_total{status~5..}[5m])) / sum(rate(http_requests_total[5m]))这里的status~5..是正则匹配筛选所有以5开头的HTTP状态码。这一通操作下来你就能在自己的监控面板上看到每个接口的请求量和错误率跟团队里的同学对线上问题做分析时用这套数据说话比什么都有效。查询之外还有几个用得极其频繁的函数topk(5, 指标)用于看Top Nsum by (label)用于按标签维度聚合histogram_quantile()用于从Histogram指标里计算百分位数比如P99延迟。这些函数组合起来基本能覆盖日常95%以上的查询需求。4. 告警体系从一条规则到“让人真想处理”的通知4.1 告警规则的状态机Prometheus的告警规则写在独立的规则文件里主配置通过rule_files字段引入。规则的基本结构是这样groups: - name: node_rules rules: - alert: NodeHighLoad expr: node_load1 4 for: 5m labels: severity: warning annotations: summary: {{ $labels.instance }} 负载过高 description: {{ $labels.instance }} 的1分钟负载达到 {{ $value }}这里最关键的一个字段是for它表示规则表达式持续满足多长时间后才会真正触发告警。比如上面这条写的是node_load1 4持续5分钟才告警这能过滤掉绝大多数瞬时抖动。我见过很多团队以为prometheus告警很笨、老发误报其实八成是for设得太短甚至没设。比如某次发布过程中CPU瞬间冲高1分钟就回落如果没有for告警会在1分钟评估周期内触发然后立刻恢复制造一轮无意义的告警噪声。加个for: 5m就能很平滑地过滤掉这类情况。每条告警从产生到恢复会经历三个状态Pending条件满足但还没到for时长→Firing条件持续满足正式触发告警→Resolved条件恢复自动消除。在Alertmanager的Web页面上能清楚看到这些状态变化这也是排查“为什么没收到告警”的第一站——如果是Pending说明只是时长还不够如果页面压根没有这条规则说明配置没加载或表达式写错了。告警规则的labels和annotations经常被人忽略但其实它们对后续的路由分派至关重要。labels是用来给告警打标签的比如severity: warning或severity: criticalAlertmanager根据这些标签决定走哪条通知渠道annotations是给告警补充人类可读的信息可以用模板引用当前值、实例名、阈值等。写告警时把这两个字段设计好后面维护通知逻辑能省一大半心。4.2 Alertmanager把告警变成“该看的信息”Prometheus负责发现异常Alertmanager负责把异常通知到人。告警规则触发Firing后Prometheus会把告警推给Alertmanager由它统一做分组、去重、抑制和发送通知。Alertmanager配置文件的核心是route和receivers两部分。route决定了告警的路由规则receivers定义了通知渠道。一个最简且完整的配置大概是route: group_by: [alertname] group_wait: 10s group_interval: 5m repeat_interval: 4h receiver: default routes: - match: severity: critical receiver: page-oncall receivers: - name: default webhook_configs: - url: http://wechat-webhook:8080/send - name: page-oncall webhook_configs: - url: http://pager-service:8080/notify重点说下group_by、group_wait、group_interval和repeat_interval这四个参数它们直接决定告警的“烦人程度”。group_by: [alertname]含义是按告警名称分组——同一类告警只发一条通知而不是每台机器都给你轰炸一条。group_wait: 10s是分组后等待10秒再发这给同一组内的其他告警留了聚合窗口。group_interval: 5m表示如果同一组内持续有新的告警至少隔5分钟才发一次新通知避免同一问题不停地刷屏。repeat_interval: 4h是最核心的“喘气”配置——同一个告警如果一直没恢复也至少4小时才重复提醒一次防止值班的人被同一个问题连续骚扰几小时。我在实际项目中把值班首页接到企业IM的Webhook机器人上关键告警往专门的“监控告警群”里推非关键告警只记日志。告警模板里除了标题和描述一定带上当前值、时间、实例IP和排查跳转链接。这些信息可以在annotations的description里通过模板语法拼接出来。一条告警如果光说“CPU高”收到的人不知道该干嘛但如果写上“负载已持续5分钟超过4当前值7.2实例IP 10.x.x.x请优先查看PHP-FPM进程”那处理效率完全是两个量级。这也是我在教团队同事写PromQL时反复强调的一点告警的目标不是把异常报出来而是让人在30秒内知道从哪里开始查。5. 常见问题与性能调优实录5.1 磁盘占用飙升与数据保留策略很多人第一次跑Prometheus跑着跑着发现磁盘满了这几乎是必经的一课。原因不外乎三个一是保留时间设太长二是抓取周期太密三是目标上暴露的指标太杂。还是用前面那个估算公式你可以根据实际每秒摄入量先大致算一下每秒样本数 * 86400 * 保留天数 * 2字节压缩前 / 压缩率。如果发现数据量远超预期第一件事是把--storage.tsdb.retention.time缩短到实际业务需要的最短值比如7天或15天第二件事是拉长抓取周期从15秒改成30秒存储量直接减半第三件事是在抓取配置里用metric_relabel_configs把不需要的高基数指标过滤掉。比如这样一个场景某公司在K8s环境里给每个Pod都打上了deployment、pod、namespace等多个标签而且业务埋点又爱单独立Counter序列数一下子冲到几百万条Prometheus内存直接被打爆。处理办法是在metric_relabel_configs里丢弃纯调试用的指标以及那些明显不可能用于告警或分析的明细标签scrape_configs: - job_name: app kubernetes_sd_configs: - role: pod metric_relabel_configs: - source_labels: [__name__] regex: app_debug_.* action: drop这不是让人少采集有用的东西而是在“全都采”和“采有用的”之间做出选择。Prometheus毕竟是运维工具不是数据中心它该采集的是能支撑告警和趋势分析的指标而不是把业务日志也塞进来。很多用了一段时间Prometheus的人会觉得“这玩意怎么那么吃内存”回头一看抓取配置里面躺着几十个从网上复制来的exporter配置每个都开了一大堆默认collector采集了一堆压根没人看的指标。所以采集层做减法往往比给服务器加内存更有效。5.2 Targets Down与查询变慢的排查思路监控系统本身也会出问题最常见的一个现象是页面上某个Target的状态一直是Down。碰到这种问题我的排查顺序固定是这样第一看Prometheus自身的日志有没有抓取超时或连接拒绝的记录。第二在Targets页面直接点开目标看Prometheus抓取时报的具体错误常见的是connection refused、context deadline exceeded、403 Forbidden三种。对应处理方式分别是目标服务没启动或端口不对、目标响应太慢导致超过scrape_timeout默认10秒、目标接口做了访问控制但没给Prometheus加白名单。第三看抓取路径有没有配错。很多应用实际暴露指标的路径是/metrics但有些应用是/actuator/prometheus或/metrics/custom配置里不写对抓到404还一脸懵。查询变慢的情况首先要检查查询里有没有跨很长的[5h]窗口去范围查询大量老数据这会让TSDB扫描很多block。其次看查询里的标签匹配是否能用上索引如果查询条件里带了label!~...这类否定正则基本就会全表扫描。最有效的优化手段是缩小查询范围时间范围缩到最近30分钟标签匹配精确到具体instance或job并优先用sum、avg等聚合函数减少返回的序列数。我曾见过一个Grafana面板同一行有三个查询每个都扫24小时全量数据结果图表加载要十几秒后来把时间范围缩到6小时加载时间降到1秒以内根本没动任何底层配置。5.3 内存暴涨背后的真相活跃序列数和高基数Prometheus内存占用过高绝大多数情况都和活跃序列数直接相关。Prometheus会把最近写入的样本缓存在内存里序列越多内存占用越高。什么是活跃序列简单说就是“当前正在采集且最近有新增样本的时间序列”。如果一个指标有3个标签组合成1万条序列而且每15秒都来一个新样本那这1万条序列都算活跃。高基数问题最常见于两类情况一类是把user_id、url、ip这些高维度信息塞进标签——标签的值可以无限多序列数就跟着无限膨胀另一类是在K8s环境里把Pod标签全量保留Pod每次重启就产生一组新的标签组合。遇到这类问题根治的办法是让业务方改写埋点把高基数维度从标签移到指标值里或者干脆丢弃。Prometheus社区官方给出的建议是单实例长期活跃时间序列不要超过1000万但实际经验是如果你用一台内存只有16GB的常规虚机200万到300万活跃序列就已经捉襟见肘了。我自己的经验线是普通场景下单实例平均500万条以内内存32GB起步基本可以顺畅运行。超过这个量级就应该考虑分片、联邦或者远端存储方案了。此外还有一个冷门但常见的坑Prometheus数据目录里如果有很多未完成压缩的wal文件说明实例异常重启过或者写入压力过大重启后可能会花很长时间做回放这段时间内查询会明显卡顿。我遇到过一次某团队没配systemd的Restarton-failurePrometheus被OOM杀掉后一直处于半死状态数据目录里堆了几百个WAL文件重启后花了将近两个小时才恢复服务。所以Kubernetes或systemd里的健康检查、自动重启、以及数据盘容量监控这三样在做Prometheus高可用时必须同步配上。最后再分享一个我踩过几次坑之后固定的习惯每一条新加的告警规则都会先在测试环境用promtool跑一遍然后用生产环境的Prometheus数据做一次回放校验确认表达式输出的值跟预期一致才上生产。原因很简单告警规则里的PromQL如果写错表面上不会报错但它会让你在半夜收到一堆莫名其妙的告警或者在真正出事的时候静默无声。监控系统最重要的不是炫酷面板也不是全指标采集而是当线上真正出问题时它能给你一条准确、及时、不吵人的消息。这比任何花哨的展示都更有价值。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →