25.5 分布式快照 (Chandy-Lamport)
一句话
Chandy-Lamport 用一个 marker 把 “瞬间拍照” 变成 “各节点各拍各的 + 记下通道里的在途消息”:收到第一个 marker 就记录本地状态,并沿 FIFO 通道转发——最终拼出一个一致的全局快照,哪怕它不对应任何真实时刻。
算法(每个进程)
| 步 | 动作 |
|---|---|
| ① | 发起者:记录本地状态,向所有出边发 marker |
| ② | 收到第一个 marker(且自己还没记录):立即记录本地状态,记录该入边为 “空”,向所有出边转发 marker |
| ③ | 收到其他入边的 marker:把该入边在 “记录本地状态之后、收到此 marker 之前” 收到的消息,记为通道状态 |
| ④ | 所有入边都收到 marker → 本进程完成 |
拼起来(本地状态 + 各通道状态)就是一致快照。
性质与前提
| 性质 | 说明 |
|---|---|
| 一致性 | 快照满足 “所有消息要么都在、要么都不在”,可线性化到某个时刻 |
| 不对应真实时刻 | 它是 “某个可能的全局状态”,不是物理瞬间 |
| 前提 | 通道可靠且 FIFO(marker 与消息不超车) |
| 消息量 | O(E)(每条边至少一个 marker) |
与流处理的关系
Flink 的异步屏障快照(ABS) 就是它的工程化:
| 概念 | 对应 |
|---|---|
| Barrier | marker |
| 算子状态 | 本地状态 |
| 通道状态 | 缓冲的在途记录(对齐时暂存) |
| 对齐(alignment) | 等所有入边的 barrier 到齐再快照 |
| 非对齐(unaligned) | 不等待,把在途数据也写进快照(避免反压放大) |
“对齐” 的代价是反压:一条慢入边会阻塞整个算子;所以高吞吐场景常用非对齐快照(代价是快照更大)。
实测:快照里的 “消息总数” 必须对得上
本机 Python 3.14 模拟(3 进程、FIFO 通道、5 条消息、P0 发 5 条后 P1 消费 2 条时发起快照):
| 量 | 值 |
|---|---|
| 快照内进程状态 | [0, 2, 0](P1 已处理 2 条) |
| 快照内通道在途 | [[1,1,1]](P0→P1 还剩 3 条) |
| 快照内消息总数 | 5 |
| 全局已发消息数 | 5 |
断言:状态之和 + 在途消息数 == 全局已发数。若漏记通道状态,快照总数会是 2——恢复后就丢了 3 条消息。这就是 “只记本地状态一定不一致” 的实证。
# 收到第一个 marker:记录本地状态 + 把“此后收到的消息”记进通道状态
if not recorded and first_marker(in_edge):
snapshot.state = copy(state); recorded = True
for e in out_edges: send(e, MARKER)
elif recorded and marker(in_edge):
snapshot.channels[(in_edge, self)] = buffer[in_edge] # 通道状态
反模式
| 反模式 | 后果 |
|---|---|
| 在非 FIFO 通道上用 marker | 快照可能包含 “晚于 marker 的消息”,不一致 |
| 快照期间做状态变更却不纳入 | 恢复后状态错 |
| 快照阻塞业务线程 | 抖动,吞吐下降 |
| 只存快照不存日志 | 无法做 “从快照 + 增量” 的精确恢复 |
三条铁律
🔑 快照 = 本地状态 + 在途消息;只记本地状态一定不一致。
🔑 前提是可靠 FIFO 通道;否则 marker 语义失效。
🔑 快照要配增量日志,才能恢复到任意点。
自检
Q1 为什么 Chandy-Lamport 的快照可能不对应 “某一真实时刻”?
▸ 答案:因为各进程记录状态的时间点不同(收到 marker 的先后不同),中间有消息在飞。快照拼出来的是 “一个可能的全局状态”——它满足一致性(消息不丢不重),但不等于 “某个物理瞬间的所有节点状态”。
Q2 通道状态为什么要单独记录?
▸ 答案:因为 “A 记录了状态之后发出、B 还没记录状态之前收到” 的消息,属于跨快照边界的消息:不记录它,恢复时就会丢;记录它,才能保证 “发出去的都在快照里”。
Q3 “对齐” 为什么会造成反压?
▸ 答案:因为算子必须等所有入边的 barrier 到齐才能做快照;慢入边没到,快算子就要暂停处理并缓冲数据,反压沿链路向上传播。非对齐快照把这些在途数据直接写进快照,避免等待,但快照更大、恢复更慢。
Q4 这个算法能用于 “备份数据库” 吗?
▸ 答案:思路可以借鉴(一致性快照),但数据库有自己的 WAL/checkpoint 机制,直接用 CL 算法没有意义。它的真正用武之地是流处理/分布式计算的状态快照,以及理解 “为什么 marker 能拼出全局一致状态”。
Q5 如果某个进程在快照过程中崩溃了怎么办?
▸ 答案:本轮快照作废(不完整),需要在恢复后重新发起。工程实现(如 Flink)会把快照与 checkpoint 协调器绑定:任一节点失败即整体回滚到上一个成功的 checkpoint,而不是 “修补半个快照”。
Q7 为什么实验中 “状态之和 + 在途” 恰好等于全局已发,而不是 “某一时刻的真实值”?
▸ 答案:因为它对应的确实是 “P0 发完 5 条、P1 处理了 2 条” 的那个时刻——在单通道、顺序执行的例子里,快照正好落在一个真实点上。只有在多进程并发、消息在多个通道同时飞时,快照才可能拼出 “不对应任何真实时刻” 的一致状态;本例只是验证 “账要算平”。