快照:历史越来越长,恢复不能越来越慢
本文是 Agent 工程里的经典架构 系列的一篇。文中的业务场景和代码为讲解设计,产品能力以所链接的官方资料为准。
一个 Agent 连续为客户服务一年,累计了十万条事件。系统重启后,为了回答“这位客户还有什么待办”,先把十万条事件全部读取一遍。即使最后计算正确,恢复延迟也可能让用户觉得系统坏了。持久化解决了有没有记录,恢复路径还需要单独设计。
Martin Fowler 的 Snapshot 讨论的是时间对象在某个时点的状态视图,并不是某个工作流引擎的检查点教程。结合持久执行文章,我把这个思想延伸到恢复优化:在一个已知位置保存状态,再从那里继续应用后续事件。下面的代码属于这种工程延伸。
先区分两类东西。执行快照保存可用于恢复的结构化状态,要求与历史位置一致;对话摘要是压缩后的文字,可能省略信息,也可能写错。模型说“客户已经同意退款”,不能替代审批记录。两者都能减少读取量,但承担的可靠性责任不同。
例子维护一个账户的累计积分。第三条事件之后保存快照,恢复时直接从积分十七开始,只处理后面的事件。代码中还保留了状态结构版本,避免新代码把旧结构误读成合法数据。积分用于演示计算,不代表这个脚本已经具有真实账户系统的事务保障。
from copy import deepcopy
def apply(state, event):
state["points"] += event["delta"]
def restore(events, snapshot=None):
if snapshot and snapshot["schema"] == 1:
offset = snapshot["offset"]
if not 0 <= offset <= len(events):
raise ValueError("快照位置越界")
state = deepcopy(snapshot["state"])
else:
offset, state = 0, {"points": 0}
for event in events[offset:]:
apply(state, event)
return state
events = [{"delta": n} for n in (10, -2, 9, 4, -1)]
snapshot = {
"schema": 1, "offset": 3,
"state": restore(events[:3]),
}
assert snapshot["state"]["points"] == 17
assert restore(events, snapshot) == restore(events)
assert restore(events, {"schema": 0}) == restore(events)
print("快照加增量事件与完整回放结果一致")
完整示例可下载,使用 Python 3.9 或以上版本运行:python3 example.py。只依赖标准库,不调用真实业务服务。
快照必须回答两个问题:里面是什么状态,对应哪一个历史位置。只保存 points=17 不够,因为无法判断哪条事件已经应用。如果位置写成二,恢复时就会重复加一次;写成四,又会漏掉一次。这种错误不会让进程崩溃,却会悄悄算错账,比明确失败更难发现。
因此,生成快照时要获取一致的状态和序号。可以在同一事务里读取,或者在已经串行处理事件的实体内部生成。写文件时还要考虑先写临时文件、同步,再原子替换;写数据库时可以使用事务。这里的关键是发布一份完整快照,而不是让读取者看到一个正在写入的中间版本。
快照通常应该是可丢弃的加速材料。发现结构版本不兼容或校验失败,可以回退到旧快照,甚至从完整事件流重建。前提是那些事件还在。如果为了节省空间已经删掉旧历史,快照就变成唯一恢复依据,备份和验证要求会明显提高。删历史这一步需要单独评估,不能随着“做了快照”顺手完成。
保存频率是成本问题。每个事件都保存,写入成本太高;一年只保存一次,恢复时几乎没有收益。我会先测回放速度,再根据恢复时间目标设定阈值。比如按事件数量或累计字节触发,同时记录生成耗时、文件大小和回退次数。阈值应该来自业务目标,而不是照搬别人的固定数字。
在 Agent 场景,还要考虑长期实体和单次任务的区别。客户档案可能持续数年,单次售后任务只持续几天。前者更容易积累长历史,后者往往自然结束。没有必要给所有任务建立复杂的快照体系,先找出真正影响唤醒速度的那部分状态。
还有一种情况是代码升级后,状态结构没变,但计算规则变了。旧快照可能已经包含按旧规则算出的聚合值,新代码继续处理增量时,得到的是混合口径。除了结构版本,还可能需要记录投影算法版本;口径变化时重新构建,而不是盲目复用。
做验证时,可以固定同一条事件流,随机选择多个快照位置,对比所有恢复路径的结果。这样的检查直接覆盖“边界前后有没有漏算或重算”,比只验证快照文件存在更贴近实际风险。
我会把恢复演练加入发布检查:用旧快照加新事件恢复,用损坏快照验证回退,再用完整历史计算结果做对照。快照的价值在于让恢复更快,同时保留验证恢复结果的办法。只追求启动速度,却没有办法证明状态一致,实际上是把性能优化变成了数据风险。