☰
Prometheus+Grafana监控告警实战:从Docker到K8s
2026/9/30 8:44:19 网站建设 项目流程

Prometheus 加 Grafana,这两个名字放在一起,基本就是当下中小规模监控体系的默认答案。我最早接触这套东西的时候,项目的监控还停留在脚本加邮件的方式,出故障全靠用户投诉,后来换成 Prometheus 采集、Grafana 出图、Alertmanager 发告警这条链路,排查问题的效率完全不是一个量级。这篇文章不打算写成官方文档的搬运,而是把我从零搭建、踩坑、调参的完整过程整理出来,覆盖单机 Docker Compose 起步、告警规则配置、Grafana 面板与报错排查、K8s 集群内的落地方式这几个环节。不管你是刚接手运维的新手,还是想把现有监控换掉的开发,都能照着走一遍。

1. 先把这套组合的性质说清楚

1.1 监控到底在解决什么问题

很多人对监控的理解停留在“服务器挂了发短信”,这个认知会让你在做技术选型时走偏。监控实际要回答的是四类问题:系统现在是否健康、什么时候会不健康、出了故障问题在哪一层、故障恢复后趋势是否正常。这四类问题对数据的要求完全不同——第一类要实时,第二类要历史趋势,第三类要足够细的维度,第四类要长期留存。传统方案通常只能在某一个点上做得好,比如 Nagios 擅长“活着没活着”的检查,但你想查“昨天下午三点哪个接口的 P99 抖动”,它给不出答案。

Prometheus 的设计恰好把这几类需求串起来了:它以固定周期抓取指标,天然带时间维度;它用标签(Label)给每条数据打上多维度属性,让“按实例、按接口、按集群聚合”变成一行查询;它自带的 PromQL 支持对时间序列做速率、分位数、预测运算,把“什么时候会满”也变成可计算的问题。而 Grafana 解决的是最后一公里的表达问题——把一堆查询结果变成人一眼能看懂的图,并且支持变量、模板、告警面板这些提升日常使用效率的功能。

所以这套组合的价值不是“两个工具装在一起”,而是采集、存储、计算、展示四件事各自有清晰的边界,且这条链路每一环都可以替换和扩展。

1.2 Prometheus 的拉模型意味着什么

Prometheus 是开源的,采用 Apache 2.0 许可,隶属于云原生计算基金会,目前已经是毕业项目。它的核心采集方式是 Pull(拉取),也就是 Prometheus 主动去目标暴露的 HTTP 端点抓数据,默认路径是/metrics。这一点和 Zabbix、部分商业 APM 的 Push(推送)模型是相反的,理解这个差异非常关键,因为它直接决定了你的网络规划、防火墙策略和服务发现方式。

拉模型的好处有几个。第一,目标的存活状态本身就是一种信号,抓不到就是异常,不需要额外做心跳检测。第二,采集频率、超时、并发完全由 Prometheus 侧控制,调整起来不影响被监控方。第三,目标清单集中管理,在 K8s 这类动态环境里配合服务发现非常好用。坏处也很明显:短生命周期任务(跑几秒就退出的批处理)根本来不及被抓,这类场景就得靠 Pushgateway 中转;另外被监控端必须对 Prometheus 网络可达,跨网段时需要提前打通。

注意:拉模型下如果被监控端做了鉴权或者只能反向访问,优先考虑 Pushgateway 或者 OpenTelemetry Collector 中转,不要为了省事把鉴权关掉。

1.3 Grafana 只是“画图的”,但它决定你排查问题的速度

Grafana 本身不存储数据,它是个查询代理加可视化层,接 Prometheus、接关系型数据库、接日志系统都可以。很多新手会误以为“装了 Grafana 就有监控了”,实际上 Grafana 没数据的时候只是空白面板,数据完全来自下游数据源。

但不要因此低估它的分量。故障现场最关键的是三十秒内看到正确的图,而一张好的 Grafana 面板和一张烂的面板,差距可能就是十分钟。我在实际项目里总结下来,Grafana 面板的设计要点是:指标要在正确的聚合粒度上、时间范围要默认贴合排查习惯(比如默认最近 6 小时而不是最近 5 分钟)、变量要能快速切换实例和集群、阈值线要画在图里而不是靠眼看。这些细节和 Prometheus 的采集配置是同等重要的。

1.4 什么场景该用,什么场景不该用

Prometheus 走的是数值型时间序列路线,它不适合做日志全文检索,也不适合做链路追踪,这两件事交给专门的日志系统和链路系统更合适。它的强项是指标:QPS、延迟、错误率、资源使用率、队列深度这类可以量化、可以聚合的数字。

另外,Prometheus 的单机存储模型决定了它不适合超大规模长期存储。本地 TSDB 在几百万活跃序列的规模下表现还不错,但如果你的序列数上到千万级或者需要保留一两年,就得考虑远程存储方案,把数据写到对象存储或者专门的时序数据库里。这不是说它不好,而是要知道边界在哪里,避免一开始就选错路线。

2. 数据模型是理解一切的地基

2.1 指标名、标签与时间序列

Prometheus 里每一条数据都由三部分组成:指标名(Metric Name)、一组标签(Labels)、以及时间戳加值。指标名描述“这是什么”,比如node_cpu_seconds_total;标签描述“哪一个”,比如instance="10.0.1.5:9100"、job="node"、mode="idle"。指标名加完整标签组合唯一确定一条时间序列(Time Series)。

这个模型最容易被忽略的点是标签的基数(Cardinality)。标签值的组合数量就是序列数量,如果把用户 ID、请求 UUID、会话 ID 这种高基数的东西做成标签,序列数会瞬间爆炸,Prometheus 内存会被吃光。我见过一个真实案例:有人在标签里塞了 URL 的完整路径,结果每条不同的 ID 都变成独立序列,几天之后内存直接打满,抓取开始大面积超时。正确做法是把这类信息放到日志里,指标层面只保留有限枚举的维度。

反过来,标签也不宜太少。只有指标名没有维度的数据,查询时无法区分来源,出了故障定位不到具体实例。一般来说,job、instance、cluster、service、env这几个是基础维度,再根据业务特点补充有限的几个。

2.2 四种指标类型和它们适用的场景

Prometheus 客户端库定义了四种指标类型,理解它们的差异才能写出正确的 PromQL:

类型含义典型用途查询关键词
Counter只增不减的累计值请求总数、错误总数、CPU 累计时间rate、increase
Gauge可增可减的瞬时值内存使用量、连接数、温度直接聚合、delta
Histogram分桶统计请求延迟、响应体大小histogram_quantile
Summary客户端计算的分位数延迟分位数(不可聚合)直接读取

Counter 最关键的一点是不要直接看它的绝对值,那个数字从进程启动开始累加,没有任何意义。要用rate()把它转成每秒速率,或者用increase()看某段时间的增量。Counter 在进程重启后会归零,rate()函数会自动处理这种重置,但如果用delta()就会算出负值,这是新手最常见的错误之一。

Histogram 和 Summary 都用来做分位数,区别在于 Histogram 是服务端分桶、可以在 PromQL 里聚合,Summary 是客户端算好分位数、无法跨实例聚合。生产环境绝大多数情况下应该选 Histogram,因为聚合能力对多实例服务是刚需。

2.3 exporter、Pushgateway、OpenTelemetry Collector 的关系

被监控的程序通常不直接暴露 Prometheus 格式的指标,需要中间层转换,这就是 exporter 的角色。操作系统指标靠node_exporter,MySQL 靠mysqld_exporter,Redis 靠redis_exporter,Nginx 有nginx-prometheus-exporter。这些 exporter 做的事情本质一样:采集本地或远程的状态,转成 Prometheus 文本格式暴露在/metrics上。

Pushgateway 用于短生命周期任务的指标上报。批处理脚本跑完就退出,Prometheus 根本抓不到,所以脚本主动把结果推到 Pushgateway,Prometheus 再从 Pushgateway 抓。这里有个坑要注意:Pushgateway 里的数据不会自动过期,如果脚本不再运行,旧数据会一直保留,导致告警一直基于过期值。所以要么在脚本里显式删除,要么给推送数据加上时间戳并在告警表达式里做判断。

OpenTelemetry Collector 是另一条路。它本身是一个可编程的遥测数据处理管道,能接收 OTLP 协议的数据,也能把数据导出成 Prometheus 格式。具体做法是在 Collector 配置里启用prometheusexporter,它会监听一个端口(常见是 8888 或 8889)并暴露/metrics,同时 Collector 内部收到的 OTLP 指标会转换成 Prometheus 格式从这个端点输出,Prometheus 把那台 Collector 当作普通 target 去抓就行。这个方案适合已有 OTel 埋点体系、不想在应用里额外引入 Prometheus SDK 的团队。

提示:OTel 转 Prometheus 时,单位会被拼到指标名里,比如http_server_duration_milliseconds,同时 Counter 类型会补上_total后缀。写查询的时候如果发现指标名对不上,先去端点上curl看一眼实际输出,别凭记忆写。

2.4 PromQL:从 rate() 到直方图分位数

PromQL 是这套体系的核心竞争力,掌握十条左右的表达式就能覆盖八成的日常查询。以下几个是我用得最多的:

# 单实例 CPU 使用率(排除 idle 模式) 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) # 每秒接收的 HTTP 请求数,按服务聚合 sum by (service) (rate(http_requests_total[5m])) # 5xx 错误占比 sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) # P99 延迟(需要 Histogram 类型指标) histogram_quantile(0.99, sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))) # 磁盘预计 4 小时后写满 predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4 * 3600) < 0

这里有几个容易翻车的细节。rate()的时间窗口至少要包含两个采样点,如果采集间隔是 15 秒,窗口用[1m]是安全的,用[10s]会经常返回空。irate()只看最后两个点,反应快但波动大,适合看瞬时变化,不适合做告警。histogram_quantile()必须在le标签上聚合,漏掉by (le)会得到完全错误的结果。predict_linear()的第二个参数是外推的秒数,负数场景(比如磁盘可用空间趋于 0)要用< 0判断。

3. 单机环境从零搭起来

3.1 组件清单与目录规划

先用最小集合把链路跑通:Prometheus 负责抓取和存储,node_exporter 提供主机指标,Grafana 负责展示。告警组件 Alertmanager 可以稍后加。目录结构我习惯这样规划,好处是挂载清楚、迁移时整个目录拷走就行:

opt/monitor/ ├── prometheus/ │ ├── prometheus.yml │ ├── rules/ │ │ └── node.rules.yml │ └── data/ ├── alertmanager/ │ └── alertmanager.yml └── grafana/ └── data/

如果目标机器不能直接连外网,先在能联网的机器上把镜像拉下来,用docker save导出成 tar 包,拷进去再docker load导入。这个方式在内网环境里非常实用,比临时搭一个私有仓库省事得多。

docker pull prom/prometheus:v2.53.0 docker pull grafana/grafana:11.1.0 docker pull prom/node-exporter:v1.8.1 docker pull prom/alertmanager:v0.27.0 docker save prom/prometheus:v2.53.0 grafana/grafana:11.1.0 \ prom/node-exporter:v1.8.1 prom/alertmanager:v0.27.0 \ -o monitor-images.tar

3.2 Docker Compose 编排文件

版本标签一定要锁死,不要用latest。监控组件升级经常伴随配置格式变更,某次重启拉到了新镜像,配置直接不兼容,故障现场还要处理监控本身,非常被动。

services: prometheus: image: prom/prometheus:v2.53.0 container_name: prometheus restart: unless-stopped user: root ports: - "9090:9090" volumes: - /opt/monitor/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - /opt/monitor/prometheus/rules:/etc/prometheus/rules:ro - /opt/monitor/prometheus/data:/prometheus command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--storage.tsdb.retention.time=15d" - "--storage.tsdb.retention.size=20GB" - "--web.enable-lifecycle" - "--web.enable-admin-api" node-exporter: image: prom/node-exporter:v1.8.1 container_name: node-exporter restart: unless-stopped pid: host network_mode: host volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - "--path.procfs=/host/proc" - "--path.sysfs=/host/sys" - "--path.rootfs=/rootfs" - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)" grafana: image: grafana/grafana:11.1.0 container_name: grafana restart: unless-stopped ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=改成你自己的密码 - GF_USERS_ALLOW_SIGN_UP=false volumes: - /opt/monitor/grafana/data:/var/lib/grafana

--web.enable-lifecycle这个参数值得单独说。加上它之后改完配置不用重启容器,直接curl -X POST http://localhost:9090/-/reload就能热加载,日常调采集配置时能省掉大量重启等待。但要注意它同时也意味着任何能访问到 9090 端口的人都能触发重载,所以这个端口绝对不要暴露到公网。

3.3 prometheus.yml 采集配置逐行说明

global: scrape_interval: 15s evaluation_interval: 15s external_labels: cluster: prod-shanghai alerting: alertmanagers: - static_configs: - targets: ["alertmanager:9093"] rule_files: - "/etc/prometheus/rules/*.yml" scrape_configs: - job_name: "prometheus" static_configs: - targets: ["localhost:9090"] - job_name: "node" static_configs: - targets: ["10.0.1.5:9100", "10.0.1.6:9100"] labels: env: prod role: app relabel_configs: - source_labels: [__address__] regex: "([^:]+):.*" target_label: instance replacement: "$1" metrics_path: /metrics scrape_timeout: 10s

逐项拆解一下。scrape_interval决定数据点密度,15 秒是通用起点,核心业务可以降到 10 秒,边缘服务放宽到 30 秒甚至 60 秒。evaluation_interval是告警规则的执行周期,一般和采集间隔保持一致就够。external_labels在联邦部署或者远程写入时特别有用,给所有数据打上集群标识,避免多集群数据混淆。

relabel_configs是配置里最容易被跳过但最该掌握的部分。上面那条规则做的是把10.0.1.5:9100里的端口去掉,只留 IP 作为instance标签值,这样查询时看到的实例名更干净。relabel 的执行时机是在抓取之前,可以对__address__、__meta_*这类内部标签做改写、丢弃、替换,K8s 服务发现里主要靠它把 Pod 元数据转成业务标签。

注意:relabel_configs和metric_relabel_configs是两个不同的阶段,前者作用于目标,后者作用于抓到的样本。想丢弃某个指标要用后者,写错阶段是排查时的常见困惑点。

3.4 容量与保留时间的估算

本地 TSDB 的容量不能凭感觉设,简单算一下心里有底。假设你有 2000 条活跃时间序列,采集间隔 15 秒:

每天样本数 = 2000 × (86400 / 15) = 11,520,000 Prometheus 压缩后每样本约 1.5 字节 每天磁盘占用 ≈ 11,520,000 × 1.5B ≈ 17MB 保留 15 天 ≈ 260MB

这个数字看起来很小,但要注意两个放大因素。一是实际序列数往往远超预估,因为每个标签组合都是独立序列,一个 20 实例、每个实例 300 条序列的集群就是 6000 条序列。二是压缩率受数据特征影响,波动大的数据压缩效果差。我的经验是按计算值的 3 到 5 倍预留磁盘,同时给retention.size设一个硬上限作为兜底,避免磁盘写满引发连锁故障。

retention.time和retention.size同时配置时,哪个先触发就按哪个清理。如果 Grafana 面板需要看 30 天的趋势而本地只留 15 天,那要么延长保留时间,要么接远程写入把长期数据放到对象存储。

4. 告警链路:从规则到 Alertmanager

4.1 告警规则的三种典型写法

告警规则写在规则文件里,由 Prometheus 周期性求值,触发后把告警推给 Alertmanager。我把常用的规则分成三类:资源类、服务类、趋势类,写法各有侧重。

groups: - name: node-resource interval: 30s rules: - alert: NodeCpuHigh expr: | 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85 for: 10m labels: severity: warning team: infra annotations: summary: "实例 {{ $labels.instance }} CPU 使用率持续偏高" description: "当前值 {{ $value | printf \"%.1f\" }}%,已持续 10 分钟" - alert: NodeMemoryPressure expr: | (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9 for: 5m labels: severity: critical annotations: summary: "实例 {{ $labels.instance }} 内存紧张" - alert: DiskWillFillIn4Hours expr: | predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[6h], 4 * 3600) < 0 for: 30m labels: severity: warning annotations: summary: "挂载点 {{ $labels.mountpoint }} 预计 4 小时内写满"

服务类规则通常基于错误率和延迟:

- name: service-slo rules: - alert: HighErrorRate expr: | sum by (service) (rate(http_requests_total{status=~"5.."}[5m])) / sum by (service) (rate(http_requests_total[5m])) > 0.05 for: 5m labels: severity: critical annotations: summary: "服务 {{ $labels.service }} 5xx 比例超过 5%" - alert: LatencyP99High expr: | histogram_quantile(0.99, sum by (le, service) (rate(http_request_duration_seconds_bucket[5m])) ) > 1 for: 10m labels: severity: warning

for字段是三条规则里最重要的配置。它表示条件必须持续满足多久才真正触发,作用是过滤抖动。CPU 偶尔冲到 90% 是正常的,持续 10 分钟才是问题。但for也不能乱设:核心接口的错误率告警设 10 分钟,用户已经投诉十分钟了,设 2 分钟更合适;而磁盘预测这类慢变量,设 30 分钟甚至 1 小时都没问题。

4.2 Alertmanager 路由与分组参数

告警触发后进入 Alertmanager,这里做的是去重、分组、静默、路由、通知。配置文件的核心是 route 和 receivers:

global: resolve_timeout: 5m route: receiver: default-webhook group_by: ["alertname", "cluster", "service"] group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - matchers: - severity = "critical" receiver: oncall-webhook group_wait: 10s repeat_interval: 1h - matchers: - team = "infra" receiver: infra-webhook receivers: - name: default-webhook webhook_configs: - url: "http://notify-gateway:8080/default" - name: oncall-webhook webhook_configs: - url: "http://notify-gateway:8080/oncall" send_resolved: true - name: infra-webhook webhook_configs: - url: "http://notify-gateway:8080/infra" inhibit_rules: - source_matchers: - severity = "critical" target_matchers: - severity = "warning" equal: ["alertname", "instance"]

四个分组参数的行为要理解透。group_by决定哪些告警会被合并成一条通知,上面按告警名、集群、服务分组,同一个服务下十台机器的同类告警会被压成一条,避免通知轰炸。group_wait是首次通知前的等待时间,给同一批告警留出聚集窗口,critical 设 10 秒是为了尽快发出,普通告警 30 秒足够。group_interval是同一分组内新增告警后再次通知的间隔,repeat_interval是告警未恢复时的重复提醒周期,设 4 小时意味着同一个问题一天最多提醒 6 次,这个频率我认为比较合理,设 5 分钟会让人麻木,设 24 小时又容易漏掉。

inhibit_rules是抑制规则,用得非常值。当某台机器的 critical 告警已经发出,同一台机器上的 warning 就没有通知价值了,抑制掉能大幅降低噪音。关键是equal字段要选对,写["instance"]表示同实例内的告警互相抑制,如果写成["alertname"]就变成跨实例抑制,会误伤其他机器的告警。

4.3 告警治理的几个硬经验

告警配好只是开始,真正的难点是让它保持可用。我踩过最深的坑是告警疲劳:一开始恨不得所有指标都配上规则,结果一天几十条通知,两周之后所有人都不看了,真出故障反而没人响应。

后来我给自己定了几条规矩。第一,每条告警必须有明确的处置动作,如果需要人工判断“这个要不要处理”,那它就不该是告警,应该是面板上的图。第二,告警数量控制在值班人员半小时内能处理完的量级,超出的部分降级成日报。第三,critical 必须对应可量化的业务影响,CPU 高不等于业务受影响,用户看到的错误率上升才是。第四,定期回顾告警记录,把连续一个月没触发但配置复杂的规则删掉,把频繁触发且无需干预的规则调阈值或者加for。

还有一个实操细节:告警的annotations里一定要带runbook_url或者处置步骤的简短说明。凌晨三点收到告警的人脑子是懵的,如果通知里只有“CPU 高”,他还得先去翻文档,这中间的十分钟可能就是故障扩大的时间。

5. Grafana 落地:数据源、面板、报错排查

5.1 数据源接入与那个烦人的 legacy queries 报错

Grafana 接入 Prometheus 很简单,Configuration 里加 Data Source,URL 填http://prometheus:9090,保存后点 Save & Test 看到绿色提示就行。如果你的环境用固定 IP 加端口,注意 Grafana 容器内能不能解析到这个地址,跨容器通信用服务名更稳。

比较容易踩的是升级后的报错:failed to upgrade legacy queries datasource xxx was not found。这个错误的根源在于面板 JSON 里保存的数据源引用方式。早期版本的 Grafana 面板里,数据源是按名称(name)引用的,新版本改成了按唯一标识(uid)引用。升级时如果数据源被重新创建、uid 变了,或者数据源名称和面板里记录的名称不一致,打开面板就会报这句。

处理方式分三种。第一种最省事:在 Grafana 界面里手动创建一个数据源,在 uid 输入框里明确填成面板期望的那个值(把报错里的im7_otuvz抄进去),这样旧的引用就能重新对应上。第二种是通过 API 批量修改面板 JSON:

# 导出所有仪表盘 for uid in $(curl -s -H "Authorization: Bearer $TOKEN" \ "http://grafana:3000/api/search?type=dash-db" | jq -r '.[].uid'); do curl -s -H "Authorization: Bearer $TOKEN" \ "http://grafana:3000/api/dashboards/uid/$uid" > "dash-$uid.json" done

拿到 JSON 之后,把里面所有"datasource"字段统一替换成{"type":"prometheus","uid":"<新数据源uid>"},再通过/api/dashboards/db接口推回去。第三种是配置层面统一:在 provisioning 的 datasource yaml 里写死uid,只要这个值不变,以后怎么重建都不会出问题。我在生产环境用的就是第三种,一劳永逸。

apiVersion: 1 datasources: - name: Prometheus type: prometheus uid: prom-main access: proxy url: http://prometheus:9090 isDefault: true jsonData: timeInterval: 15s

timeInterval这个参数容易被忽略,它告诉 Grafana 最小的采集间隔,影响$__rate_interval这类内置变量的计算。设成和scrape_interval一致能避免查询里出现空数据点。

5.2 面板与变量的实用写法

变量(Variables)是让一张面板复用的关键。我一般至少配三个:实例、服务和环境。类型选 Query,查询语句直接用 label_values:

# 变量 instance label_values(node_cpu_seconds_total, instance) # 变量 service,只列出有流量的服务 label_values(http_requests_total, service) # 变量 env,从 external_labels 里过滤 label_values(up{cluster="$cluster"}, env)

配置变量时记得勾选 Multi-value 和 Include All,配合=~"$instance"这种正则匹配使用。空值时=~""会返回全部序列,这正好符合“全选”的语义,但如果变量没配置好的话也会导致查询结果爆炸,需要留意。

面板本身有几个提升可读性的做法。阈值线用 Field 的 Thresholds 配置,比如错误率图上画一条 5% 的红线,一眼就能看出是否越界。单位一定要设对,Prometheus 里很多指标是秒为单位,Grafana 里选 seconds 会自动显示成 ms 或者 s,选错了会让人误判量级。时间范围我建议默认设成最近 6 小时,因为排查故障通常需要看故障发生前后的对比,默认 5 分钟太短。

还有一个很实用的技巧是给面板配$__interval自适应分辨率。查询里写rate(http_requests_total[$__rate_interval]),Grafana 会根据当前时间范围自动调整窗口,看一天的数据时不会因为窗口太碎而卡死浏览器。这个变量比手写[5m]好用太多,强烈建议全局替换。

5.3 在 K8s 里换一套部署姿势

容器环境下的部署思路和单机完全不同,因为 Pod 是动态的,静态 targets 列表根本不适用。主流做法是上 Prometheus Operator,通过 CRD 来声明式管理。

整体链路是这样的:kube-prometheus-stack这个 Helm chart 一次性装好 Prometheus、Alertmanager、Grafana、node-exporter、kube-state-metrics 以及一堆默认规则。Prometheus 实例由Prometheus这个 CR 定义,采集目标由ServiceMonitor和PodMonitor定义,告警规则由PrometheusRule定义。

apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: my-app labels: release: kube-prometheus-stack spec: selector: matchLabels: app: my-app endpoints: - port: metrics interval: 15s path: /metrics relabelings: - sourceLabels: [__meta_kubernetes_pod_name] targetLabel: pod

这里有个特别容易让人抓狂的点:ServiceMonitor 上的release标签必须和 Prometheus CR 里的serviceMonitorSelector匹配,否则 Prometheus 根本不会去发现它,你在界面里配置页看不到任何 target,会以为是网络问题。装完 chart 之后第一件事就是确认这个选择器,然后给业务侧的 ServiceMonitor 补上对应的标签。

另一个注意点是资源限制。Prometheus 在 K8s 里采集目标和序列数都比单机多一个量级,requests 给少了会被频繁驱逐,limits 给少了会 OOM。我的经验是按活跃序列数估算,每 100 万序列大约需要 2 到 3GB 内存,再加上查询时的峰值开销。同时给存储挂上 PVC,不要用 emptyDir,Pod 重建数据就没了。

6. 常见问题速查与踩坑记录

6.1 采集类

采集类问题在产品上表现为面板出现断点,或者 Targets 页面显示 DOWN。定位顺序我一般是:先看 Targets 页面的 Error 列,它会直接写清楚是连接超时、状态码不对还是格式解析失败;然后到被监控机器上手动curl一次/metrics,确认端点本身是否正常;最后检查网络和防火墙。

现象常见原因处理方式
connection refusedexporter 没启动或端口写错确认进程和监听端口
context deadline exceeded网络不通或响应过慢检查防火墙、调大 scrape_timeout
返回 404metrics_path 配置错误改成实际路径
解析失败输出不是合法文本格式用 promtool check metrics 校验
时有时无的断点采集并发过高降低并发或优化 exporter

promtool这个工具值得装一下,它能校验配置、校验规则、校验指标格式,在改配置之前先跑一遍能避免很多低级错误:

promtool check config /etc/prometheus/prometheus.yml promtool check rules /etc/prometheus/rules/node.rules.yml

6.2 存储与性能

Prometheus 自身的性能问题八成来自序列基数过高。判断方式是看状态页面的头部统计,Head samples和Head series两个数字。如果序列数远超预期,去查一下哪个指标贡献最多:

# 按指标名统计序列数 topk(10, count by (__name__) ({__name__=~".+"})) # 找出某个高基数指标的标签取值分布 count by (path) ({__name__="http_requests_total"})

找到之后处理方式要么是加metric_relabel_configs丢弃,要么是修改埋点,把高基数标签去掉。我遇到过一次http_requests_total里带了完整 URL 的情况,序列数从 2000 涨到 80 万,加上metric_relabel_configs用正则把路径归一化之后立刻恢复正常。

查询慢是另一类问题。PromQL 如果聚合维度写得不合适,会一次性加载巨量样本。优化方向是缩小时间范围、用 recording rules 预聚合高频查询、避免在查询里用{__name__=~".+"}这种全匹配。对于仪表盘上打开就要用的复杂查询,我习惯配一条 recording rule 提前算好:

groups: - name: recording interval: 30s rules: - record: job:http_requests:rate5m expr: sum by (service) (rate(http_requests_total[5m]))

6.3 告警类

告警不触发、重复触发、恢复不通知,这三类问题最常见。不触发先检查表达式在 Graph 页面手动执行的结果,看当前值是否真的越过了阈值,如果值是对的但告警不响,八成是for时间没满足或者规则文件没加载。重复触发通常是group_wait和repeat_interval配得太激进,或者group_by的维度太细导致分组过多。恢复不通知要检查 receiver 里有没有开send_resolved,默认是关闭的。

还有一类问题是告警恢复后状态卡在 firing。这通常是因为表达式在数据缺失时的行为不符合预期,absent()和unless的用法要仔细核对。稳妥做法是给关键告警加一条“指标消失”的伴随规则:

absent(up{job="my-app"} == 1)

6.4 几条我个人总结的实操原则

搭建过程中我给自己立了几条规矩,回头看每一条都是被坑出来的。第一,版本全部锁死并记录在案,升级前先在测试环境跑一周,尤其是 Prometheus 大版本升级时常有配置项废弃。第二,配置文件纳入版本管理,任何改动走评审,监控配置改错的后果比业务代码改错还严重,因为它会让你在出故障时失去观测能力。第三,先保证采集稳定再追求指标丰富,一条稳定运行三个月的简单规则,价值高于二十条天天误报的复杂规则。第四,面板不要追求大而全,首页放四到六个核心指标足够,细节钻取放到下钻面板里。

最后分享一个我觉得很值的小习惯:给每个监控栈写一份“自监控”规则,监控 Prometheus 自己的抓取成功率、告警队列长度、TSDB 压缩状态。监控系统本身挂了却没人知道,这是最尴尬也最危险的情况。上面这些配置我都放在同一个仓库里,新环境直接改改 IP 和标签就能用,比每次从零写省下大量时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询