56.3 on-call (值班 / 升级路径 / runbook)
一句话
on-call 的目标不是“有人接电话”,而是在可接受的时间内,让有权限、有上下文的人开始止血;排班、路由、认领、升级、交接,缺一个都会把 MTTR 拉长。
先破除四个误区
| 误区 | 现实 |
|---|---|
| “on-call 就是加班” | 值班是责任 + 授权 + 补偿的组合;只给责任不给权限,值班就变成“转发消息的人” |
| “谁写的服务谁值班” | 写的人可能不在、不熟、没权限;值班要覆盖“服务 + 平台 + 依赖”,靠排班而不是靠记忆 |
| “升级 = 不信任” | 升级是时间盒:到点没人接就找下一个人,避免一个睡着的手机把事故拖成几小时 |
| “值班就是盯大屏” | 盯屏的时段之外全是盲区;值班靠告警路由与 runbook,不靠人眼 |
值班体系的五件套
| 件 | 要解决的问题 | 缺失的后果 |
|---|---|---|
| 排班 | 谁在什么时间负责 | 出事没人认领,群里互相问“谁看下” |
| 路由 | 哪类告警找谁 | 所有告警发一个大群,最后没人看 |
| 认领(ack) | 谁已经开始处理 | 两人同时改、或都以为对方在处理 |
| 升级 | 多久没接就找谁 | 一个手机静音,事故过夜 |
| 交接 | 换班时哪些事在途 | 事故断档、重复排查、用户被重复打扰 |
五件套之外还有两个隐形的:runbook(怎么止血)与权限(能不能执行)。没有权限的 on-call,只能看着事故发生。
升级路径:用时间盒,不用“看情况”
| 层 | 时间 | 谁 | 权限 |
|---|---|---|---|
| L1 | 0–5 分钟 | 服务 primary | 重启/回滚/扩容/关开关 |
| L2 | 5–10 分钟 | 平台 secondary | 切流/改配置/动数据库只读 |
| L3 | 10–15 分钟 | 经理/技术负责人 | 跨团队协调、对外通报、拍板降级 |
| L4 | 15 分钟+ | 总监/事故响应 | 启动事故流程、资源调配、对外声明 |
时间不是越长越好:P0 的 ack 超时通常 5 分钟,P1 可以 15 分钟;超时即升级,不询问“要不要升级”。
下面的程序把三条事故的升级过程跑出来,并统计值班负荷:
package main
import "fmt"
// 升级路径是一个时间盒:到点没人接,就找下一个人。
// 不是“不信任”,而是避免一个睡着的手机把事故拖成几个小时。
type policy struct {
ackTimeout int // 分钟:多久没 ack 就升级
tiers []string
}
type incident struct {
id string
sev string
start string
acker string // 谁最终接手;空 = 没人接
ackMin int // 第几分钟接手;-1 = 没接
}
type page struct {
min int
who string
note string
}
func simulate(p policy, in incident) []page {
var pages []page
for i, who := range p.tiers {
at := p.ackTimeout * i
// 到这一层之前已经有人接手,就不再打扰后面的人
if in.ackMin >= 0 && in.ackMin <= at {
break
}
note := "首轮"
if i > 0 {
note = "上一位未 ack,升级"
}
pages = append(pages, page{at, who, note})
// 在下一次升级之前接手 → 停止
next := p.ackTimeout * (i + 1)
if in.ackMin > at && (i == len(p.tiers)-1 || in.ackMin < next) {
break
}
}
return pages
}
func main() {
p := policy{ackTimeout: 5, tiers: []string{"primary(order)", "secondary(platform)", "manager", "director"}}
incidents := []incident{
{"INC-1", "P1", "02:00", "primary(order)", 3},
{"INC-2", "P0", "02:00", "secondary(platform)", 12},
{"INC-3", "P0", "03:20", "", -1},
}
total := map[string]int{}
pagesAll, escalations := 0, 0
for _, in := range incidents {
fmt.Printf("== %s %s 开始(%s)==\n", in.id, in.sev, in.start)
pages := simulate(p, in)
for _, pg := range pages {
fmt.Printf(" +%2dmin page %-22s %s\n", pg.min, pg.who, pg.note)
}
if in.ackMin >= 0 {
fmt.Printf(" +%2dmin %s ack,停止升级\n", in.ackMin, in.acker)
} else {
fmt.Println(" 无人 ack:四层都叫过,仍未接 → 触发备用流程(二线负责人/事故响应)")
}
pagesAll += len(pages)
if len(pages) > 1 {
escalations += len(pages) - 1
}
for _, pg := range pages {
total[pg.who]++
}
fmt.Println()
}
fmt.Println("== 值班负荷 ==")
for _, who := range p.tiers {
if total[who] > 0 {
fmt.Printf(" %-22s %d 次\n", who, total[who])
}
}
fmt.Printf(" 本班次共 %d 次呼叫(%d 起事故,%d 次升级)\n", pagesAll, len(incidents), escalations)
fmt.Println(" 判据:夜班 >1 次/班、或 >20% 的呼叫无人立即接,就要改路由/阈值/排班")
}
本机运行结果:
== INC-1 P1 开始(02:00)==
+ 0min page primary(order) 首轮
+ 3min primary(order) ack,停止升级
== INC-2 P0 开始(02:00)==
+ 0min page primary(order) 首轮
+ 5min page secondary(platform) 上一位未 ack,升级
+10min page manager 上一位未 ack,升级
+12min secondary(platform) ack,停止升级
== INC-3 P0 开始(03:20)==
+ 0min page primary(order) 首轮
+ 5min page secondary(platform) 上一位未 ack,升级
+10min page manager 上一位未 ack,升级
+15min page director 上一位未 ack,升级
无人 ack:四层都叫过,仍未接 → 触发备用流程(二线负责人/事故响应)
== 值班负荷 ==
primary(order) 3 次
secondary(platform) 2 次
manager 2 次
director 1 次
本班次共 8 次呼叫(3 起事故,5 次升级)
判据:夜班 >1 次/班、或 >20% 的呼叫无人立即接,就要改路由/阈值/排班
注意 INC-2:secondary 在第 12 分钟接手,但 manager 已经在第 10 分钟被叫醒——升级的代价是“可能多叫醒一个人”,但不升级的代价是“事故可能没人接”。两害相权,选前者;减少误升级的办法是提高告警质量(见 56.4 告警治理),不是取消升级。
值班负荷:用数字看疲劳
| 指标 | 算法 | 经验目标 | 超标的动作 |
|---|---|---|---|
| 每班呼叫数 | 一个班次的 page 总数 | 夜班 ≤ 1 次 | 降噪、调阈值、合并告警 |
| 非工作时间呼叫占比 | 22:00–08:00 的 page / 总 page | 越低越好,持续 > 30% 要复盘 | 把可延迟的处理挪到工作时间 |
| 立即接听率 | 5 分钟内 ack / 总 page | ≥ 90% | 检查路由、手机设置、值班意愿 |
| 误报率 | 无需动作的 page / 总 page | < 20% | 先按 50.5 告警(分级 / 症状 vs 原因 / runbook / 降噪) 的“症状 vs 原因”修信号,再按 56.4 告警治理 做存量清理 |
| 连续夜班 | 连续值夜班天数 | ≤ 7 天 | 调整排班、补休 |
| MTTA | 告警到 ack 的中位数 | < 5 分钟(P0/P1) | 检查通知渠道与值班覆盖 |
🔑 值班疲劳不是“个人抗压能力”问题,是排班与告警质量的乘积。一个每晚被叫醒三次的团队,半年内一定出现“瞄一眼就关掉”的习惯。
新人上岗:shadow → reverse shadow → solo
| 阶段 | 做什么 | 通过标准 |
|---|---|---|
| Shadow | 跟班观察,不动手 | 能复述值班流程与升级路径 |
| Reverse shadow | 新人处理,老人在旁 | 独立完成一次真实/模拟事故的止血 |
| Solo | 独立值班 | 连续两个班次无遗漏,runbook 使用顺畅 |
上岗前必须演练:找一个已知告警,让新人从收到告警走到止血(可以在预发做)。没演练过的新人直接 solo,是把事故当培训。
交接:三句话说完
- 在途事故:哪起、当前状态、下一个动作、谁在负责。
- 风险:即将发布/变更、容量临界、依赖异常、静默中的告警。
- 待办:需要下一班跟进的事(含截止时间)。
交接写进值班群/交接单,不靠口头。口头交接的典型后果是“我以为他说了”。交接记录也是复盘与改进的输入,见 56.6 无指责复盘。
生产落地:指名 → 可抄 → 可验 → 可退
指名:托管值班用 PagerDuty(升级策略、日程、事件时间线成熟)或 Grafana OnCall(开源、与 Prometheus/Alertmanager 集成);事故协作可用 incident.io。小团队用“Alertmanager 路由 + 轮值表 + 电话”也能跑,但升级路径要写死。
可抄(Terraform 管理 PagerDuty 升级策略,本机无账号,未验证):
# pagerduty_user.* 资源略;升级策略本身如下
resource "pagerduty_escalation_policy" "order" {
name = "order-oncall"
num_loops = 2
rule {
escalation_delay_in_minutes = 5
target {
type = "user_reference"
id = pagerduty_user.primary.id
}
}
rule {
escalation_delay_in_minutes = 5
target {
type = "user_reference"
id = pagerduty_user.secondary.id
}
}
rule {
escalation_delay_in_minutes = 10
target {
type = "user_reference"
id = pagerduty_user.manager.id
}
}
}
可验:本节模拟器真跑过,输出如上;它验证的是“升级时间盒与负荷统计”。线上至少验证一次:发一条测试告警 → 不 ack → 5 分钟后确实升级 → 全过程记录在事件时间线里。再做一次“断网/静音”演练,确认升级不依赖某一个人的手机。
可退:
- 值班人失联:手动触发升级(PagerDuty 的 “reassign”/“escalate”),不要等自动超时。
- 告警风暴:先开启抑制/静默(设过期时间),再按 56.5 事故响应 宣布事故。
- 排班冲突:提前在排班表标注不可用时段,系统自动找替补;不要靠“到时候再说”。
反模式
| 反模式 | 后果 |
|---|---|
| 值班没有权限,只能转发 | MTTR 取决于“找到有权限的人”的时间 |
| 升级路径只写在文档里 | 真出事没人执行,或不好意思升级 |
| 所有告警发同一个群 | 没人认领,消息被刷走 |
| 值班表按“谁写谁值” | 关键人休假即盲区 |
| 连续夜班不限制 | 疲劳导致误判、漏看 |
| 新人直接 solo | 事故变培训,用户买单 |
| 交接靠口头 | 在途事故断档 |
| 用“值班补贴”代替降噪 | 钱补不了健康与判断力 |
自检
Q1 为什么 on-call 必须带“权限”,不能只带“责任”?
▸ 答案:值班的目标是止血,不是转达。没有回滚/扩容/关开关/切流的权限,值班人只能找人、等审批,MTTR 被组织流程拉长。正确做法是给“受限但足够止血”的权限(预授权操作、双人复核的高危操作),而不是让值班人当传声筒。
Q2 升级路径为什么用固定时间盒,而不是“看情况”?
▸ 答案:“看情况”在凌晨三点会退化成“再等等”。固定时间盒(如 5 分钟)把“要不要升级”变成无需判断的规则,避免值班人因为怕打扰、怕显得无能而拖延。时间盒也可以按级别不同(P0 5 分钟、P1 15 分钟),但必须是事先写死的。
Q3 升级到 manager,但 manager 被叫醒时 secondary 已经接手——这是浪费吗?
▸ 答案:是“可接受的浪费”。升级的收益是“事故一定有人接”,代价是“可能多叫醒一个人”。降低浪费的办法是提高告警质量、调整超时(比如 P1 用 15 分钟),而不是取消升级。只要误升级率持续高,就说明告警或路由有问题,去改那里。
Q4 值班负荷看哪些指标?超标先做什么?
▸ 答案:每班呼叫数、非工作时间占比、5 分钟接听率、误报率、连续夜班天数、MTTA。超标先做降噪(合并/抑制/调阈值/删原因类告警),再做排班调整;只调排班不降噪,等于把疲劳分摊给更多人。
Q5 交接必须包含哪三件事?
▸ 答案:在途事故(哪起、状态、下一个动作、负责人)、风险(即将发布/变更、容量临界、依赖异常、静默中的告警)、待办(下一班要跟进的事与截止时间)。交接要留文字记录,口头交接容易漏,尤其是“我以为他说了”的部分。