Skip to content

Latest commit

 

History

History
165 lines (110 loc) · 5.81 KB

node-sync-incident.md

File metadata and controls

165 lines (110 loc) · 5.81 KB

Node 同步问题复盘与修复说明

本文档记录一次节点同步问题的排查过程,解释问题根因、推导过程与最终修复方案,并用字符画描述数据流的变化。


背景

在 proxy-tui 中执行 删除节点 操作后,刷新列表发现节点仍然存在。初步判断为“删除未生效”,但实际上服务器返回了成功。


现象与复现步骤

  1. 使用 proxy-tui 进入 Nodes 选项卡。
  2. 选中某个非本机节点,按 d 删除。
  3. 刷新或等待一段时间(自动刷新)。
  4. 节点重新出现在列表里。

排查过程(逐步推导)

1) 验证删除是否生效

  • 服务端 RemoveNode 控制指令返回成功。
  • 删除动作触发保存 nodes.json。
  • 说明删除请求在 本地节点 已处理。

2) 观察同步机制

继续查看节点同步逻辑,发现存在“定期全量广播”:

  • 每个节点在固定间隔(默认 30s)向所有已知节点发送 全量节点列表。
  • 接收方无条件合并该列表。

这意味着:

  • 节点 A 删了 X,但节点 B 仍保存 X。
  • 下一轮 B 的“全量同步”把 X 再发回来。
  • A 收到后又把 X 加回。

3) 结论

问题不是“删除失败”,而是 同步机制把已删除节点重新同步回来了。


原始数据流(问题版)

┌────────┐        每 30s 全量 SyncNodes         ┌────────┐
│ Node A │ ───────────────────────────────────▶ │ Node B │
└───┬────┘                                      └───┬────┘
    │  Remove X                                     │
    │  (本地删除成功)                               │  仍持有 X
    ▼                                               ▼
┌────────┐        每 30s 全量 SyncNodes         ┌────────┐
│ Node A │ ◀─────────────────────────────────── │ Node B │
└────────┘                                      └────────┘
     ↑
     └─ X 被重新加回(删除被覆盖)

结论:周期性全量同步 + 无删除墓碑 ⇒ 删除无法收敛。


修复思路

  1. 删除必须有“墓碑/tombstone”传播,否则会被其它节点的旧数据覆盖。
  2. 不能让每个节点被动定期全量同步。同步应由 变更方主动触发,否则会引入重复数据和回滚。
  3. 删除信息必须持久化,以防重启后再次同步回来。

修复方案

1) 改为变更驱动(Change‑Driven)

彻底移除周期性定时同步,仅在 增删改 发生时广播。

Remove/Add/Update 触发 ──▶ 主动广播 SyncNodes
没有变更             ──▶ 不再同步

2) 增加阻止列表(删除墓碑)

新增 blocked 列表,所有删除操作将写入此列表并持久化:

nodes.json:
{
  "peers": [...],
  "blocked": ["node-x", "node-y"]
}

接收 SyncNodes 时先合并 blocked,再合并 peers,确保 delete wins。

3) 删除时清理组引用

删除节点时会自动从所有分组中移除,防止残留无效引用。


修复后数据流(正确版)

┌────────┐     Change-Driven SyncNodes(含 blocked)     ┌────────┐
│ Node A │ ─────────────────────────────────────────▶  │ Node B │
└───┬────┘                                             └───┬────┘
    │ Remove X                                              │
    │ blocked += X                                          │
    ▼                                                      ▼
┌────────┐     Change-Driven SyncNodes(含 blocked)     ┌────────┐
│ Node A │ ◀─────────────────────────────────────────  │ Node B │
└────────┘                                             └────────┘
     ↑
     └─ X 被阻止列表拦截,删除不会被覆盖

最终效果

  • 删除节点后不会再“回弹”。
  • 节点同步仅在发生变更时广播,减少噪声和误同步。
  • 删除语义在集群中可收敛(delete wins)。

为什么不采用 Delta(只同步变更)?

Delta 看似更轻量,但在当前场景中会引入“新节点无法完整收敛”的风险:

  1. 新节点加入的首次同步

    • 若仅发送增量,接收方需要依赖“历史全量状态”才能合并出完整视图。
    • 一旦接收方错过某次增量(网络抖动、节点重启),就会产生永久缺失。
  2. 并发变化

    • 多个节点同时发生变更时,增量合并需要可靠的版本与因果顺序。
    • 否则可能出现“新增被删除覆盖”或“删除被新增复活”的错误。
  3. 节点重启/恢复

    • 新启动的节点如果只收到后续增量,无法复原集群完整视图。

因此我们选择 变更触发的全量同步:

  • 仍然避免了定期全量同步带来的噪声
  • 但可以确保任何一次同步都能恢复完整状态
  • 再配合 blocked tombstone,删除也不会回滚

备注与扩展

当前同步仍是“变更时的全量列表”,若你希望进一步优化为 只同步 delta(add/remove),可以扩展 ControlOp:

SyncNodesDelta { add: [...], remove: [...], blocked: [...] }

这样可以降低 payload 并简化冲突合并逻辑。需要的话我可以继续实现。