【架构实战】监控告警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 仪表盘设计原则
- 核心指标放左上角:QPS、延迟、错误率放在最显眼位置,一眼就能判断系统健康状况
- 按职责分层:基础设施(JVM/DB)→ 服务(HTTP)→ 业务(订单量/转化率)
- 给每张图设置合理的告警阈值背景色:绿色/黄色/红色,不需要点进详情就能感知异常
- 使用变量实现仪表盘复用 :通过
$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 核心要点总结
-
指标体系要分层:基础设施(USE方法)+ 应用层(RED方法)+ 业务层(自定义),三层指标缺一不可。
-
告警要分级,沉默要可控 :P1 电话、P2 钉钉、P3 飞书/邮件、P4 日志。告警要设置合理的
for持续时间,防止抖动触发。 -
标签基数是性能杀手:永远不要把 user_id、request_id 等高基数字段作为 Prometheus 标签。
-
预计算优于实时查询:使用 Recording Rules 预计算常用聚合指标,减轻 Prometheus 查询压力。
-
监控本身也需要监控:Prometheus 和 Grafana 的健康状态也要监控,防止监控挂了还不知情。
-
黄金三角:延迟(Latency)+ 流量(Traffic)+ 错误(Errors),这三个维度盯住了,系统的基本健康状况就八九不离十了。
8.2 思考题
-
如果你的监控系统在业务高峰时本身也出现性能问题(比如 Prometheus OOM),你应该如何设计"监控的监控"来提前发现?
-
在多云/混合云架构下,不同云厂商的监控数据如何统一?Prometheus 的 Federation 模式能否解决跨集群聚合问题?
-
AIOps(智能运维)是趋势,但机器学习模型需要大量高质量的历史告警数据作为训练集。你会如何设计数据管道,让 Prometheus 的告警数据成为 AIOps 的输入?
8.3 个人观点
监控不是救火队员,而是预警系统。很多团队把监控当成了"出了问题再看"的工具,这本身就是一个认知误区。真正好的监控,是在问题影响到用户之前就能感知到异常,在故障发生之前就已经推送了告警。
Prometheus + Grafana 的组合之所以成为业界标准,不是因为它们功能最多或者性能最强,而是因为它们足够简单、足够灵活、社区足够活跃。在你的监控体系建立起来之前,不要追求"大而全"------先把 CPU、内存、QPS、延迟、错误率 这五个基础指标盯起来,再逐步扩展到 JVM、DB、中间件、业务层。
记住:你只能监控你信任的系统,而信任来自测量。 那些你觉得"肯定不会出问题"的地方,往往是出问题最多的地方。所以,保持谦逊,保持监控,把数据说真话当成一种工程文化------这才是监控体系真正的价值所在。
本文约 4300 字,涵盖 Prometheus + Grafana 监控体系设计、Java 埋点实践、Grafana 仪表盘配置、AlertManager 告警路由,以及 5 个真实踩坑案例,适合需要构建生产级监控体系的开发者阅读。