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,是把事故当培训。

交接:三句话说完

  1. 在途事故:哪起、当前状态、下一个动作、谁在负责。
  2. 风险:即将发布/变更、容量临界、依赖异常、静默中的告警。
  3. 待办:需要下一班跟进的事(含截止时间)。

交接写进值班群/交接单,不靠口头。口头交接的典型后果是“我以为他说了”。交接记录也是复盘与改进的输入,见 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 交接必须包含哪三件事?

▸ 答案:在途事故(哪起、状态、下一个动作、负责人)、风险(即将发布/变更、容量临界、依赖异常、静默中的告警)、待办(下一班要跟进的事与截止时间)。交接要留文字记录,口头交接容易漏,尤其是“我以为他说了”的部分。

关联