4.3 指标与告警:QPS、延迟、错误率
TraceID 解决「一次请求怎么串」,这一篇解决「一个服务健康不健康」。我们用 prometheus 客户端库暴露指标,自研采集器把 17 个服务的指标聚到 2 台 CVM 上,告警走 CLS 检索加企业微信推送。QPS、延迟、错误率三条线各定各的阈值与看板,这一篇把口径、阈值来源、误报治理写全。指标口径对照 go-micro-cps-guide 的可观测篇,本篇只讲我们 17 个服务上的数字。
场景:无口径告警的两种死法
| 死法 | 现象 | 根因 |
|---|---|---|
| 误报风暴 | 半夜被 CPU 80% 叫醒三次,实际无业务影响 | 阈值拍脑袋,没和 P99 挂钩 |
| 漏报 | 订单服务延迟已涨 4 倍,告警没响 | 只看平均延迟,被快请求稀释 |
第二种最致命。1.1 那次 35 分钟排障,前 10 分钟没人收到告警,因为平均延迟 38ms 离 200ms 阈值还远,而真实情况是慢请求已经占到 30%。三条线必须分开看,延迟看 P99 不看平均。
还有一个隐性死法:告警有了但不收敛。2.3 一次 etcd 抖动,17 个服务的熔断、错误率、延迟三条告警在 10 分钟里响了 40 多条,值班同学关掉三条才反应过来「这是同一件事」。告警的价值在收敛后还立不立得住,是这一篇验收的一条硬指标。
第二条死法在 1.1 里出现过完整过程:订单服务延迟涨 4 倍时平均延迟才从 38ms 挪到 55ms,离 200ms 阈值还差着,告警一片安静,等 4.2 的链路检索被手工触发时已经是 35 分钟后。稀释效应在大促更狠:快请求占比越高,平均曲线越平,P99 与慢请求占比才是唯一不撒谎的读数。所以三条线的看板排版规则是「P99 与慢请求占比在上、平均延迟在下且不参与告警」,视觉权重直接决定值班同学第一眼读哪条线。
方案:三条线的口径
| 线 | 指标 | 口径 | 告警阈值 | 来源 |
|---|---|---|---|---|
| QPS | 每秒请求数 | 入口网关分服务统计 | 同比昨日跌 50% | 压测基线 |
| 延迟 | P50/P95/P99 | 网关到响应完整耗时 | P99 超 800ms 持续 5 分钟 | 4.1 灰度观测口径 |
| 错误率 | 5xx 与超时占比 | 入口统计 | 超 2% 持续 3 分钟 | 3.1 熔断触发线 |
三条线共同带维度:service、version、trace 命中与否。灰度期 4.1 说 10% 流量跑新版本,看板的 version 维度是分新旧曲线的前提,没这个维度灰度等于盲发。
延迟用直方图(histogram)而非 Gauge:客户端侧埋 8 个桶(10、50、100、200、500、1000、2000、5000ms),服务端只上报桶计数,聚合机算分位。17 个服务全量上报桶计数,比每请求存一个样本省 20 倍存储。P99 的口径再强调一次:用「慢请求占比」辅助读,占比 = 落在 500ms 以上桶的计数 / 总计数。1.1 那次事故,P99 从 120ms 涨到 640ms,占比曲线却已提前 8 分钟过 30%,慢请求占比才是最早的信号。
// 请求结束钩子:三条线一次埋点
httpReqDuration.WithLabel(svc, ver).Observe(elapsed)
httpReqTotal.WithLabel(svc, ver, code).Inc()
if isTrace(sampled) { traceDepth.Inc() }
配置与代码:聚合与告警
采集侧:每服务本地 ring buffer 攒 15 秒,2 台 CVM 上跑聚合器拉取,内存里按 service、version 分桶计算。告警判定在聚合器里完成,命中即推企业微信机器人,消息体带「服务、维度、当前值、阈值、对应 traceId 检索串」,值班同学点检索串直接进 CLS 看现场。
| 告警项 | 窗口 | 触发 | 动作 |
|---|---|---|---|
| 错误率超线 | 3 分钟 | >2% | 推群 + 自动摘灰度流量 |
| P99 超线 | 5 分钟 | >800ms | 推群 + 拉 trace 样本 |
| QPS 断崖 | 10 分钟 | 同比 -50% | 电话告警 |
| 熔断打开 | 即时 | 半开探测连续失败 | 推群 |
QPS 断崖是唯一电话告警项,理由是入口跌 50% 往往意味着上游挂了或网关卡了,这类问题等人看群已经晚了。电话告警每月复盘一次误报,超过 2 次误报就要调窗口。
// 告警判定:同源收敛,5 分钟内同一 service 只推一条
func (g *group) fire(item Alert) {
if g.lastFired(item.Service).Add(5 * time.Minute) > now {
g.merge(item) // 并入已推消息的子项清单
return
}
g.push(wechat, item.WithTraceLookup()) // 消息体带 CLS 检索串
}
收敛逻辑放在聚合器里而不是推送通道里,是为了保证「子项清单」始终完整:同一 service 在 5 分钟内触发的错误率、延迟、熔断三条告警,值班只收到第一条加合并后的清单,不会出现「三条消息讲同一件事」。
配置与代码:告警推送与检索串
告警消息体带 CLS 检索串,值班点进去即现场。消息体四件套:
| 字段 | 内容 | 用途 |
|---|---|---|
| service、version | 告警维度 | 定位哪个版本 |
| 当前值、阈值 | 数字对照 | 判断严重度 |
| traceId 检索串 | CLS 直链 | 10 分钟定位现场 |
| 已响子项 | 同源收敛清单 | 避免重复告警 |
收敛后值班一周内抽检三条告警,要求 10 分钟内能进 CLS 讲清现场,讲不清就说明告警带信息不足,改消息体模板而不是加告警项。
踩坑:误报治理四招
第一,平均延迟误导。看板加 P99 后,慢请求占比曲线和平均延迟曲线分开摆,2.4 说某实例负载倾斜,倾斜的证据就来自「P99 分 version 对比」。
第二,同比基线被活动打穿。大促 QPS 是平日 5 倍,同比昨日 -50% 的告警在大促当晚连响 6 次。改法:QPS 断崖告警叠加「且绝对值低于压测基线的 1/10」双条件,大促不再误报。
第三,告警不收敛。熔断、错误率、延迟三个告警同一事故各响一次,值班收到三条重复消息。改法:告警按 service 维度收敛,5 分钟内同源告警合并一条,附上已响过的子项清单。
第四,灰度期阈值不切换。新版本上线初期 P99 因冷启动偏高,旧阈值 800ms 连响 3 天。改法:version 维度独立的阈值组,新版本前 24 小时用宽松阈值(1500ms),之后切回公共线。4.1 的放量节奏就是按这套「先宽松后收紧」的曲线走的。
演练:每周一次误报复盘
误报治理不靠自觉,靠制度。每周一值班复盘最近 7 天的告警:逐条标记「真警」或「误报」,误报的进原因表(阈值拍脑袋、活动打穿、同源没收敛、灰度冷启动四类),每类定一个消减动作。连续两周同类误报清零,才允许把该类阈值放宽一档。17 个服务的告警项共 52 条,这套复盘把月均误报从 40 多条压到 6 条以内,告警信誉维持住了。
验收与参数速查
| 参数 | 取值 | 说明 |
|---|---|---|
| 采集窗口 | 15 秒 | 本地 ring buffer |
| 延迟桶 | 8 桶至 5000ms | 客户端直方图 |
| 错误率线 | 2% / 3 分钟 | 与 3.1 熔断线同源 |
| 收敛 | 5 分钟同源合并 | 按 service |
验收动作两条:值班周会抽检 3 条告警,从推送消息点进 CLS 检索串,10 分钟内要能定位到现场 traceId;每月把电话告警的误报次数报给服务负责人,连续两月零误报即把窗口放宽一档。这套验收让告警保持「响了就该动」的信誉,误报治理才有抓手。收敛率也是验收项:同源 5 分钟内只推一条,月抽检里收敛不达标(一条事故推了 3 条)就要回到收敛逻辑改合并窗口。告警项每月做一次「僵尸项」清理:连续 90 天未触发的项标记待审,要么调阈值要么下线,避免 52 条告警里混进一批永不响的摆设。
小结
三条线一句话:QPS 防断崖、P99 防稀释、错误率与熔断同源。指标口径定死在 etcd,告警带 traceId 检索串直达 CLS(4.4),灰度期按 version 切换阈值。下一篇 4.4 讲日志怎么进 CLS、检索模板怎么配,三条线的现场证据都在那里。
延伸阅读
- 指标体系与告警分级的更多设计,见 go-micro-cps-guide。
- 云侧监控与推送的实操,见 tencent-cloud-practice。
- 采集器与检索的日志流水线对照,见 rust-logpipe-guide。
- 灰度放量与指标联动的节奏,见 k8s-pipeline-guide。
