监控告警:系统的"健康体检"

这是基础设施系列的第5篇文章。前面我们聊了网关、配置中心、消息队列,今天来聊聊一个同样重要但常被忽视的话题——监控告警。

一、为什么监控如此重要?

你有没有遇到过这样的情况:

这就是缺乏监控的后果。没有监控的系统,就像盲人开车——你永远不知道下一秒会发生什么。

监控的本质是什么?

很多人把监控理解为"看数据",但这个理解太浅了。

监控的本质是:让你的系统变得可观测

可观测性(Observability)这个词来源于控制理论。一个系统被称为"可观测的",是指通过系统的外部输出(指标、日志、追踪),能够推断出系统内部的状态。就像医生通过体温、血压、心电图这些外部指标,能判断你身体内部哪里出了问题。

一个可观测的系统,应该能回答这三个问题:

  1. 现在发生什么?(实时状态)—— Metrics
  2. 为什么发生?(根因分析)—— Logs + Traces
  3. 将来会发生什么?(趋势预测)—— 历史数据分析

没有监控,这些问题都回答不了。有了监控,你才能真正掌控系统。

可观测性三支柱

业界把可观测性总结为三个支柱,它们各有分工:

这三个支柱不是互相替代,而是互相补充。就像医学检查:指标是体检报告,日志是病历记录,追踪是 CT 扫描。三者结合,才能全面了解系统健康状况。

三支柱的协同工作

让我们用一个实际场景来说明三支柱如何协同工作:

监控发现:订单服务 P99 延迟从 50ms 飙升到 3s
时间点:14:30-14:45 之间
影响范围:约 5% 的请求受影响
链路追踪显示:慢请求都卡在「查询库存」这一步
调用链路:订单服务 → 库存服务 → MySQL
瓶颈点:库存服务的 MySQL 查询
日志搜索结果:
[14:32:15] ERROR - Slow query detected: SELECT * FROM inventory WHERE sku_id = 'SKU001'
[14:32:15] WARN - Query took 2847ms, rows scanned: 500000
根因:某个 SKU 的库存查询触发了全表扫描

这就是三支柱的威力:Metrics 告诉你"有问题",Traces 告诉你"问题在哪",Logs 告诉你"为什么"。

监控带来的价值

根据 Google SRE 的数据,完善的监控可以把平均故障发现时间(MTTD)从 30 分钟缩短到 5 分钟以内。这意味着用户受影响的时间减少了 80% 以上。

一个形象的比喻:没有监控就像医生没有听诊器和化验报告,只能靠"望闻问切"来诊断病情。有了监控,就等于有了 CT、核磁共振、血液分析,问题一目了然。

举个例子:通过监控发现某个接口的 P99 延迟是 P50 的 10 倍,深入分析后发现是某个慢查询拖累了整体性能。优化这个查询后,P99 延迟下降了 80%,用户体验大幅提升。

某电商公司通过分析监控数据,发现晚上 10 点是下单高峰,于是把大促活动的时间调整到这个时段,结果转化率提升了 15%。

二、监控指标设计

知道了监控的重要性,接下来一个问题:到底该监控什么?

很多人犯的错误是:什么都监控。结果监控面板几百个指标,真正出问题时,反而找不到关键信息。

指标分类:RED 和 USE

业界有两个经典的指标分类方法:RED 方法和 USE 方法。

这三个指标几乎适用于所有对外提供服务的系统。如果你的服务只监控三个东西,就监控这三个。

这个方法适用于 CPU、内存、磁盘、网络等资源型组件。

分层监控:从基础设施到业务

一个完整的监控体系,应该覆盖多个层次:

每一层都有其特定的监控指标,不能遗漏。

指标设计的几个原则

指标的四种类型

在 Prometheus 等监控系统中,指标通常分为四种类型,理解它们对于设计监控非常重要:

三、告警策略

监控数据收集上来了,接下来就是告警。但告警是最容易出问题的地方。

告警的困境

很多人都有过这样的经历:告警短信一天收到几十条,大部分是无关紧要的,最后干脆把告警屏蔽了。

这就是告警疲劳。当告警太多时,人会麻木,真正重要的告警反而被忽略。

好的告警系统,不是告警越多越好,而是每个告警都有价值

告警分级

解决告警疲劳的第一步是告警分级。不是所有问题都需要立即处理。

不同级别的告警,通知方式也不同:

告警规则设计原则

好的告警规则是监控系统的核心。设计告警规则时,需要遵循几个关键原则:

告警规则配置示例

以下是一些常见场景的告警规则配置示例(以 Prometheus AlertManager 格式为例):

# 服务可用性告警
- alert: ServiceDown
  expr: up{job="order-service"} == 0
  for: 1m
  labels:
    severity: critical
    priority: P0
  annotations:
    summary: "订单服务不可用"
    description: "订单服务 {{ $labels.instance }} 已经宕机超过 1 分钟"

# 响应时间告警
- alert: HighLatency
  expr: histogram_quantile(0.99, 
         sum(rate(http_request_duration_seconds_bucket{job="order-service"}[5m])) 
         by (le)) > 1
  for: 5m
  labels:
    severity: warning
    priority: P1
  annotations:
    summary: "订单服务响应时间过高"
    description: "P99 延迟 {{ $value | printf \"%.2f\" }}s,超过 1s 阈值"

# 错误率告警
- alert: HighErrorRate
  expr: |
    sum(rate(http_requests_total{job="order-service",status=~"5.."}[5m])) 
    / sum(rate(http_requests_total{job="order-service"}[5m])) > 0.05
  for: 5m
  labels:
    severity: critical
    priority: P0
  annotations:
    summary: "订单服务错误率过高"
    description: "5xx 错误率 {{ $value | printf \"%.2%%\" }},超过 5% 阈值"

# 资源使用率告警
- alert: HighMemoryUsage
  expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.9
  for: 10m
  labels:
    severity: warning
    priority: P1
  annotations:
    summary: "内存使用率过高"
    description: "节点 {{ $labels.instance }} 内存使用率 {{ $value | printf \"%.1%%\" }}"

告警聚合与抑制

# AlertManager 分组配置
route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s        # 等待 30 秒收集同组告警
  group_interval: 5m     # 同组新告警的发送间隔
  repeat_interval: 4h    # 重复告警的发送间隔
# 当集群不可用时,抑制该集群下所有服务的告警
inhibit_rules:
- source_match:
    alertname: 'ClusterDown'
  target_match_re:
    alertname: '.+'
  equal: ['cluster']

飞书机器人集成

我们的告警系统集成了飞书机器人,实现了快速、可靠的告警通知。

# AlertManager 飞书 webhook 配置
receivers:
- name: 'feishu-default'
  webhook_configs:
  - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx'
    send_resolved: true  # 问题解决后也发送通知

- name: 'feishu-critical'
  webhook_configs:
  - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx'
    send_resolved: true
{
  "msg_type": "interactive",
  "card": {
    "header": {
      "title": {
        "content": "🚨 P0告警:订单服务不可用",
        "tag": "plain_text"
      },
      "template": "red"
    },
    "elements": [
      {
        "tag": "div",
        "text": {
          "content": "**告警级别**:P0(紧急)\n**影响范围**:所有用户无法下单\n**持续时间**:3分钟\n**当前QPS**:0(正常值:1500)",
          "tag": "lark_md"
        }
      },
      {
        "tag": "action",
        "actions": [
          {
            "tag": "button",
            "text": {
              "content": "查看Grafana",
              "tag": "plain_text"
            },
            "url": "https://grafana.example.com/d/order-service",
            "type": "default"
          },
          {
            "tag": "button",
            "text": {
              "content": "确认告警",
              "tag": "plain_text"
            },
            "url": "https://alertmanager.example.com/#/alerts",
            "type": "primary"
          }
        ]
      }
    ]
  }
}
# 分级告警路由
route:
  receiver: 'feishu-default'
  routes:
  - match:
      severity: critical
      priority: P0
    receiver: 'feishu-sms-phone'
  - match:
      severity: warning
      priority: P1
    receiver: 'feishu-sms'

receivers:
- name: 'feishu-default'
  webhook_configs:
  - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx'

- name: 'feishu-sms'
  webhook_configs:
  - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx'
  # 集成短信服务
  sms_configs:
  - to: '13800138000'
    body: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'

- name: 'feishu-sms-phone'
  webhook_configs:
  - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx'
  # 集成短信和电话服务
  sms_configs:
  - to: '13800138000'
  phone_configs:
  - to: '13800138000'
    # 语音电话告警

避免告警风暴

告警风暴是最可怕的事情。某个核心服务挂了,瞬间触发几百条告警,值班人员的手机被打爆,反而无法快速定位问题。

防止告警风暴的方法:

  1. 告警去重:同一个问题在短时间内只告警一次
  2. 告警静默:某些告警可以被手动静默一段时间
  3. 告警聚合:相关的告警合并成一个
  4. 分级处理:低级别告警不发送通知,只在系统里记录
  5. 设置告警预算:限制每个服务/团队的告警数量,超过阈值需要优化

告警升级机制

当告警长时间未处理时,需要有升级机制:

# 告警升级配置示例
escalation:
  - after: 15m
    if_severity: [P0]
    action: 
      - notify: oncall-lead
      - notify: manager
  - after: 30m
    if_severity: [P0, P1]
    action:
      - notify: director
      - create_incident

数据采集的具体实现

了解数据采集的两种模式后,我们来看具体的实现方案:

最常用的方式是在应用中集成 SDK,暴露 /metrics 端点供 Prometheus 拉取。以下是不同语言的常用客户端:

语言 客户端库 特点
Java micrometer-registry-prometheus Spring Boot 官方支持
Go prometheus/client_golang 官方维护,性能优秀
Python prometheus_client 简单易用
Node.js prom-client 社区活跃

一个简单的 Go 应用指标暴露示例:

package main

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "net/http"
)

var (
    // 定义 Counter 指标
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )
    
    // 定义 Histogram 指标
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
        },
        []string{"method", "path"},
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal)
    prometheus.MustRegister(httpRequestDuration)
}

func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":8080", nil)
}

使用 Exporter 将基础设施的指标转换为 Prometheus 格式:

# prometheus.yml 配置示例
scrape_configs:
  # Node Exporter - 主机指标
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['10.0.1.1:9100', '10.0.1.2:9100']
  
  # MySQL Exporter - 数据库指标
  - job_name: 'mysql-exporter'
    static_configs:
      - targets: ['10.0.2.1:9104']
  
  # Redis Exporter - 缓存指标
  - job_name: 'redis-exporter'
    static_configs:
      - targets: ['10.0.3.1:9121']
  
  # 应用服务 - 自动发现
  - job_name: 'app-services'
    consul_sd_configs:
      - server: 'consul.service.consul:8500'
        services: ['app-service']

对于数据库、缓存、消息队列等中间件,需要配置专门的 Exporter:

# MySQL Exporter 配置
DATA_SOURCE_NAME="user:password@(mysql-host:3306)/" \
  mysqld_exporter \
  --collect.info_schema.processlist \
  --collect.info_schema.innodb_metrics \
  --collect.perf_schema.eventsstatements

数据降采样与聚合

原始监控数据量非常大,需要进行降采样来节省存储空间。降采样的核心思想是:近期数据保留高精度,远期数据保留低精度

原始数据(10秒精度)    →  保留 7 天
    ↓ 降采样
1分钟精度数据          →  保留 30 天
    ↓ 降采样
1小时精度数据          →  保留 1 年
    ↓ 降采样
1天精度数据            →  永久保留

降采样的聚合方式取决于指标类型:

在 Prometheus 中,可以使用 Recording Rules 预计算常用指标:

# recording_rules.yml
groups:
  - name: service_metrics
    rules:
      # 预计算每分钟的请求速率
      - record: job:http_requests:rate1m
        expr: sum by (job) (rate(http_requests_total[1m]))
      
      # 预计算 P99 延迟
      - record: job:http_request_duration:p99_5m
        expr: histogram_quantile(0.99, 
               sum by (job, le) (rate(http_request_duration_seconds_bucket[5m])))

四、监控系统架构

知道了要监控什么、怎么告警,接下来是实现层面的问题:监控系统本身应该怎么设计?

整体架构

一个典型的监控系统架构包含以下组件:

┌─────────────────────────────────────────────────────────────┐
│                      可视化层                                │
│         (Grafana / Kibana / 自研 Dashboard)                  │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      告警层                                  │
│         (AlertManager / PagerDuty / 自研告警)               │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      存储层                                  │
│    (Prometheus / InfluxDB / VictoriaMetrics / Thanos)       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      采集层                                  │
│   (Exporters / StatsD / OpenTelemetry / 自定义 Agent)       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      数据源                                  │
│   (应用 / 中间件 / 基础设施 / 云服务 / 业务系统)            │
└─────────────────────────────────────────────────────────────┘

数据采集

监控的第一步是数据采集。主要有两种方式:

实际应用中,两种模式经常混用。基础设施层多用拉模式,应用层多用推模式。现在业界趋势是采用 OpenTelemetry 统一标准,它支持两种模式,并能同时收集指标、日志和链路追踪。

数据存储

监控数据的特点是:写入量大、查询频繁、有时效性。

时序数据库是监控数据的最佳选择。相比传统数据库,时序数据库针对时间序列数据做了专门优化:

数据库 特点 适用场景
Prometheus 单机性能好,生态丰富 中小规模集群
VictoriaMetrics 兼容 Prometheus,性能更高 大规模部署
InfluxDB 功能全面,有商业版 需要企业级支持
Thanos Prometheus 集群方案 多集群、长期存储
M3DB Uber 开源,大规模场景 超大规模监控

这样既保证近期数据的精度,又控制了存储成本。

数据展示

监控数据收集上来后,需要可视化展示。一个好的监控面板应该:

第一层:业务大盘
├── 核心业务指标(订单量、GMV、转化率)
├── 系统健康度(服务可用性、错误率)
└── 关键资源使用情况

第二层:服务面板(按服务分类)
├── 请求量、延迟、错误率
├── 依赖服务状态
└── 资源使用情况

第三层:实例面板(按实例分类)
├── 单实例详细指标
├── 日志链接
└── 链路追踪入口

高可用设计

监控系统自身也需要高可用,否则它挂了,你就两眼一抹黑。

五、故障排查流程

有了监控和告警,当问题发生时,该如何排查?

故障排查的黄金时间

故障发生后,每一秒都很宝贵。前 5 分钟是黄金时间,这个阶段的操作决定了故障恢复的速度。

黄金时间内要做的事:

  1. 确认问题:通过监控确认问题的范围和严重程度
  2. 止损优先:如果问题在扩大,先想办法止损(限流、降级、回滚)
  3. 保留现场:收集日志、dump、快照等信息
  4. 通知相关方:让需要知道的人知道(用户、老板、相关团队)

排查思路:从外到内

故障排查的核心思路是:从外到内,逐层定位

每一层都要看监控数据,而不是凭感觉猜测。

实战案例:一个慢查询问题的排查

  1. 确认问题范围:查看监控面板,发现订单服务的 P99 延迟从 100ms 飙升到 3s,错误率从 0.1% 上升到 5%。
  1. 定位问题服务:通过链路追踪发现,慢请求都卡在数据库查询上。
  1. 分析数据库:查看数据库监控,发现某条 SQL 的执行时间从 10ms 变成了 2s+。
  1. 找到根因:查看慢查询日志,发现是一条新上线的 SQL 没有走索引:
   SELECT * FROM orders WHERE DATE(created_at) = '2026-02-28'
   

这个查询对 created_at 字段使用了函数,导致索引失效,全表扫描。

  1. 临时止血:紧急回滚上一个版本的代码。
  1. 永久修复:修改 SQL,改为范围查询,走索引:
   SELECT * FROM orders 
   WHERE created_at >= '2026-02-28 00:00:00' 
     AND created_at < '2026-03-01 00:00:00'
   
  1. 复盘改进
- 增加 SQL 审核流程,上线前检查执行计划

- 增加数据库慢查询告警(>500ms 告警) - 在测试环境添加生产级别的数据量

故障复盘

故障恢复后,事情还没结束。还要做故障复盘。

故障复盘的核心是:找到根本原因,制定预防措施

一个完整的故障复盘应该包括:

  1. 故障概况:什么时间、什么问题、影响范围
  2. 时间线:故障发生、发现、处理、恢复的时间点
  3. 根因分析:为什么会发生这个问题(用 5 Whys 方法)
  4. 处理过程:做了什么操作,效果如何
  5. 经验教训:学到了什么,哪里可以做得更好
  6. 改进措施:后续要做什么改进,谁来负责,什么时候完成

复盘不是追责大会,而是学习机会。好的复盘文化是:对事不对人,关注改进而不是惩罚

六、可观测性最佳实践

命名规范

好的指标命名能让监控更易维护:

# 推荐格式
<namespace>_<subsystem>_<name>_<unit>

# 示例
http_requests_total           # HTTP 请求总数
http_request_duration_seconds # HTTP 请求延迟(秒)
db_connections_active         # 活跃数据库连接数
queue_messages_pending        # 队列待处理消息数

标签设计

标签(Labels)是指标的多维度信息,好的标签设计能提高查询效率:

# 好的标签设计
http_requests_total{
  method="POST",
  path="/api/orders",
  status="200",
  service="order-service",
  instance="10.0.1.1:8080"
}

仪表盘标准化

建立标准化的仪表盘模板,让所有服务使用统一的展示风格:

# 标准服务仪表盘结构
1. 概览行
   - 服务状态(红/黄/绿)
   - 请求量趋势
   - 错误率趋势
   - P99 延迟趋势

2. 详细指标
   - RED 指标详细图表
   - 资源使用情况
   - 依赖服务状态

3. 业务指标
   - 核心业务指标
   - 转化漏斗
   - 用户活跃度

4. 链接区
   - 跳转到日志
   - 跳转到链路追踪
   - 跳转到相关服务

监控即代码

把监控配置也当作代码管理,纳入版本控制:

monitoring/
├── rules/
│   ├── service-alerts.yaml
│   ├── infra-alerts.yaml
│   └── business-alerts.yaml
├── dashboards/
│   ├── service-overview.json
│   └── business-metrics.json
└── recording-rules/
    └── precomputed-metrics.yaml

这样做的好处:

SLI/SLO/SLA:监控的业务视角

技术指标固然重要,但最终我们关心的是业务可用性。SLI/SLO/SLA 是把技术指标转化为业务指标的关键概念。

传统的阈值告警是"CPU > 80% 就报警",但 SLO 驱动的告警是"如果按照当前错误率,这个月的 SLO 会不达标,就报警"。

这叫做错误预算(Error Budget)告警。假设你的 SLO 是 99.9% 可用性,那么每个月有 43 分钟的"预算"可以出错。当错误预算消耗到 50% 时告警,到 90% 时紧急告警。

# 错误预算告警规则
- alert: ErrorBudgetBurnRate
  expr: |
    (
      sum(rate(http_requests_total{status!~"2.."}[1h]))
      / sum(rate(http_requests_total[1h]))
    ) > (1 - 0.999) * 24  # 1小时消耗了1天的预算
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "错误预算消耗过快,1小时内消耗了1天的预算"

这种告警方式的好处是:它直接关联业务目标,而不是技术指标。CPU 高不高不重要,重要的是会不会影响 SLO。

监控系统的容量规划

监控系统本身也需要容量规划,否则它会成为系统的瓶颈。

每天数据量 = 指标数量 × 每分钟采样点数 × 每个数据点大小 × 60 × 24

示例:
- 1000 个指标
- 每分钟 6 个采样点(10秒采集一次)
- 每个数据点 16 字节
- 每天 = 1000 × 6 × 16 × 60 × 24 ≈ 138 MB/天

保留 30 天原始数据 + 1 年聚合数据 ≈ 4 GB + 500 MB ≈ 4.5 GB
  1. 使用 Recording Rules 预计算:将复杂查询预先计算好
  2. 合理设置查询时间范围:避免查询过长时间范围
  3. 使用标签优化查询:在标签上建立索引,提高查询效率
  4. 分离读写:将查询请求分流到只读副本

七、总结

监控告警是系统稳定性的基石。没有监控的系统就像在黑暗中行走,你永远不知道前面是坦途还是悬崖。

一个完善的监控体系需要考虑:

监控告警不是一次性工作,而是持续演进的过程。随着系统的发展,监控体系也要不断调整和优化。



💬 评论 (0)

0/500
排序: