本文档记录一次节点同步问题的排查过程,解释问题根因、推导过程与最终修复方案,并用字符画描述数据流的变化。
在 proxy-tui 中执行 删除节点 操作后,刷新列表发现节点仍然存在。初步判断为“删除未生效”,但实际上服务器返回了成功。
- 使用
proxy-tui进入 Nodes 选项卡。 - 选中某个非本机节点,按
d删除。 - 刷新或等待一段时间(自动刷新)。
- 节点重新出现在列表里。
- 服务端
RemoveNode控制指令返回成功。 - 删除动作触发保存
nodes.json。 - 说明删除请求在 本地节点 已处理。
继续查看节点同步逻辑,发现存在“定期全量广播”:
- 每个节点在固定间隔(默认 30s)向所有已知节点发送 全量节点列表。
- 接收方无条件合并该列表。
这意味着:
- 节点 A 删了 X,但节点 B 仍保存 X。
- 下一轮 B 的“全量同步”把 X 再发回来。
- A 收到后又把 X 加回。
问题不是“删除失败”,而是 同步机制把已删除节点重新同步回来了。
┌────────┐ 每 30s 全量 SyncNodes ┌────────┐
│ Node A │ ───────────────────────────────────▶ │ Node B │
└───┬────┘ └───┬────┘
│ Remove X │
│ (本地删除成功) │ 仍持有 X
▼ ▼
┌────────┐ 每 30s 全量 SyncNodes ┌────────┐
│ Node A │ ◀─────────────────────────────────── │ Node B │
└────────┘ └────────┘
↑
└─ X 被重新加回(删除被覆盖)
结论:周期性全量同步 + 无删除墓碑 ⇒ 删除无法收敛。
- 删除必须有“墓碑/tombstone”传播,否则会被其它节点的旧数据覆盖。
- 不能让每个节点被动定期全量同步。同步应由 变更方主动触发,否则会引入重复数据和回滚。
- 删除信息必须持久化,以防重启后再次同步回来。
彻底移除周期性定时同步,仅在 增删改 发生时广播。
Remove/Add/Update 触发 ──▶ 主动广播 SyncNodes
没有变更 ──▶ 不再同步
新增 blocked 列表,所有删除操作将写入此列表并持久化:
nodes.json:
{
"peers": [...],
"blocked": ["node-x", "node-y"]
}
接收 SyncNodes 时先合并 blocked,再合并 peers,确保 delete wins。
删除节点时会自动从所有分组中移除,防止残留无效引用。
┌────────┐ Change-Driven SyncNodes(含 blocked) ┌────────┐
│ Node A │ ─────────────────────────────────────────▶ │ Node B │
└───┬────┘ └───┬────┘
│ Remove X │
│ blocked += X │
▼ ▼
┌────────┐ Change-Driven SyncNodes(含 blocked) ┌────────┐
│ Node A │ ◀───────────────────────────────────────── │ Node B │
└────────┘ └────────┘
↑
└─ X 被阻止列表拦截,删除不会被覆盖
- 删除节点后不会再“回弹”。
- 节点同步仅在发生变更时广播,减少噪声和误同步。
- 删除语义在集群中可收敛(delete wins)。
Delta 看似更轻量,但在当前场景中会引入“新节点无法完整收敛”的风险:
-
新节点加入的首次同步
- 若仅发送增量,接收方需要依赖“历史全量状态”才能合并出完整视图。
- 一旦接收方错过某次增量(网络抖动、节点重启),就会产生永久缺失。
-
并发变化
- 多个节点同时发生变更时,增量合并需要可靠的版本与因果顺序。
- 否则可能出现“新增被删除覆盖”或“删除被新增复活”的错误。
-
节点重启/恢复
- 新启动的节点如果只收到后续增量,无法复原集群完整视图。
因此我们选择 变更触发的全量同步:
- 仍然避免了定期全量同步带来的噪声
- 但可以确保任何一次同步都能恢复完整状态
- 再配合
blockedtombstone,删除也不会回滚
当前同步仍是“变更时的全量列表”,若你希望进一步优化为 只同步 delta(add/remove),可以扩展 ControlOp:
SyncNodesDelta { add: [...], remove: [...], blocked: [...] }
这样可以降低 payload 并简化冲突合并逻辑。需要的话我可以继续实现。