第 6 章 度量数据的分析与报警#
本章你将学到
度量数据的分析方法:趋势分析、对比分析、关联分析、异常检测
USE/RED/黄金信号方法论的实际应用
告警策略设计:分级、聚合、抑制
APDEX 用户满意度评分
如何避免告警疲劳
开篇故事#
一个告警规则配错了阈值——把 CPU 告警从 90% 改成了 70%。 结果团队一晚上收到了 200 条告警通知。 第二天早上,所有人都把告警通知静音了。
一周后,一个真正的 P0 故障发生了。告警正常触发了, 但没有人看到——因为所有人都已经对告警麻木了。
这就是"告警疲劳"——它比没有告警更危险。
本章讲解如何分析度量数据,揭示隐藏在图表背后所蕴含的意义, 以及如何构建一个有效的(而非令人疲劳的)报警系统。
6.1 度量数据的分析#
6.1.1 分析方法#
方法 |
说明 |
应用场景 |
|---|---|---|
趋势分析 |
观察指标随时间的变化趋势 |
容量规划、性能退化检测 |
对比分析 |
与基线、历史或其他维度对比 |
版本发布影响评估 |
关联分析 |
分析多个指标之间的关联关系 |
根因分析 |
异常检测 |
自动识别异常的数据模式 |
故障预警 |
TopN 分析 |
找出排名前 N 的项目 |
性能瓶颈定位 |
6.1.2 USE 方法#
Brendan Gregg 提出的 USE 方法,适用于分析系统资源:
U (Utilization): 资源的使用率 (如 CPU 使用率 90%)
S (Saturation): 资源的饱和度 (如队列长度 > 100)
E (Errors): 错误事件 (如磁盘 I/O 错误)
对每种系统资源 (CPU、内存、磁盘、网络) 逐一检查 USE 指标。
6.1.3 RED 方法#
Tom Wilkie 提出的 RED 方法,适用于分析微服务:
R (Rate): 请求速率 (每秒请求数)
E (Errors): 错误率 (失败请求的比例)
D (Duration): 请求持续时间 (延迟)
系统资源 → USE 方法 (Utilization, Saturation, Errors)
微服务 → RED 方法 (Rate, Errors, Duration)
业务指标 → 自定义 (DAU, 转化率, 收入等)
6.1.4 黄金信号 (Golden Signals)#
Google SRE 提出的四大黄金信号:
延迟 (Latency): 服务请求所需时间
流量 (Traffic): 系统的需求量
错误 (Errors): 请求失败的速率
饱和度 (Saturation): 系统的负载程度
小技巧
在实践中,建议结合使用:
基础设施层: USE 方法
应用服务层: RED 方法 / 黄金信号
业务层: 自定义业务指标
6.2 报警实现#
6.2.1 报警策略#
报警级别:
级别 |
颜色 |
含义 |
响应要求 |
|---|---|---|---|
P0 |
红色 |
严重故障,服务不可用 |
立即响应,所有相关人员 |
P1 |
橙色 |
重大问题,部分功能受影响 |
15 分钟内响应 |
P2 |
黄色 |
一般问题,性能下降 |
1 小时内响应 |
P3 |
蓝色 |
轻微问题,需关注 |
下一个工作日处理 |
Info |
绿色 |
信息通知 |
了解即可 |
报警原则:
及时性: 故障发生后尽快报警
准确性: 减少误报和漏报
可操作性: 报警信息应包含足够的上下文,便于快速定位和处理
不疲劳: 避免报警风暴,使用抑制和聚合策略
6.2.2 报警规则#
# Prometheus 报警规则
groups:
- name: potato-service-alerts
rules:
# 高错误率报警
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
> 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "高错误率: {{ $value | humanizePercentage }}"
description: "服务 {{ $labels.job }} 的 5xx 错误率超过 5%"
# 高延迟报警
- alert: HighLatency
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m])
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "P99 延迟过高: {{ $value }}s"
# 服务不可用
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "服务不可用: {{ $labels.instance }}"
6.2.3 报警通知渠道#
# Alertmanager 配置
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'cluster']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'default'
routes:
- match:
severity: critical
receiver: 'pagerduty'
- match:
severity: warning
receiver: 'slack'
receivers:
- name: 'default'
email_configs:
- to: 'team@example.com'
- name: 'slack'
slack_configs:
- api_url: 'https://hooks.slack.com/services/...'
channel: '#alerts'
- name: 'pagerduty'
pagerduty_configs:
- service_key: '<key>'
6.2.4 报警抑制与聚合#
避免报警风暴的策略:
分组 (Grouping): 将相似的报警合并为一条通知
抑制 (Inhibition): 高级别报警抑制低级别报警
静默 (Silencing): 在维护窗口期间静默报警
去重 (Deduplication): 避免重复发送相同报警
6.3 多语言报警实现#
6.3.1 Go: 自定义健康检查与告警#
// Go 实现轻量级度量检查器
package alerter
import (
"fmt"
"net/http"
"encoding/json"
"time"
)
type MetricsChecker struct {
prometheusURL string
alertWebhook string
}
type AlertLevel string
const (
AlertInfo AlertLevel = "info"
AlertWarning AlertLevel = "warning"
AlertCritical AlertLevel = "critical"
)
func (c *MetricsChecker) CheckErrorRate(threshold float64) error {
query := `sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))`
result, err := c.queryPrometheus(query)
if err != nil {
return err
}
if result > threshold {
return c.sendAlert(AlertCritical,
fmt.Sprintf("Error rate %.2f%% exceeds threshold %.2f%%",
result*100, threshold*100))
}
return nil
}
func (c *MetricsChecker) CheckP99Latency(thresholdSeconds float64) error {
query := `histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))`
result, err := c.queryPrometheus(query)
if err != nil {
return err
}
if result > thresholdSeconds {
return c.sendAlert(AlertWarning,
fmt.Sprintf("P99 latency %.2fs exceeds threshold %.2fs",
result, thresholdSeconds))
}
return nil
}
6.3.2 Python: 基于 Elasticsearch 的报警#
from elasticsearch import Elasticsearch
from datetime import datetime, timedelta
import httpx
class MetricsChecker:
def __init__(self, es_host='localhost:9200'):
self.es = Elasticsearch([es_host])
def check_error_rate(self, index: str, threshold: float = 0.05):
"""检查错误率是否超过阈值"""
now = datetime.utcnow()
query = {
"size": 0,
"query": {"range": {"@timestamp": {"gte": "now-5m"}}},
"aggs": {
"total": {"value_count": {"field": "status"}},
"errors": {"filter": {"range": {"status": {"gte": 500}}}}
}
}
result = self.es.search(index=index, body=query)
total = result['aggregations']['total']['value']
errors = result['aggregations']['errors']['doc_count']
if total > 0 and (errors / total) > threshold:
self.send_alert(
level="critical",
message=f"Error rate {errors/total:.2%} > {threshold:.2%}"
)
def send_alert(self, level: str, message: str):
"""发送报警到 webhook (Slack/飞书/钉钉)"""
httpx.post(self.webhook_url, json={
"level": level,
"message": message,
"timestamp": datetime.utcnow().isoformat(),
"service": "potato-server"
})
6.3.3 C++: 嵌入式系统的度量报警#
// C++ 中对性能敏感的度量检查
#include <chrono>
#include <functional>
class LatencyGuard {
public:
using AlertCallback = std::function<void(double, double)>;
LatencyGuard(double threshold_ms, AlertCallback on_exceed)
: threshold_ms_(threshold_ms)
, on_exceed_(std::move(on_exceed))
, start_(std::chrono::steady_clock::now()) {}
~LatencyGuard() {
auto elapsed = std::chrono::steady_clock::now() - start_;
double ms = std::chrono::duration<double, std::milli>(elapsed).count();
if (ms > threshold_ms_) {
on_exceed_(ms, threshold_ms_);
}
}
private:
double threshold_ms_;
AlertCallback on_exceed_;
std::chrono::steady_clock::time_point start_;
};
// 使用: 超过 100ms 自动报警
void handle_request() {
LatencyGuard guard(100.0, [](double actual, double threshold) {
spdlog::warn("Request took {:.1f}ms (threshold: {:.1f}ms)",
actual, threshold);
alert_counter.Increment();
});
// ... 处理请求 ...
}
6.3.2 APDEX 应用性能指数#
APDEX (Application Performance Index) 是一个衡量用户满意度的标准:
APDEX = (满意数 + 容忍数 × 0.5) / 总数
其中:
- 满意: 响应时间 ≤ T (如 T = 500ms)
- 容忍: T < 响应时间 ≤ 4T
- 失望: 响应时间 > 4T
APDEX 值:
- 0.94-1.00: 优秀
- 0.85-0.93: 良好
- 0.70-0.84: 一般
- 0.50-0.69: 差
- < 0.50: 不可接受
def calculate_apdex(response_times, threshold_t=0.5):
"""计算 APDEX 分数"""
satisfied = sum(1 for t in response_times if t <= threshold_t)
tolerating = sum(1 for t in response_times
if threshold_t < t <= 4 * threshold_t)
total = len(response_times)
if total == 0:
return 1.0
return (satisfied + tolerating * 0.5) / total
6.4 AI 应用的分析与告警策略#
AI 应用(LLM/RAG/Agent)的告警策略与传统微服务有显著差异, 因为 AI 应用的"错误"不仅仅是 HTTP 5xx,还包括质量退化、成本失控等。
6.4.1 AI 应用的 USED 告警矩阵#
维度 |
指标 |
告警条件 |
说明 |
|---|---|---|---|
Usage |
Token 使用量 |
日消耗 > 预算 80% |
成本控制 |
Usage |
API 调用次数 |
接近速率限制 (> 80% rate limit) |
防止被限流 |
Saturation |
请求队列深度 |
队列积压 > 100 |
LLM API 处理不过来 |
Error |
LLM API 错误率 |
429/529 错误率 > 5% |
被限流或服务过载 |
Error |
质量评分下降 |
Faithfulness < 0.7 |
生成质量退化 |
Delay |
TTFT (首 Token 时间) |
P99 > 5s |
用户等待过久 |
Delay |
E2E 延迟 |
P99 > 30s |
端到端体验差 |
6.4.2 成本告警:AI 应用的特有需求#
传统微服务很少需要"成本告警",但 LLM 应用的成本可能在一夜之间失控:
# LLM 成本告警规则
groups:
- name: llm-cost-alerts
rules:
# 单次请求成本异常
- alert: LLMHighCostPerRequest
expr: |
rate(llm_cost_dollars_total[5m])
/ rate(llm_requests_total[5m]) > 0.10
for: 5m
annotations:
summary: "单次 LLM 请求平均成本 > $0.10"
# 日成本超预算
- alert: LLMDailyBudgetExceeded
expr: |
increase(llm_cost_dollars_total[24h]) > 100
annotations:
summary: "LLM 日成本超过 $100 预算"
# Token 使用量异常飙升
- alert: LLMTokenSpike
expr: |
rate(llm_token_usage_total[5m])
> 2 * rate(llm_token_usage_total[1h] offset 1d)
for: 10m
annotations:
summary: "LLM Token 使用量是昨天同期的 2 倍以上"
6.4.3 质量告警:从"能用"到"好用"#
AI 应用的质量退化往往是渐进的,不像传统服务那样突然崩溃:
传统服务: 正常 → 错误率飙升 → 告警 → 修复
AI 应用: 正常 → 质量缓慢下降 → 用户投诉增多 → 才发现问题
因此需要持续的质量监控:
定期采样评估: 每小时随机抽取 N 个请求,用 LLM-as-Judge 评估
用户反馈关联: 将用户的 👍👎 反馈与度量数据关联
基线对比: 与历史质量基线对比,检测退化趋势
6.5 本章小结#
本章讲解了度量数据的分析方法和报警系统的构建:
USE 方法用于分析系统资源
RED 方法和黄金信号用于分析微服务
报警需要分级、及时、准确且可操作
APDEX 是衡量用户满意度的标准指标
AI 应用需要额外的成本告警和质量告警
备注
关键要点:
分析方法: USE (资源层) + RED (服务层) + 黄金信号
报警原则: 及时、准确、可操作、不疲劳
使用分组、抑制、静默避免报警风暴
APDEX 将复杂的性能数据转化为简单的满意度评分
AI 应用的告警维度: 成本 + 质量 + 传统 USED
💡 避坑指南:告警系统的 4 个致命错误
错误 1:告警没有 Runbook
告警触发了,然后呢?如果 on-call 工程师不知道该怎么处理,告警就是噪音。 每条告警规则都应该附带一个 Runbook 链接,说明排查步骤和处理方法。
错误 2:阈值拍脑袋
"CPU > 80% 就告警"——这个 80% 是怎么来的?应该基于历史数据和 SLO 来设定阈值, 而不是凭感觉。
错误 3:只告警不恢复
告警触发了通知,但问题自动恢复后没有"恢复通知"。 结果 on-call 工程师半夜爬起来排查一个已经自愈的问题。
错误 4:所有告警同一优先级
如果所有告警都是 P0,那就等于没有 P0。 严格区分 P0 (立即响应)、P1 (1 小时内)、P2 (下个工作日)。
📝 思考题
你的团队是否经历过"告警疲劳"?你会如何改善?
用 APDEX 公式计算你最重要的 API 的用户满意度。T 值应该设为多少?
如果你的 AI 应用质量缓慢下降 (每周降 2%),你的告警系统能检测到吗?