21.10-告警规则

要点

  • 告警规则基于指标、日志或健康检查状态触发
  • 区分不同严重级别:P0(立即处理)、P1(1 小时内)、P2(工作时间处理)
  • 避免告警风暴:相同告警在一定时间内只发送一次
  • 告警渠道:Slack、邮件、短信、电话

内容

1. 告警的三个要素

一个完整的告警规则包含三个要素:

要素含义示例
触发条件什么情况下触发告警错误率 > 5% 持续 5 分钟
严重级别告警的紧急程度P0/P1/P2
通知渠道告警发送到哪里Slack、邮件、短信、电话

2. 告警级别定义

级别含义响应时间通知方式示例
P0服务完全不可用立即电话 + 短信所有 API 返回 5xx
P1核心功能受损1 小时内短信 + SlackLLM 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. 小结

告警规则的关键点:

  1. 触发条件:基于指标、日志或健康检查状态
  2. 严重级别:P0/P1/P2/P3,不同级别用不同通知方式
  3. 告警去重:相同告警在冷却期内只发送一次
  4. 告警路由:根据级别发送到不同渠道(Slack、邮件、短信)
  5. 告警升级:未处理的告警自动升级

告警是运维自动化的核心。没有告警,你就只能靠人工巡检。有了告警,系统会在问题发生时主动通知你。