排班可以分配责任,但相同班次数量并不保证负担公平。应同时检查夜间告警、事故处理时间、恢复休息和可靠备援,而不只看每个人在表里出现了几次。

一份被共同承担的职责

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

交接是工作的一半

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

在铃声响起前演练

演练应安排在告警响起之前,而不是等故障发生。低峰期把 runbook 平铺地走一遍,练的是「恢复路径」而不是症状集;每一条 pager 都应映射到一份写好的计划,让雪夜里的铃声到来时,它是 checklist 而不是谜。提前练习可以增加团队在压力下的可靠选择;平静时候一遍遍过的常规动作,正是把「到时候再想」变成「按部就班照做」的地方。

坏掉的样子

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

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

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

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

虚构排班让五人轮转二十个连续星期,每周主值班与备援不同。前五周含两个虚构特殊覆盖周;周期重复后每人每角色四次,但不能证明负担公平。
每周都保留主值班与备援。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。

快速验证

在正式发布排班前,可以先运行这个纸面模型。它为二十个连续星期安排虚构的主值班与备援,并保留两周虚构的特殊覆盖标记。标记不代表任何国家真实的公共假日,遇到标记也不会让整个服务无人负责。持续运行的服务,在某位工程师休假时仍需要明确接替,不能以节日为由直接删除那一周的覆盖。

  1. 把代码保存为空目录下的 rota_probe.py。
  2. 用 Python 3.9 或更新版本运行 python3 rota_probe.py;程序不访问网络或文件。运行时不要使用 -O、-OO 或非零的 PYTHONOPTIMIZE,以免断言被跳过。
  3. 核对逐周人员、各角色次数和最后的 PASS 行。
from collections import Counter
from datetime import date, timedelta

TEAM = ["ana", "ben", "cara", "dev", "elin"]  # fictional names
START = date(2027, 8, 2)
WEEKS = 20
SPECIAL_WEEKS = {START + timedelta(weeks=1), START + timedelta(weeks=4)}
rota = [(START + timedelta(weeks=i), TEAM[i % 5], TEAM[(i + 1) % 5])
        for i in range(WEEKS)]
assert len(rota) == WEEKS
assert len({week for week, _, _ in rota}) == WEEKS
assert all(primary != secondary for _, primary, secondary in rota)
assert all(primary in TEAM and secondary in TEAM for _, primary, secondary in rota)
assert all(b[0] - a[0] == timedelta(days=7) for a, b in zip(rota, rota[1:]))
assert all(a[1] != b[1] for a, b in zip(rota, rota[1:]))
primary_counts = Counter(primary for _, primary, _ in rota)
secondary_counts = Counter(secondary for _, _, secondary in rota)
assert set(primary_counts.values()) == {4}
assert set(secondary_counts.values()) == {4}
for week, primary, secondary in rota:
    marker = " special-coverage" if week in SPECIAL_WEEKS else ""
    print(f"{week}: primary={primary} secondary={secondary}{marker}")
assert sum(week in SPECIAL_WEEKS for week, _, _ in rota) == 2
print("primary:", dict(sorted(primary_counts.items())))
print("secondary:", dict(sorted(secondary_counts.items())))
print("PASS: 20 covered weeks, distinct backup, four turns per role; special weeks remain staffed")

每一周都有不同的主值班与备援,包括两周特殊覆盖。每人分别获得四次主值班和四次备援,相邻两周的主值班也不同。本轮在 Python 3.12.14 中执行通过了这些断言。它们只证明表里明确检查的排班性质:星期没有缺口、两个角色不由同一人兼任,以及各角色计数相等。

计数相等不代表夜间打断相等,也不衡量同时发生多起事故、人员真实可用性、时区、补偿或恢复休息。正式启用前,仍应让团队确认这些约束,并测试真实升级联系人。若安排了请假、培训或其他职责,应重新验证替补后的完整覆盖,而不是只把某个名字从表里删除。模型没有给任何真实人员发送通知,也没有承诺他们同意承担某次值班。

延伸阅读

来源