事件进行时,记忆不可靠,机器时钟也可能存在偏差。一条时间线——谁被叫了、观察到了什么、尝试了什么、变更何时落地——是让一次响应可以被记录、并稍后可审阅的东西。
边发生边记录
事件现场里,记忆是最不可靠的记录者,所以要在报警刚响起时就开始记。写下首次告警的时间、最初的症状、每个尝试过的动作以及它对应的可观察结果,还有修复真正生效的那一刻。这份如实记录,既让团队在事件中间不会重复试一个已经失败的修复,也让事后能写出一份诚实复盘的素材。把时间线当做从告警响起的实时文档这习惯很重要:记下触发告警的指标、怀疑的原因与后来的准确实因,并写下「现象依旧,未缓解」,而不是默默再试一遍。
把沟通与执行分开
事件扩大后,排错与同步信息会争夺注意力。明确运维与沟通职责:一个人执行操作,另一个人维护时间线,持续同步状态、影响范围和未解决的问题。这第二个角色,正是为了防止「所有人都在抢修、没人留痕」的困局。共享时间线还能缩短接手成本:后来加入的人扫一眼记录就知道进行到哪里,而不是打断每个人、听一段带着情绪的重述。事件发生后及时更新,并附上相关日志与决策的证据链接,降低交接时反复查找材料的成本。
学习需要这份记录
靠记忆写的复盘,只是一个看似合理的说法;靠时间线写的,则是一份能逐条复核的事实重建。正因为时间线把每个动作钉在具体的分钟上,复盘才更容易聚焦系统缺口而非个人责任:哪个控制项当时不存在、哪本 runbook 写得不全、哪件事迟迟没有负责人。这才是从第一秒就开始记录的意义所在。尽可能从首次告警开始记录;如果开始较晚,应把回填项明确标记出来。只要来源可靠,不完整的记录仍有价值,不能为了叙述顺畅,就把未知空白补成未经证实的确定结论。
坏掉的样子
实时时间线是在事件还在进行时坏掉的,而不是在事后复盘时写坏的,这正是它要与复盘文章区分开的地方。第一种坏法是时间戳没有时区:一个人用笔记本本地时间写下十点零二分,告警流水线却用协调世界时记录同样时刻,相同钟点可能代表不同瞬间。应保留日期、偏移、时钟来源和已知偏差;即使时钟已经同步,时间戳本身也不能证明因果关系。
第二种坏法是第二天早晨凭记忆补记。团队靠回忆重建记录,只保留最后生效的修复,删掉两次没有缓解的重试,还把顺序整理成通顺的故事。这个版本恰恰丢掉接手者最需要的东西:已经试过什么,每次尝试带来什么可观察变化。补记越圆滑,失真越严重,重复试错几乎不可避免。
第三种坏法是并行操作缺乏协调。即使编辑器完整保留每行,两名响应者也可能同时声称负责回滚。应明确变更负责人,为事件分配稳定标识,保留原始记录顺序,并标记迟到的补记。按事件时间生成的展示视图,不应悄悄改写底层证据;相同时间戳也可能来自合法的并行动作。
第四种坏法是关键决策被聊天记录淹没。切换机房的决定藏在两百条表情与插话深处,有人说了同意,却没人把决策内容、负责人与预期效果抄进时间线。一小时后谁决定了什么、谁来执行、何时见效,全都说不清楚。与复盘篇关注事后如何学习不同,这里讲的都是响应进行中的记录失效。
快速验证
把下面代码存为 timeline_check.py,在空目录运行 python3 timeline_check.py。它只用标准库,在内存里构造一好一坏两条实时时间线,不写任何文件,检查每条记录必备的三个字段:t、actor 与 action,并校验时间戳是否可解析、是否带时区、是否按时间递增。
"""Live incident timeline validator: each event needs t/actor/action, ordered by time."""
from datetime import datetime
def parse_time(raw):
text = str(raw or "").strip().replace("Z", "+00:00")
# Require a full timestamp with date and timezone, not a bare wall-clock time.
if "T" not in text or not text:
raise ValueError(f"bad timestamp {raw!r}: use ISO-8601 with date and timezone")
try:
stamp = datetime.fromisoformat(text)
except ValueError:
raise ValueError(f"bad timestamp {raw!r}: use ISO-8601 with date and timezone") from None
if stamp.tzinfo is None:
raise ValueError(f"bad timestamp {raw!r}: missing timezone")
return stamp
def check_timeline(events):
errors = []
seen = []
for i, ev in enumerate(events):
for field in ("t", "actor", "action"):
if not isinstance(ev.get(field), str) or not ev[field].strip():
errors.append(f"event {i} missing {field} or not a nonempty string")
try:
seen.append(parse_time(ev.get("t", "")))
except ValueError as exc:
errors.append(f"event {i} {exc}")
seen.append(None)
stamps = [s for s in seen if s is not None]
# Only judge ordering over entries that parsed, so one bad clock
# does not hide a second sequencing mistake.
ordered = sorted(stamps)
if stamps != ordered:
errors.append("events out of order: review clock sources and late entries")
return errors
GOOD = [
{"t": "2027-03-01T10:01:00+00:00", "actor": "pager", "action": "alert fired: error-rate burn"},
{"t": "2027-03-01T10:04:00+00:00", "actor": "ana", "action": "took timeline keeper seat, opened shared doc"},
{"t": "2027-03-01T10:09:00+00:00", "actor": "lee", "action": "rolled back deploy 1a2b3c, error-rate still 8%"},
{"t": "2027-03-01T10:14:00+00:00", "actor": "ana", "action": "decision: fail over to region-b, owner lee, effect visible by 10:20"},
]
BAD = [
{"t": "10:09", "actor": "lee", "action": "rollback done?"},
{"t": "2027-03-01T10:04:00+00:00", "actor": None, "action": "someone joined late, backfilled from memory"},
{"t": "2027-03-01T10:02:00+00:00", "actor": "ana", "action": []},
]
def main():
good_errors = check_timeline(GOOD)
bad_errors = check_timeline(BAD)
assert good_errors == [], f"good sample should pass, got: {good_errors}"
assert any("missing actor" in e for e in bad_errors), f"bad sample should flag actor, got: {bad_errors}"
assert any("missing action" in e for e in bad_errors), f"bad sample should flag action, got: {bad_errors}"
assert any("bad timestamp" in e or "timezone" in e for e in bad_errors), f"bad sample should flag clock, got: {bad_errors}"
assert any("out of order" in e for e in bad_errors), f"bad sample should flag order, got: {bad_errors}"
print("PASS: good timeline has t/actor/action on every event in time order")
print(f"FAIL as expected: bad timeline -> {'; '.join(bad_errors)}")
print("summary: 1 passed check, 1 failed check (expected 1 and 1)")
if __name__ == "__main__":
main()
好样本通过的原因是每条事件都有完整日期与时区的标准时间戳、明确记录者与可观察动作,并且按非递减时间排列;允许同一时间戳对应多条事件。坏样本把四种失真集中在一起:只有钟点没有日期时区、补记缺记录者、关键动作缺描述、时间戳还出现倒流。预期看到一行通过、一行按字段名指出全部缺陷的失败,以及一过一败的汇总。修订脚本已用 Linux 上的 Python 3.12.14 运行。它拒绝空值与非字符串字段,却不能验证观察是否真实,也不保证事件序号唯一。这里接受 Python 支持的若干 ISO-8601 形式,并非严格的 RFC 3339 校验器。它只校验响应中的记录纪律,不涉及事后复盘的责任划分,那部分请看复盘配套文章。
延伸阅读
来源
- Google SRE:事件管理,事件角色、实时文档与明确交接,已核验 2026-10-15。
- Google SRE:复盘文化,基于证据且不追责的学习,已核验 2026-10-15。
- Python datetime,带时区时间解析与比较,已核验 2026-10-15。
- RFC 3339 时间戳,偏移与文本排序的适用条件,已核验 2026-10-15。