【架构实战】监控告警Prometheus+Grafana:让系统问题无处遁形

【架构实战】监控告警Prometheus+Grafana:让系统问题无处遁形

字数统计:约 4300 字


开篇故事

2019年双十一前夜,某互联网公司运维团队在最后一次压测中发现了一个诡异的问题:CPU 使用率只有 40%,内存使用率 50%,磁盘 IO 完全正常------但接口响应时间 P99 已经超过了 3 秒。监控面板上一切绿灯,告警一个都没触发。

最后,一个老架构师看了一眼 Grafana 的一个小众指标------GC 暂停时间,发现了问题:CMS GC 每 5 秒就要停顿 800ms。用户虽然不会收到错误,但体感已经烂透了。压测通过了,却在上线第一天就被用户骂了个狗血淋头。

这个故事告诉我们:只监控 CPU 和内存,就像只量体温却量不出血压------基础指标正常,不代表系统真的健康。 本文将以实战为导向,讲解 Prometheus + Grafana 的监控体系设计,从指标体系规划到告警规则配置,再到踩过的那些坑。


一、为什么是 Prometheus + Grafana?

在进入实操之前,先回答一个灵魂拷问:Prometheus + Grafana 是银弹吗?什么场景下不适用?

1.1 监控体系三支柱

任何成熟的监控体系,都离不开三层:

复制代码
┌─────────────────────────────────────────────┐
│          Metrics(指标监控)                  │
│   QPS、延迟、错误率、CPU、内存、磁盘、网络    │
│   → 回答"现在系统稳不稳?"                   │
├─────────────────────────────────────────────┤
│          Logs(日志监控)                    │
│   应用日志、访问日志、错误日志                │
│   → 回答"哪里出了问题?"                     │
├─────────────────────────────────────────────┤
│          Traces(链路追踪)                  │
│   全链路追踪、依赖拓扑、耗时分布              │
│   → 回答"问题是怎么发生的?"                 │
└─────────────────────────────────────────────┘

Prometheus 专注于 Metrics 层,Grafana 负责可视化,ELK/Loki 负责日志,Jaeger/Zipkin 负责链路追踪。四者配合,才是完整的可观测性体系。

1.2 Prometheus 优势与局限

优势

  • Pull 模型(主动拉取),天然支持动态发现
  • 时序数据库优化,查询性能极佳
  • 生态系统丰富,Exporter 覆盖各类中间件
  • Kubernetes 原生支持
  • 完全开源,免费

局限

  • 不适合存储日志和链路追踪
  • 单机版不支持集群扩展(需要 Thanos / Cortex / VictoriaMetrics 等)
  • 写入吞吐有限(但对于中等规模足够)

二、整体架构设计

2.1 经典部署架构

复制代码
┌──────────────────────────────────────────────────────────────┐
│                        Kubernetes Cluster                    │
│                                                              │
│   ┌─────────────┐                    ┌──────────────────────┐│
│   │  应用 Pod   │                    │     Prometheus Pod   ││
│   │ ┌─────────┐ │                    │                      ││
│   │ │Exporter │ │◄─── ServiceMonitor │                      ││
│   │ └─────────┘ │     (自动发现)      │  ┌──────────────┐   ││
│   └─────────────┘                    │  │  Prometheus  │   ││
│                                      │  │  TSDB        │   ││
│   ┌─────────────┐                    │  └──────────────┘   ││
│   │  应用 Pod   │                    │         │           ││
│   │ ┌─────────┐ │◄───────────────────┤         │           ││
│   │ │Exporter │ │  (手动配置 target) │         ↓           ││
│   │ └─────────┘ │                    │  ┌──────────────┐   ││
│   └─────────────┘                    │  │   Grafana    │   ││
│                                      │  │   (可视化)   │   ││
│   ┌─────────────┐                    │  └──────────────┘   ││
│   │   MySQL    │                    │                      ││
│   │ ┌─────────┐ │                    └──────────────────────┘│
│   │ │Exporter │ │◄─── node_exporter / mysqld_exporter       │
│   │ └─────────┘ │                                           │
│   └─────────────┘                                           │
└──────────────────────────────────────────────────────────────┘

2.2 指标体系规划(USE 方法 + RED 方法)

监控指标这么多,哪些才是最重要的?业界有两个经典方法论:

USE 方法(资源层)------用于基础设施:

  • Utilization(利用率):CPU 使用率、内存使用率、磁盘使用率
  • Saturation(饱和度):CPU 队列长度、内存换页率、磁盘 IO 队列长度
  • Errors(错误率):网络丢包率、磁盘坏道率、进程崩溃数

RED 方法(应用层)------用于服务:

  • Rate(请求率):QPS,每秒请求数
  • Errors(错误率):错误请求占比(4xx、5xx)
  • Duration(响应时间):P50、P90、P99、P999

黄金指标(Google SRE 提出的):

复制代码
latency(延迟)→ traffic(流量)→ errors(错误)→ saturation(饱和度)

三、环境搭建

3.1 Helm 安装 Prometheus Operator(推荐方式)

如果你的集群支持 Helm,这是最省事的安装方式:

bash 复制代码
# 添加 Helm 仓库
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# 创建命名空间
kubectl create namespace monitoring

# 安装 Prometheus Operator(含 Grafana)
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set prometheus.prometheusSpec.retention=15d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=50Gi \
  --set grafana.persistence.enabled=true \
  --set grafana.persistence.size=10Gi \
  --set prometheus.alertmanager.enabled=true

3.2 Prometheus 核心配置

yaml 复制代码
# prometheus.yaml(通过 ConfigMap 挂载)
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s        # 拉取间隔,默认15秒
      evaluation_interval: 15s     # 规则评估间隔
      external_labels:             # 外部标签,区分环境
        env: production
        cluster: bj-main-01

    alerting:
      alertmanagers:
        - static_configs:
            - targets:
              - alertmanager:9093

    rule_files:
      - "/etc/prometheus/rules/*.yml"

    scrape_configs:
      # Prometheus 自身监控
      - job_name: 'prometheus'
        static_configs:
          - targets: ['localhost:9090']

      # Kubernetes API Server
      - job_name: 'kubernetes-apiservers'
        kubernetes_sd_configs:
          - role: endpoints
        scheme: https
        tls_config:
          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
        relabel_configs:
          - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name]
            action: keep
            regex: default;kubernetes

      # Kubernetes Pod(自动发现)
      - job_name: 'kubernetes-pods'
        kubernetes_sd_configs:
          - role: pod
        relabel_configs:
          # 只抓取有 /metrics 端口的 Pod
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
            action: keep
            regex: true
          # 从注解中读取路径,默认 /metrics
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)
          # 从注解中读取端口,默认 9090
          - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
            action: replace
            regex: ([^:]+)(?::\d+)?;(\d+)
            replacement: $1:$2
            target_label: __address__
          # 保留 Kubernetes 标签作为 Prometheus 标签
          - action: labelmap
            regex: __meta_kubernetes_pod_label_(.+)
          - source_labels: [__meta_kubernetes_namespace]
            action: replace
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_pod_name]
            action: replace
            target_label: kubernetes_pod_name

      # MySQL 监控
      - job_name: 'mysql'
        static_configs:
          - targets: ['mysql-exporter.monitoring.svc.cluster.local:9104']
        metrics_path: /metrics

      # Redis 监控
      - job_name: 'redis'
        static_configs:
          - targets: ['redis-exporter.monitoring.svc.cluster.local:9121']

四、Java 应用埋点

4.1 Micrometer + Spring Boot 2.x/3.x 集成

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
yaml 复制代码
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,metrics,logfile  # 暴露的端点
      base-path: /actuator  # 端点基础路径
  metrics:
    tags:
      application: ${spring.application.name}   # 给所有指标加应用名标签
      environment: ${ENV:dev}                   # 环境标签
    export:
      prometheus:
        enabled: true                            # 开启 Prometheus 格式输出

  endpoint:
    health:
      show-details: always                        # 健康检查显示详细信息

# 给特定 Bean 的方法加监控
# 路径: /actuator/prometheus
# 路径: /actuator/metrics

4.2 核心指标埋点示例

java 复制代码
package com.example.monitoring;

import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.functionSupplier;

@Configuration
public class MetricsConfig {

    // 让 @Timed 注解生效(需要引入 AOP)
    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}

@Service
public class OrderServiceImpl implements OrderService {

    private final Counter orderCreatedCounter;
    private final Counter orderFailedCounter;
    private final Timer orderProcessTimer;

    public OrderServiceImpl(MeterRegistry registry) {
        // 计数器:记录订单创建总数(带业务标签)
        this.orderCreatedCounter = Counter.builder("order.created.total")
                .description("订单创建总数")
                .tag("service", "order-service")
                .register(registry);

        this.orderFailedCounter = Counter.builder("order.failed.total")
                .description("订单失败总数")
                .tag("service", "order-service")
                .register(registry);

        // 计时器:记录订单处理耗时分布
        this.orderProcessTimer = Timer.builder("order.process.duration")
                .description("订单处理耗时")
                .tag("service", "order-service")
                .publishPercentiles(0.5, 0.9, 0.95, 0.99)    // 公布各百分位数
                .publishPercentileHistogram()                // 发布直方图(用于计算 P99)
                .sla(TimeUnit.MILLISECONDS, 50, 100, 200, 500, 1000, 2000)
                .register(registry);
    }

    /**
     * @Timed 注解:自动记录方法执行时间
     * successChannel/suffixFailureChannel: 区分成功/失败路径的耗时
     */
    @Override
    @io.micrometer.core.annotation.Timed(value = "order.create",
                                          description = "创建订单",
                                          percentiles = {0.5, 0.95, 0.99})
    public Order createOrder(OrderCreateRequest request) {
        orderCreatedCounter.increment();

        // 用 Timer.record() 记录耗时(比注解方式更灵活)
        return orderProcessTimer.record(() -> {
            try {
                Order order = doCreateOrder(request);
                return order;
            } catch (Exception e) {
                orderFailedCounter.increment();
                throw e;
            }
        });
    }

    /**
     * 自定义 Gauge:监控队列长度(动态变化的值)
     */
    @Timed(value = "queue.process", type = Timer.Type.GAUGE)
    public int getQueueSize() {
        return messageQueue.size();
    }

    // 给第三方调用埋点(HTTP 客户端)
    @Override
    public Product getProduct(Long productId) {
        Timer.Sample sample = Timer.start();

        try {
            return productClient.getProduct(productId);
        } finally {
            // 记录第三方调用的耗时,并按服务名打标签
            sample.stop(Timer.builder("http.client.requests")
                    .tag("service", "product-service")
                    .tag("uri", "/products/{id}")
                    .tag("status", "success")
                    .register(meterRegistry));
        }
    }
}

4.3 自定义 Exporter(监控非标准指标)

有些指标无法通过 Micrometer 直接获取,需要写自定义 Exporter:

python 复制代码
#!/usr/bin/env python3
"""
自定义Exporter:监控订单系统的业务指标
暴露端口: 8080
指标路径: /metrics
Prometheus 拉取间隔: 30s
"""
from prometheus_client import start_http_server, Gauge, Counter, Histogram, REGISTRY
import random
import time

# 业务指标
order_pending = Gauge('order_pending_count', '待处理订单数',
                      ['tenant_id', 'biz_line'])
order_processing = Gauge('order_processing_count', '处理中订单数',
                         ['tenant_id', 'biz_line'])
order_success = Counter('order_success_total', '成功订单总数',
                        ['tenant_id', 'biz_line', 'payment_method'])
order_process_duration = Histogram('order_process_duration_seconds',
                                    '订单处理耗时(秒)',
                                    ['tenant_id'],
                                    buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0))

def collect_metrics():
    """从数据库或其他数据源拉取业务指标"""
    tenants = ['T001', 'T002', 'T003']
    biz_lines = ['EC', 'MRCH', 'EDU']

    for tenant in tenants:
        for biz in biz_lines:
            # 模拟从数据库查询
            pending = random.randint(0, 100)
            processing = random.randint(0, 20)

            order_pending.labels(tenant_id=tenant, biz_line=biz).set(pending)
            order_processing.labels(tenant_id=tenant, biz_line=biz).set(processing)

            # 模拟成功订单计数
            if random.random() > 0.1:
                method = random.choice(['WECHAT', 'ALIPAY', 'CARD'])
                order_success.labels(tenant_id=tenant, biz_line=biz,
                                     payment_method=method).inc(random.randint(1, 5))

if __name__ == '__main__':
    start_http_server(8080)  # 启动 HTTP 服务,Prometheus 从这里拉取
    print("自定义Exporter已启动,端口: 8080,路径: /metrics")

    while True:
        collect_metrics()
        time.sleep(30)  # 每30秒更新一次指标

五、Grafana 仪表盘设计

5.1 经典仪表盘模板(Order Service Overview)

json 复制代码
{
  "dashboard": {
    "title": "订单服务监控面板",
    "uid": "order-service-overview",
    "timezone": "Asia/Shanghai",
    "panels": [
      {
        "title": "QPS(每秒请求数)",
        "type": "graph",
        "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
        "targets": [
          {
            "expr": "sum(rate(http_server_requests_seconds_count{job=\"order-service\"}[1m]))",
            "legendFormat": "总QPS",
            "refId": "A"
          },
          {
            "expr": "sum(rate(http_server_requests_seconds_count{job=\"order-service\", status=~\"2..\"}[1m]))",
            "legendFormat": "成功QPS(2xx)",
            "refId": "B"
          },
          {
            "expr": "sum(rate(http_server_requests_seconds_count{job=\"order-service\", status=~\"4..\"}[1m]))",
            "legendFormat": "客户端错误QPS(4xx)",
            "refId": "C"
          },
          {
            "expr": "sum(rate(http_server_requests_seconds_count{job=\"order-service\", status=~\"5..\"}[1m]))",
            "legendFormat": "服务端错误QPS(5xx)",
            "refId": "D"
          }
        ]
      },
      {
        "title": "响应时间 P99",
        "type": "graph",
        "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
        "targets": [
          {
            "expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{job=\"order-service\"}[5m])) by (le, uri))",
            "legendFormat": "{{ uri }}",
            "refId": "A"
          }
        ]
      },
      {
        "title": "JVM GC 暂停时间",
        "type": "graph",
        "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
        "targets": [
          {
            "expr": "rate(jvm_gc_pause_seconds_sum[1m]) * 1000",
            "legendFormat": "{{ action }} ({{ cause }})",
            "refId": "A"
          }
        ]
      },
      {
        "title": "JVM 堆内存使用",
        "type": "graph",
        "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
        "targets": [
          {
            "expr": "jvm_memory_used_bytes{job=\"order-service\", area=\"heap\"} / 1024 / 1024",
            "legendFormat": "已使用 (MB)",
            "refId": "A"
          },
          {
            "expr": "jvm_memory_max_bytes{job=\"order-service\", area=\"heap\"} / 1024 / 1024",
            "legendFormat": "最大 (MB)",
            "refId": "B"
          }
        ]
      },
      {
        "title": "数据库连接池使用率",
        "type": "gauge",
        "gridPos": {"h": 8, "w": 6, "x": 0, "y": 16},
        "targets": [
          {
            "expr": "hikaricp_connections_active / hikaricp_connections_max * 100",
            "refId": "A"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"color": "green", "value": null},
                {"color": "yellow", "value": 70},
                {"color": "red", "value": 85}
              ]
            },
            "unit": "percent",
            "max": 100
          }
        }
      },
      {
        "title": "线程数",
        "type": "graph",
        "gridPos": {"h": 8, "w": 6, "x": 6, "y": 16},
        "targets": [
          {
            "expr": "jvm_threads_live_threads{job=\"order-service\"}",
            "legendFormat": "活跃线程",
            "refId": "A"
          },
          {
            "expr": "jvm_threads_daemon_threads{job=\"order-service\"}",
            "legendFormat": "守护线程",
            "refId": "B"
          },
          {
            "expr": "jvm_threads_peak_threads{job=\"order-service\"}",
            "legendFormat": "峰值线程",
            "refId": "C"
          }
        ]
      },
      {
        "title": "业务指标:各业务线订单量",
        "type": "graph",
        "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16},
        "targets": [
          {
            "expr": "sum(increase(order_success_total[1h])) by (tenant_id, biz_line)",
            "legendFormat": "{{ tenant_id }}/{{ biz_line }}",
            "refId": "A"
          }
        ]
      }
    ]
  }
}

5.2 仪表盘设计原则

  1. 核心指标放左上角:QPS、延迟、错误率放在最显眼位置,一眼就能判断系统健康状况
  2. 按职责分层:基础设施(JVM/DB)→ 服务(HTTP)→ 业务(订单量/转化率)
  3. 给每张图设置合理的告警阈值背景色:绿色/黄色/红色,不需要点进详情就能感知异常
  4. 使用变量实现仪表盘复用 :通过 $env$cluster 等变量,一张模板可以监控多个环境

六、告警规则配置

6.1 Prometheus 告警规则

yaml 复制代码
# prometheus-rules.yml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: order-service-alerts
  namespace: monitoring
  labels:
    prometheus: k8s
    role: alert-rules
spec:
  groups:
    - name: order-service.rules
      interval: 30s
      rules:
        # 【P1-紧急】API 错误率超过 1%
        - alert: HighAPIErrorRate
          expr: |
            sum(rate(http_server_requests_seconds_count{job="order-service", status=~"5.."}[5m]))
            /
            sum(rate(http_server_requests_seconds_count{job="order-service"}[5m])) > 0.01
          for: 1m          # 持续1分钟才触发(防止抖动)
          labels:
            severity: critical
            team: order-platform
            business_impact: high
          annotations:
            summary: "API 5xx 错误率超过 1%"
            description: "订单服务 5xx 错误率已达 {{ $value | humanizePercentage }},超过阈值 1%"
            runbook_url: "https://wiki.example.com/runbooks/high-api-error-rate"
            dashboard_url: "{{ $labels.instance }}/d/order-overview"

        # 【P1-紧急】响应时间 P99 超过 2 秒
        - alert: HighLatencyP99
          expr: |
            histogram_quantile(0.99,
              sum(rate(http_server_requests_seconds_bucket{job="order-service"}[5m])) by (le)
            ) > 2
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: "API 响应时间 P99 超过 2 秒"
            description: "P99 延迟: {{ $value | humanizeDuration }}"

        # 【P2-严重】JVM 堆内存使用率超过 85%
        - alert: JVMMemoryPressure
          expr: |
            jvm_memory_used_bytes{job="order-service", area="heap"}
            /
            jvm_memory_max_bytes{job="order-service", area="heap"} > 0.85
          for: 5m
          labels:
            severity: warning
            team: order-platform
          annotations:
            summary: "JVM 堆内存使用率超过 85%"
            description: "当前使用率: {{ $value | humanizePercentage }},最大: {{ $labels.area }}"

        # 【P2-严重】GC 暂停时间过长(CMS / G1 异常)
        - alert: HighGC PauseTime
          expr: |
            rate(jvm_gc_pause_seconds_sum[1m]) * 1000 > 500
          for: 2m
          labels:
            severity: warning
          annotations:
            summary: "GC 暂停时间异常(可能引发延迟抖动)"
            description: "GC 暂停速率: {{ $value | humanize }}ms/s,请检查内存配置"

        # 【P3-警告】数据库连接池即将耗尽
        - alert: DBConnectionPoolExhaustion
          expr: |
            hikaricp_connections_active / hikaricp_connections_max > 0.8
          for: 3m
          labels:
            severity: warning
          annotations:
            summary: "数据库连接池使用率超过 80%"
            description: "当前活跃连接: {{ $value | humanizePercentage }}"

        # 【P3-警告】服务离线
        - alert: ServiceDown
          expr: |
            up{job="order-service"} == 0
          for: 30s
          labels:
            severity: critical
          annotations:
            summary: "服务实例离线"
            description: "服务 {{ $labels.instance }} 已离线超过 30 秒"

        # 【P4-通知】业务指标异常(订单量突降)
        - alert: OrderVolumeDrop
          expr: |
            sum(increase(order_success_total{job="order-service"}[10m])) < 10
          for: 10m
          labels:
            severity: warning
            business: true
          annotations:
            summary: "订单量异常下降"
            description: "过去 10 分钟仅产生 {{ $value }} 个订单,请确认是否正常"

6.2 AlertManager 告警路由配置

yaml 复制代码
# alertmanager-config.yml
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'severity']   # 同类告警合并
  group_wait: 30s                         # 告警产生后等待30秒,看是否有更多同组告警
  group_interval: 5m                     # 发送间隔
  repeat_interval: 4h                     # 告警持续时重复发送间隔
  receiver: 'default-receiver'

  # 按严重级别路由
  routes:
    # P1 紧急:立即电话通知
    - match:
        severity: critical
      receiver: 'critical-receiver'
      group_wait: 10s        # P1 告警合并等待时间更短
      repeat_interval: 1h

    # 业务告警:发邮件+飞书
    - match:
        business: true
      receiver: 'business-receiver'

# 接收者配置
receivers:
  - name: 'default-receiver'
    email_configs:
      - send_resolved: true
        to: 'ops-team@example.com'
        headers:
          subject: '[Prometheus] {{ .GroupLabels.alertname }}'

  - name: 'critical-receiver'
    webhook_configs:
      - url: 'http://dingtalk-webhook.example.com/dingtalk/prometheus'
        send_resolved: true

  - name: 'business-receiver'
    email_configs:
      - to: 'biz-team@example.com'
    webhook_configs:
      - url: 'http://feishu-webhook.example.com/feishu/prometheus'

钉钉/飞书 Webhook 配置(Go 模板)

json 复制代码
{
  "msgtype": "markdown",
  "markdown": {
    "title": "【{{ .Status | toUpper }}】{{ .GroupLabels.alertname }}",
    "content": "**告警名称**: {{ .GroupLabels.alertname }}\n**严重级别**: {{ .Labels.severity }}\n**描述**: {{ .Annotations.description }}\n**时间**: {{ .StartsAt.Format "2006-01-02 15:04:05" }}\n**Dashboard**: [查看详情]({{ .Annotations.dashboard_url }})"
  }
}

七、踩坑实录

踩坑一:Prometheus 存储容量预估不足

问题描述:部署时只给了 50Gi 存储,但只撑了 7 天就开始报存储不足告警,数据被截断。

根本原因:压测时没测监控数据量。实际每个 Pod 的 metrics 端点每秒产生约 500-2000 个样本(取决于指标数量和标签基数),粗略计算:

复制代码
样本数/s = Pod数 × 指标数 × 标签基数
         = 50 Pods × 300 指标 × 3 (平均标签值)
         ≈ 45,000 样本/秒

每天存储量 ≈ 45,000 × 86,400 × 2 bytes ≈ 7.7 GB/天

解决方案

yaml 复制代码
# 合理规划存储(保留 15 天数据)
--storage.tsdb.retention.time=15d
--storage.tsdb.retention.size=200GB

# Prometheus Operator 配置
helm upgrade prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set prometheus.prometheusSpec.retention=15d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=200Gi

经验公式

复制代码
存储需求 = (每秒样本数 × 标签维度数 × 2 bytes × 86400 × 保留天数) / 压缩比
         ≈ 原始量 / 10(启用 Snappy 压缩后)

踩坑二:标签基数爆炸(Cardinality Explosion)

问题描述:Prometheus 内存占用持续增长,从 8G 飙升到 32G,最后 OOM 被 kill 了。

根本原因 :某开发者把 user_id(几百万级别)作为指标标签直接打上去了。每个用户的每项指标都会生成独立的时间序列。

java 复制代码
// 错误示范:把高基数标签打到指标上
Counter.builder("order.pay")
    .tag("user_id", userId)  // ❌ 几百万用户 = 几百万时间序列
    .register(registry);

解决方案

java 复制代码
// 正确做法:不要把 user_id 作为标签
Counter.builder("order.pay")
    .tag("payment_method", paymentMethod)  // ✅ 低基数(最多几十个)
    .tag("tenant_id", tenantId)            // ✅ 低基数
    .register(registry);

// 如果必须追踪单个用户,用 tracing(链路追踪),不要用 metrics

排查高基数指标

promql 复制代码
# 查看指标数量最多的标签
topk(10, count by (__name__, label) ({__name__=~".+"}))

踩坑三:PromQL 查询超时导致 Grafana 白屏

问题描述:Grafana 上某些仪表盘打开后一直 loading,最后显示"查询超时"。

根本原因 :Prometheus 作为单实例,每次查询都要扫描大量时间线。带 by (uri) 分组的查询在 1000+ 个时间序列的情况下性能急剧下降。

解决方案

yaml 复制代码
# prometheus.yml 中添加查询超时配置
global:
  evaluation_interval: 15s
  query_timeout: 60s  # 全局查询超时

# 针对特定查询使用 recording rules 预计算
groups:
  - name: order_service_aggregated
    interval: 1m
    rules:
      # 预计算高基数查询结果,避免每次 Dashboard 加载时实时计算
      - record: job:order_qps:1m
        expr: sum by (job) (rate(http_server_requests_seconds_count[1m]))

      - record: job:order_p99_latency:5m
        expr: histogram_quantile(0.99,
          sum by (job, le) (rate(http_server_requests_seconds_bucket[5m]))
        )

在 Grafana 中使用预计算的 recording:

promql 复制代码
# 直接查 recording rule,避免实时计算
sum(order_qps:1m{job="order-service"})

踩坑四:告警静默期设置不当

问题描述:凌晨 2 点发布了版本,发布过程中触发了大量告警,值班手机被打爆。发布结束后告警还在持续,直到人工确认才停止。

根本原因:没有区分"正常发布抖动"和"真实故障",告警没有区分来源。

解决方案

yaml 复制代码
# AlertManager 中添加静默规则(发布窗口)
# 在发布前,在 AlertManager 界面中手动添加静默规则
# 或者使用标签自动静默

route:
  routes:
    # 发布过程中的告警自动降低级别
    - match:
        release_in_progress: "true"
      receiver: 'release-receiver'
      continue: true

# 自动化发布静默(CI/CD 流水线中触发)
curl -X POST "http://alertmanager:9093/api/v1/silences" \
  -H "Content-Type: application/json" \
  -d '{
    "matchers": [
      {"name": "job", "value": "order-service"},
      {"name": "env", "value": "production"}
    ],
    "startsAt": "2024-03-01T02:00:00+08:00",
    "endsAt": "2024-03-01T03:00:00+08:00",
    "createdBy": "ci-cd-bot",
    "comment": "发布窗口,自动静默"
  }'

踩坑五:JVM 监控指标不准确(G1 GC 与 Serial Old 混淆)

问题描述:监控显示 GC 暂停时间每分钟加起来超过 5 分钟,但系统延迟完全正常。团队开始怀疑监控数据有问题。

根本原因 :JDK 8 的 jvm_gc_pause_seconds_sum 指标在 G1 GC 下的统计方式与其他 GC 不同------G1 记录的是整个 GC 循环的暂停时间,而不是单次 GC 的时间。如果使用 Serial Old(单线程标记-整理),每次 GC 都会长时间停顿,这是严重问题;但如果是 ParNew + CMS 组合,短停顿是正常的。

解决方案

yaml 复制代码
# 告警规则要区分 GC 类型
- alert: GCTypeMismatch
  expr: |
    (rate(jvm_gc_pause_seconds_count{job="order-service", cause="Allocation Failure"}[5m]) > 10)
    and
    (jvm_gc_pause_seconds_max{job="order-service"} > 0.2)
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "GC 频率异常或单次停顿过长"
    description: "GC 每分钟触发超过 10 次,或单次停顿超过 200ms,请检查 GC 日志"

另外,结合 GC 日志 做交叉验证:

bash 复制代码
# 开启 GC 日志(JVM 参数)
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=10M

# 实时分析 GC 日志
# 使用 gclog-analyzer 或 GCViewer 工具

八、总结与思考

8.1 核心要点总结

  1. 指标体系要分层:基础设施(USE方法)+ 应用层(RED方法)+ 业务层(自定义),三层指标缺一不可。

  2. 告警要分级,沉默要可控 :P1 电话、P2 钉钉、P3 飞书/邮件、P4 日志。告警要设置合理的 for 持续时间,防止抖动触发。

  3. 标签基数是性能杀手:永远不要把 user_id、request_id 等高基数字段作为 Prometheus 标签。

  4. 预计算优于实时查询:使用 Recording Rules 预计算常用聚合指标,减轻 Prometheus 查询压力。

  5. 监控本身也需要监控:Prometheus 和 Grafana 的健康状态也要监控,防止监控挂了还不知情。

  6. 黄金三角:延迟(Latency)+ 流量(Traffic)+ 错误(Errors),这三个维度盯住了,系统的基本健康状况就八九不离十了。

8.2 思考题

  1. 如果你的监控系统在业务高峰时本身也出现性能问题(比如 Prometheus OOM),你应该如何设计"监控的监控"来提前发现?

  2. 在多云/混合云架构下,不同云厂商的监控数据如何统一?Prometheus 的 Federation 模式能否解决跨集群聚合问题?

  3. AIOps(智能运维)是趋势,但机器学习模型需要大量高质量的历史告警数据作为训练集。你会如何设计数据管道,让 Prometheus 的告警数据成为 AIOps 的输入?

8.3 个人观点

监控不是救火队员,而是预警系统。很多团队把监控当成了"出了问题再看"的工具,这本身就是一个认知误区。真正好的监控,是在问题影响到用户之前就能感知到异常,在故障发生之前就已经推送了告警。

Prometheus + Grafana 的组合之所以成为业界标准,不是因为它们功能最多或者性能最强,而是因为它们足够简单、足够灵活、社区足够活跃。在你的监控体系建立起来之前,不要追求"大而全"------先把 CPU、内存、QPS、延迟、错误率 这五个基础指标盯起来,再逐步扩展到 JVM、DB、中间件、业务层。

记住:你只能监控你信任的系统,而信任来自测量。 那些你觉得"肯定不会出问题"的地方,往往是出问题最多的地方。所以,保持谦逊,保持监控,把数据说真话当成一种工程文化------这才是监控体系真正的价值所在。


本文约 4300 字,涵盖 Prometheus + Grafana 监控体系设计、Java 埋点实践、Grafana 仪表盘配置、AlertManager 告警路由,以及 5 个真实踩坑案例,适合需要构建生产级监控体系的开发者阅读。

相关推荐
小短腿的代码世界4 小时前
QCefView架构深度解析:从Chromium嵌入到Qt信号槽集成的完整技术链路
qt·架构
北京盟通科技官方账号4 小时前
工业 PC 平台 EtherCAT 主站协议栈选型探讨:开源方案与商业级实时架构的工程落地对比
架构·机器人·开源·工控·ethercat·盟通科技·ec-master
无心水4 小时前
【分布式利器:金融级】金融级分布式架构开源框架全景解读
人工智能·分布式·金融·架构·开源·wpf·金融级框架
__log5 小时前
NestJS vs Spring Boot:从架构哲学到实战选择的技术全景解析
spring boot·后端·架构·typescript
搬砖的小码农_Sky5 小时前
AMD Ryzen AI Strix Halo架构处理器:如何在笔记本上跑通原本属于服务器的模型?
人工智能·架构·gpu算力
千匠网络5 小时前
千匠网络制造行业渠道分销B2B解决方案:AI驱动,重构产业分销模式
网络·云原生·架构·制造业·b2b·电商解决方案
小短腿的代码世界5 小时前
Qt属性系统揭秘:从Q_PROPERTY宏到动态元对象系统的完整架构解析
开发语言·qt·架构
我叫张小白。5 小时前
MySQL架构与SQL执行完全解析
sql·mysql·架构
DN金猿5 小时前
SpringCloudAlibaba微服务启动报错
微服务·云原生·nacos·架构·springcloud·sca