21.10-告警规则
要点
- 告警规则基于指标、日志或健康检查状态触发
- 区分不同严重级别:P0(立即处理)、P1(1 小时内)、P2(工作时间处理)
- 避免告警风暴:相同告警在一定时间内只发送一次
- 告警渠道:Slack、邮件、短信、电话
内容
1. 告警的三个要素
一个完整的告警规则包含三个要素:
| 要素 | 含义 | 示例 |
|---|---|---|
| 触发条件 | 什么情况下触发告警 | 错误率 > 5% 持续 5 分钟 |
| 严重级别 | 告警的紧急程度 | P0/P1/P2 |
| 通知渠道 | 告警发送到哪里 | Slack、邮件、短信、电话 |
2. 告警级别定义
| 级别 | 含义 | 响应时间 | 通知方式 | 示例 |
|---|---|---|---|---|
| P0 | 服务完全不可用 | 立即 | 电话 + 短信 | 所有 API 返回 5xx |
| P1 | 核心功能受损 | 1 小时内 | 短信 + Slack | LLM API 错误率 > 20% |
| P2 | 非核心功能异常 | 工作时间 | Slack + 邮件 | 缓存命中率下降 |
| P3 | 潜在风险 | 下次迭代 | 邮件 | token 消耗比昨天高 50% |
3. 基于指标的告警
3.1 错误率告警
// src/cron/alerts.ts
export default {
async scheduled(event, env, ctx) {
// 每 5 分钟执行一次
// 1. 检查 HTTP 错误率
const errorRate = await env.ANALYTICS.sql(`
SELECT
SUM(CASE WHEN blob3 >= '500' THEN _sample_interval ELSE 0 END) * 100.0 /
NULLIF(SUM(_sample_interval), 0) AS error_rate
FROM ai_gateway_metrics
WHERE blob1 = 'http.request'
AND timestamp > NOW() - INTERVAL '5' MINUTE
`)
const rate = errorRate[0]?.error_rate || 0
if (rate > 10) {
// P0:错误率 > 10%
await sendAlert(env, {
level: 'P0',
type: 'high_error_rate',
message: `HTTP 错误率 ${rate.toFixed(2)}% > 10%`,
metric: errorRate,
})
} else if (rate > 5) {
// P1:错误率 > 5%
await sendAlert(env, {
level: 'P1',
type: 'high_error_rate',
message: `HTTP 错误率 ${rate.toFixed(2)}% > 5%`,
metric: errorRate,
})
}
},
}3.2 延迟告警
// 检查 P99 延迟
const p99 = await env.ANALYTICS.sql(`
SELECT APPROX_QUANTILE(0.99, double1) AS p99_ms
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call.success'
AND timestamp > NOW() - INTERVAL '5' MINUTE
`)
const p99Ms = p99[0]?.p99_ms || 0
if (p99Ms > 30000) {
// P1:P99 延迟 > 30s
await sendAlert(env, {
level: 'P1',
type: 'high_latency',
message: `LLM 调用 P99 延迟 ${p99Ms.toFixed(0)}ms > 30000ms`,
})
} else if (p99Ms > 10000) {
// P2:P99 延迟 > 10s
await sendAlert(env, {
level: 'P2',
type: 'high_latency',
message: `LLM 调用 P99 延迟 ${p99Ms.toFixed(0)}ms > 10000ms`,
})
}3.3 Token 消耗告警
// 检查 token 消耗
const tokenUsage = await env.ANALYTICS.sql(`
SELECT SUM(double2) AS total_tokens
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call.success'
AND timestamp > NOW() - INTERVAL '1' HOUR
`)
const tokens = tokenUsage[0]?.total_tokens || 0
// 和昨天同时段对比
const yesterdayTokens = await env.ANALYTICS.sql(`
SELECT SUM(double2) AS total_tokens
FROM ai_gateway_metrics
WHERE blob1 = 'llm.call.success'
AND timestamp > NOW() - INTERVAL '25' HOUR
AND timestamp < NOW() - INTERVAL '24' HOUR
`)
const yesterday = yesterdayTokens[0]?.total_tokens || 1
const increase = (tokens - yesterday) / yesterday * 100
if (tokens > 10000000 && increase > 100) {
// P1:token 消耗 > 1000 万且比昨天高 100%
await sendAlert(env, {
level: 'P1',
type: 'token_spike',
message: `Token 消耗 ${tokens} 比昨天同时段高 ${increase.toFixed(0)}%`,
})
} else if (increase > 50) {
// P3:token 消耗比昨天高 50%
await sendAlert(env, {
level: 'P3',
type: 'token_increase',
message: `Token 消耗比昨天同时段高 ${increase.toFixed(0)}%`,
})
}4. 基于日志的告警
某些错误不适合用指标,需要用日志:
// 检查特定错误模式
const logs = await env.LOGS.query(`
SELECT COUNT(*) as count
FROM logs
WHERE level = 'error'
AND message LIKE '%LLM API timeout%'
AND timestamp > NOW() - INTERVAL '5' MINUTE
`)
if (logs[0].count > 10) {
await sendAlert(env, {
level: 'P1',
type: 'llm_timeout',
message: `LLM API 超时 ${logs[0].count} 次`,
})
}5. 基于健康检查的告警
// 检查健康状态
const health = await fetch('https://your-worker.com/health')
const status = await health.json()
if (status.status === 'error') {
await sendAlert(env, {
level: 'P0',
type: 'service_down',
message: '服务不可用',
details: status.checks,
})
} else if (status.status === 'degraded') {
await sendAlert(env, {
level: 'P1',
type: 'service_degraded',
message: '服务降级',
details: status.checks,
})
}6. 告警去重和抑制
相同告警在一定时间内只发送一次:
// src/lib/alert-dedup.ts
export async function shouldSendAlert(
env: Env,
alertKey: string,
cooldownMinutes: number = 30
): Promise<boolean> {
const lastSent = await env.ALERT_KV.get(`alert:${alertKey}`)
if (lastSent) {
const lastSentTime = parseInt(lastSent)
const cooldownMs = cooldownMinutes * 60 * 1000
if (Date.now() - lastSentTime < cooldownMs) {
return false // 在冷却期内,不发送
}
}
// 记录发送时间
await env.ALERT_KV.put(`alert:${alertKey}`, String(Date.now()))
return true
}// src/cron/alerts.ts
if (rate > 10) {
const shouldSend = await shouldSendAlert(env, 'high_error_rate', 30)
if (shouldSend) {
await sendAlert(env, {
level: 'P0',
type: 'high_error_rate',
message: `HTTP 错误率 ${rate.toFixed(2)}% > 10%`,
})
}
}7. 告警渠道
7.1 Slack
// src/lib/alert-channels/slack.ts
export async function sendSlackAlert(env: Env, alert: Alert) {
const webhook = env.SLACK_WEBHOOK_URL
const color = {
'P0': '#ff0000', // 红色
'P1': '#ff9900', // 橙色
'P2': '#ffcc00', // 黄色
'P3': '#0099ff', // 蓝色
}[alert.level]
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [{
color,
title: `[${alert.level}] ${alert.type}`,
text: alert.message,
fields: [
{ title: 'Level', value: alert.level, short: true },
{ title: 'Time', value: new Date().toISOString(), short: true },
],
}],
}),
})
}7.2 邮件
// src/lib/alert-channels/email.ts
export async function sendEmailAlert(env: Env, alert: Alert) {
await env.EMAIL_SENDER.send({
to: env.ALERT_EMAIL_RECIPIENTS.split(','),
subject: `[${alert.level}] ${alert.type}`,
text: `${alert.message}\n\nTime: ${new Date().toISOString()}`,
})
}7.3 短信/电话
// src/lib/alert-channels/sms.ts
export async function sendSMSAlert(env: Env, alert: Alert) {
// 使用 Twilio 或其他短信服务
await fetch('https://api.twilio.com/2010-04-01/Accounts/xxx/Messages.json', {
method: 'POST',
headers: {
'Authorization': 'Basic ' + btoa(`${env.TWILIO_SID}:${env.TWILIO_TOKEN}`),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
From: env.TWILIO_PHONE,
To: env.ALERT_PHONE,
Body: `[${alert.level}] ${alert.message}`,
}),
})
}8. 告警路由
根据告警级别和类型,发送到不同渠道:
// src/lib/alert-router.ts
export async function sendAlert(env: Env, alert: Alert) {
const { level, type } = alert
// P0:电话 + 短信 + Slack
if (level === 'P0') {
await Promise.all([
sendSMSAlert(env, alert),
sendSlackAlert(env, { ...alert, channel: '#alerts-critical' }),
])
}
// P1:短信 + Slack
else if (level === 'P1') {
await Promise.all([
sendSMSAlert(env, alert),
sendSlackAlert(env, { ...alert, channel: '#alerts' }),
])
}
// P2:Slack + 邮件
else if (level === 'P2') {
await Promise.all([
sendSlackAlert(env, { ...alert, channel: '#alerts' }),
sendEmailAlert(env, alert),
])
}
// P3:邮件
else if (level === 'P3') {
await sendEmailAlert(env, alert)
}
// 记录告警到数据库
await env.DB.prepare(`
INSERT INTO alerts (level, type, message, timestamp)
VALUES (?, ?, ?, ?)
`).bind(alert.level, alert.type, alert.message, Date.now()).run()
}9. 告警升级
如果告警在一定时间内没有处理,自动升级:
// src/cron/alert-escalation.ts
export default {
async scheduled(event, env, ctx) {
// 每 10 分钟执行一次
const unacknowledged = await env.DB.prepare(`
SELECT * FROM alerts
WHERE acknowledged = false
AND timestamp < ?
`).bind(Date.now() - 30 * 60 * 1000).all() // 30 分钟未处理
for (const alert of unacknowledged.results) {
// 升级告警
const newLevel = alert.level === 'P1' ? 'P0' : alert.level
await sendAlert(env, {
...alert,
level: newLevel,
message: `[升级] ${alert.message}`,
})
// 记录升级
await env.DB.prepare(`
UPDATE alerts
SET level = ?, escalated = true
WHERE id = ?
`).bind(newLevel, alert.id).run()
}
},
}10. 实战:完整的告警系统
// src/cron/alerts.ts
import { shouldSendAlert } from '../lib/alert-dedup'
import { sendAlert } from '../lib/alert-router'
export default {
async scheduled(event, env, ctx) {
const alerts: Alert[] = []
// 1. HTTP 错误率
const errorRate = await getErrorRate(env)
if (errorRate > 10) {
alerts.push({
level: 'P0',
type: 'high_error_rate',
message: `HTTP 错误率 ${errorRate.toFixed(2)}% > 10%`,
})
} else if (errorRate > 5) {
alerts.push({
level: 'P1',
type: 'high_error_rate',
message: `HTTP 错误率 ${errorRate.toFixed(2)}% > 5%`,
})
}
// 2. P99 延迟
const p99 = await getP99Latency(env)
if (p99 > 30000) {
alerts.push({
level: 'P1',
type: 'high_latency',
message: `P99 延迟 ${p99.toFixed(0)}ms > 30000ms`,
})
}
// 3. Token 消耗
const tokenUsage = await getTokenUsage(env)
if (tokenUsage > 10000000) {
alerts.push({
level: 'P2',
type: 'high_token_usage',
message: `Token 消耗 ${tokenUsage} > 1000 万`,
})
}
// 发送告警(去重)
for (const alert of alerts) {
const shouldSend = await shouldSendAlert(env, `${alert.level}:${alert.type}`, 30)
if (shouldSend) {
await sendAlert(env, alert)
}
}
},
}11. 小结
告警规则的关键点:
- 触发条件:基于指标、日志或健康检查状态
- 严重级别:P0/P1/P2/P3,不同级别用不同通知方式
- 告警去重:相同告警在冷却期内只发送一次
- 告警路由:根据级别发送到不同渠道(Slack、邮件、短信)
- 告警升级:未处理的告警自动升级
告警是运维自动化的核心。没有告警,你就只能靠人工巡检。有了告警,系统会在问题发生时主动通知你。