合理的值班是设计,而不是某个人的宿命。轮换、交接与演练,让被深夜叫醒成为一种共同、可持续的日常。

一份被共同承担的职责

值班是团队的事,不是某一位英雄的。轮班把「谁上次没睡好就谁来」变成一张明确的排班表、一套流程,让被打断分担到每个人头上,而不是让一个人独自背完一整条告警队列。而具体排班好不好,就在于「换人不换上下文」这六个字——交接若只换名字不换现状,同一个故障会在每次轮换时被重新发现一遍。所以轮班的灵魂不是「谁那天晚上在」,而是「责任与上下文如何平稳移交」。

交接是工作的一半

一次值班最容易断的,恰是交接这个环节。当接手的人从零开始、一片黑地接过来,本来该是交接的事就变成了重新破案。堵住这个缝只需要两样:一份书面交接(当前坏在哪、试过哪些、下一棒该试什么),以及一段重叠时间——让接班的人跟着旧的人一起盯岗,旧的人在正式放下前把现场讲完。一张只用来排名字的表,会一次次把同一个事故变成新事故。做出这套的难点,不在排满班,而在排完后还留得住上下文。

在铃声响起前演练

排上一次必须在铃响之前,而不是等它响。低峰期把 runbook 平铺地走一遍,练的是「恢复路径」而不是症状集;每一条 pager 都应映射到一份写好的计划,让雪夜里的铃声到来时,它是 checklist 而不是谜。你提前练熟的那部分是肾上腺素上来后团队唯一不会搞砸的部分;平静时候一遍遍过的常规动作,正是把「到时候再想」变成「按部就班照做」的地方。

坏掉的样子

单人长期值班看似省事,实则先压垮的是人。一个人连续几个月拿着寻呼机,即使没有告警也睡不踏实,看到通知就心慌,最后要么倦怠请辞,要么在一次深夜故障中彻底失手。谷歌站点可靠性工程把这种无边界值班明确称为人员配置缺陷,而不是奉献。解法是设定单次值班上限,并保证下线周真正下线,上周的主值班不再兼任隐形备援。

升级链断裂是第二种常见死法。备援从不确认、联系方式过期,或者所谓升级到经理只是发进一个无人查看的邮箱,深夜三点主值班只能独自拆弹。升级链要像恢复流程一样演练:在工作时间实际寻呼一次备援,确认有人能在几分钟内应答,而不是只在通讯录里写个名字。

告警疲劳是第三种慢性失效。几百条噪声大、无法行动的告警教会团队寻呼机在说谎,于是唯一真实的那条也被顺手划掉。每条告警都应有负责人、有可执行的 runbook 链接、有经过上次误报调优后的阈值,无法行动的告警宁可降级为工单,也不要继续消耗信任。

交接缺失则让轮换只换名字不换上下文。周一换人时没有书面状态,周二就把周一的排查从零再做一遍。必须要求简短交接记录:当前症状、已尝试步骤、下一步动作,并在旧主值班真正下线前保留十五分钟重叠时间,把现场讲完再走。

快速验证

在让任何人牺牲周末之前,先在纸面上验证公平性。下面脚本用五人按周轮换演示:遇到节假日所在周自动跳过且不消耗轮次,最后打印人均负担统计,让管理者一眼看出是否有人被多排。

  1. 把代码保存为空目录下的 rota_probe.py。
  2. 用 Python 3.9 及以上版本运行 python3 rota_probe.py,仅用标准库,不访问网络,不读写文件。
  3. 确认输出逐周排班表、人均次数统计和末尾 PASS 行;若出现断言失败,说明轮换逻辑已经退化,需要先修排班再排人。
from collections import Counter
from datetime import date, timedelta

TEAM = ["ana", "ben", "cara", "dev", "elin"]
START = date(2027, 8, 2)  # a Monday
WEEKS = 22
HOLIDAYS = {date(2027, 8, 9), date(2027, 8, 30)}  # Monday public holidays

rota = []  # list of (monday, person or None)
cursor = 0
for week in range(WEEKS):
    monday = START + timedelta(weeks=week)
    if monday in HOLIDAYS:
        rota.append((monday, None))  # holiday week: nobody on call, pointer stays
        continue
    person = TEAM[cursor % len(TEAM)]
    rota.append((monday, person))
    cursor += 1

assigned = [person for _, person in rota if person is not None]
counts = Counter(assigned)

for monday, person in rota:
    label = person if person else "holiday-skip"
    print(f"{monday.isoformat()} -> {label}")
print("counts:", dict(sorted(counts.items())))

# Load-balance statistics: 20 assigned weeks / 5 people = 4 each.
assert len(assigned) == WEEKS - len(HOLIDAYS) == 20, len(assigned)
assert set(counts) == set(TEAM), counts
assert all(count == 4 for count in counts.values()), dict(counts)
assert max(counts.values()) - min(counts.values()) == 0, dict(counts)
# Holiday weeks consume no turn and hold nobody.
assert all(monday not in HOLIDAYS for monday, person in rota if person is not None)
assert sum(1 for _, person in rota if person is None) == len(HOLIDAYS)
# Nobody works two calendar turns in a row (pointer only advances on assigned weeks).
for prev, current in zip(assigned, assigned[1:]):
    assert prev != current, (prev, current)
print("PASS: 20 on-call weeks across 5 people, 4 each, holidays skipped, no back-to-back")

脚本共分配二十个值班周,两个节假日周空出且不推进指针,随后断言三件事:每人恰好四周、前后两周不是同一人、节假日当天无人值班。打印出的计数让不均一目了然,负责人每季度复盘时看的正是这个信号。该断言已在 macOS、Python 3.12 下运行通过。模型只覆盖按周轮换,不含昼夜拆分、跨时区接力与节假日补偿,仍需团队另行约定。

延伸阅读

来源