Spacedrive 同步日志架构解析:基于每设备 HLC 的 sync.db 设计与实现(LSYNC-008) 📅 发布时间:2026/9/19 19:20:04 👁 浏览次数: Spacedrive 同步日志架构解析基于每设备 HLC 的 sync.db 设计与实现LSYNC-008【免费下载链接】spacedriveSpacedrive is an open source cross-platform file explorer, powered by a virtual distributed filesystem written in Rust.项目地址: https://gitcode.com/gh_mirrors/sp/spacedrive本文以 Spacedrive 仓库中的同步任务设计文档 LSYNC-008-sync-log-schema.md 为核心结合 peer_log.rs 与 hlc.rs 的实际源码实现系统讲解新一代同步日志sync.db的表结构、Hybrid Logical ClockHLC排序机制、ACK 驱动的裁剪pruning策略以及从旧的领导者中心化 sync_log.db 到无领导架构的迁移路径。读完本文你将掌握如何在无中心协调节点的多设备环境下用一个始终保持在百条级别的小型日志完成共享资源标签、相册变更的可靠传播与回收。一、为什么需要独立的 sync.db从中心化日志到每设备日志在旧架构中Spacedrive 使用一个由领导者leader设备独占的sync_log.db来记录所有同步变更。这种设计的核心矛盾在于一旦拥有日志的设备离线其他设备便无法确认变更的全局顺序且该日志记录全部历史、永不裁剪规模会随使用时长无限膨胀。LSYNC-008 提出的架构变更正是为了解决这一问题将中心化的单一日志替换为每设备独立维护的sync.db。新旧设计的核心差异如下表所示原设计文档原文方面旧设计sync_log.db新设计sync.db谁持有仅领导者每台设备记录内容所有变更仅我产生的共享变更排序方式序列号sequence numbersHLC 时间戳体积大保存全部历史小激进裁剪用途事实来源source of truth待发送变更队列pending changes queue这一转变的本质是角色重定义sync.db不再扮演全局事实来源该角色由每库的主database.db承担而是扮演本设备尚未被所有对端确认消费的变更暂存队列。由于每台设备只记录自己的变更日志天然避免了多写者冲突由于可以被裁剪日志永远保持在一个很小的规模。二、排序基石HLC混合逻辑时钟要支撑无领导者、离线可用、全局可排序的日志shared_changes表的主键不能再用中心分配的序列号而必须是一种可以由任意设备独立生成、又保证全序total order的时间戳——这正是 HLC。2.1 HLC 的数据结构在 hlc.rs 中HLC由三个字段构成hlc.rs 第 19-29 行pub struct HLC { /// 物理时间组件Unix 纪元以来的毫秒数 pub timestamp: u64, /// 同一毫秒内事件的逻辑计数器 pub counter: u64, /// 生成该 HLC 的设备 ID用于确定性排序 pub device_id: Uuid, }其关键性质在任务文档 LSYNC-009-hlc-implementation.md 中被总结为四点全序任意两个 HLC 都可比较大小因果性若事件 A 因果先于 B则 HLC(A) HLC(B)分布式生成生成过程无需任何协调天然支持离线紧凑高效时间戳 计数器 设备 ID足以支撑排序与冲突判定。在Ord实现中hlc.rs 第 136-143 行比较优先级依次为timestamp→counter→device_iddevice_id 作为最后的确定性 tie-breaker保证任意两个 HLC 永远可比、绝不相等。2.2 可排序字符串表示shared_changes.hlc列存储的是 HLC 的字符串形式其格式定义在 hlc.rs 第 95-104 行{:016x}-{:016x}-{device_id}即timestamp与counter各以 16 位十六进制零填充再拼接设备 UUID。这种格式是字典序可排序的SQL 中直接使用hlc ?、ORDER BY hlc ASC、MAX(hlc)等字符串操作即可获得正确的 HLC 语义排序无需在数据库层解析结构体。解析时通过splitn(3, -)只按前两个连字符切分避免 UUID 内部连字符干扰hlc.rs 第 106-132 行。2.3 生成器与因果更新设备侧的HLCGeneratorhlc.rs 第 177-226 行维护last_hlc并对外暴露两个方法next()生成下一个 HLC——若与上一次同属一个毫秒则counter 1否则重置 counter 为 0HLC::generate见 hlc.rs 第 45-66 行update(received)收到对端 HLC 时更新本地时钟实现因果追踪——取本地、远端、物理时钟三者的最大值同毫秒则取 max counter 1HLC::update见 hlc.rs 第 74-93 行。生成器内部使用MutexOptionHLC保证线程安全并注入可测试的TimeSource抽象FakeTimeSource/SystemTimeSource由 mod.rs 统一导出。注意物理时钟回拨时HLC 依靠与上次生成值取最大值的规则天然免疫倒退这正是它在弱同步时钟环境下依然可靠的原因。三、sync.db 表结构详解3.1 设计文档中的 SchemaLSYNC-008 设计文档给出的目标 Schema 如下-- 我对共享资源的变更 CREATE TABLE shared_changes ( hlc TEXT PRIMARY KEY, -- 混合逻辑时钟可排序字符串 model_type TEXT NOT NULL, -- tag、album、user_metadata record_uuid TEXT NOT NULL, -- 被变更记录的 UUID change_type TEXT NOT NULL, -- insert、update、delete data TEXT NOT NULL, -- JSON 负载 created_at TEXT NOT NULL, ); CREATE INDEX idx_shared_changes_hlc ON shared_changes(hlc); CREATE INDEX idx_shared_changes_model ON shared_changes(model_type); CREATE INDEX idx_shared_changes_record ON shared_changes(record_uuid); -- 追踪各对端已确认到哪个 HLC用于裁剪 CREATE TABLE peer_acks ( peer_device_id TEXT NOT NULL, last_acked_hlc TEXT NOT NULL, acked_at TEXT NOT NULL, PRIMARY KEY (peer_device_id) ); CREATE INDEX idx_peer_acks_hlc ON peer_acks(last_acked_hlc);三个字段的设计要点hlc直接作为主键HLC 的唯一性与可排序性使其天然适合充当变更去重与增量拉取的游标model_type白名单化仅共享资源标签、相册、用户元数据进入该日志普通条目entry的变更仍走主库状态同步从源头控制了日志规模data为 JSON 负载完整的变更内容随行存储接收方无需回查发送方主库即可应用变更。3.2 源码中的实际建表设计文档规划创建 SeaORM entities migration而当前仓库的实际落地选择了在运行时以原生 SQL 建表的方式PeerLog::create_tablespeer_log.rs 第 50-182 行在打开数据库后依次执行CREATE TABLE IF NOT EXISTS与CREATE INDEX IF NOT EXISTS去掉了设计稿中created_at后的尾随逗号并额外创建了三张辅助表device_resource_watermarks按资源维度记录本设备的同步水位线由ResourceWatermarkStore管理watermarks.rspeer_received_watermarks记录各对端在共享资源增量同步中的已接收水位peer_watermarks.rsbackfill_checkpoints支持可恢复的后向回填backfill的检查点持久化checkpoints.rs。此外还创建了sync_event_log表及其 5 个查询索引timestamp、device_id、event_type、correlation_id、peer_device_id用于持久化同步事件日志与可观测性详见 LSYNC-022-sync-metrics-and-observability.md 相关基础设施。需要区分的是主库database.db中的 sync 相关迁移如 m20251015_000002_create_sync_tables.rs 创建的sync_conduit/sync_generation表属于文件同步conduit体系与本文讨论的共享变更日志是两套不同的存储。3.3 数据库文件位置每个库library目录下同时存在两个 SQLite 文件设计文档中的目录示意Jamies Library.sdlibrary/ ├── database.db ← 共享状态所有设备 └── sync.db ← 我的待发送共享变更可裁剪源码中PeerLog::openpeer_log.rs 第 27-47 行以sqlite://{library_path}/sync.db?moderwc的方式打开rwc表示不存在即创建并以library_id、device_id作为构造参数所有写操作自动携带本设备身份。四、SyncDb / PeerLog 封装剖析设计文档中的SyncDb结构体在实现中以PeerLog命名落地peer_log.rs 第 19-23 行字段与文档一致library_id、device_id、conn。下文对照文档 API 逐一看实现。4.1 append追加变更pub async fn append(self, entry: SharedChangeEntry) - Result(), PeerLogError对应实现 peer_log.rs 第 185-212 行将SharedChangeEntryhlc、model_type、record_uuid、change_type、data序列化后以参数化 SQL 写入shared_changes。change_type对应ChangeType枚举的三种取值Insert/Update/Delete与 schema 中的insert/update/delete字符串互转peer_log.rs 第 473-500 行。4.2 get_since按 HLC 增量拉取pub async fn get_since(self, since: OptionHLC, limit: Optionusize) - ResultVecSharedChangeEntry, PeerLogError对应实现 peer_log.rs 第 219-294 行。与设计文档的签名相比实现额外支持limit参数当提供Some(limit)时把LIMIT下推到 SQL发送方sender在只需要一个批次时不会把整个日志物化到内存None则返回全部行测试与批量维护场景使用。查询统一WHERE hlc ? ORDER BY hlc ASC以字典序直接利用 HLC 字符串的可排序性。实现还做了防御性处理usize转i64时若溢出则钳制到i64::MAX避免极端的 limit 被绑定成负数peer_log.rs 第 226 行。4.3 record_ack记录对端确认pub async fn record_ack(self, peer_id: Uuid, up_to_hlc: HLC) - Result(), PeerLogError对应实现 peer_log.rs 第 327-344 行以INSERT OR REPLACE按peer_device_id主键覆盖写入记录该对端最新确认到的 HLC 与确认时间RFC3339。对端确认即对端已成功接收并应用了 up_to_hlc的全部变更这是后续裁剪的唯一依据。4.4 prune_acked裁剪已确认条目设计文档给出了精简版实现源码实现与之完全同构peer_log.rs 第 381-401 行pub async fn prune_acked(self) - Resultusize, PeerLogError { let min_hlc self.get_min_acked_hlc().await?; match min_hlc { Some(hlc) { let hlc_str hlc.to_string(); let result self .conn .execute(Statement::from_sql_and_values( DbBackend::Sqlite, DELETE FROM shared_changes WHERE hlc ?, vec![hlc_str.into()], )) .await .map_err(|e| PeerLogError::QueryError(e.to_string()))?; Ok(result.rows_affected() as usize) } None Ok(0), } }核心思想是木桶原理先求所有对端已确认 HLC 的最小值min_ackedget_min_acked_hlc第 351-378 行凡是hlc min_acked的条目说明每个对端都已消费可以安全删除。若peer_acks为空尚无任何对端确认则返回 0 行也不误删。一个值得注意的实现细节get_min_acked_hlc的查询带有WHERE peer_device_id ! ?当前设备 ID过滤peer_log.rs 第 356 行。注释说明这是防御性设计——正常流程中不存在自己确认自己的 ACK但一旦因异常写入产生 stale self-ACK若不加过滤会拉低最小值、导致未送达的变更被提前裁剪。4.5 辅助查询方法PeerLog还提供若干面向同步流程的查询能力get_max_hlc()第 300-324 行取MAX(hlc)即本设备当前的共享水位线shared watermark用于增量同步的基准get_latest_hlc_for_record(record_uuid)第 429-454 行按记录 UUID 取最近一次变更的 HLC供冲突解决时对比已见过的变更count()第 404-423 行统计日志条目数配合监控验证日志保持小型的验收标准。五、ACK 驱动裁剪的运行流程设计文档给出了收到 ACK 后的处理逻辑原样摘录// 收到对端 ACK 之后 async fn on_ack(peer_id: Uuid, up_to_hlc: HLC) { // 记录 ACK sync_db.record_ack(peer_id, up_to_hlc).await?; // 尝试裁剪 let pruned sync_db.prune_acked().await?; if pruned 0 { info!(pruned, Pruned shared changes log); } }该流程在服务层有对应的真实调用点对端确认与裁剪在 peer.rs 中分别通过peer_log.record_ack(peer_id, up_to_hlc)约第 2529-2530 行与peer_log.prune_acked()约第 2542-2543 行执行mod.rs 的周期维护任务也会调用prune_acked()并记录peer_log_pruned指标第 643-649 行。另外从 sync/peer.rs 第 746-760 行 的调用上下文可以看出裁剪发生在收到对端同步消息并更新其对端水位之后与设计流程一致。运行效果由于每条变更一旦被所有对端确认即被删除日志中只保留至少一个对端尚未确认的在途变更设计文档给出的结论是即使在活跃使用下日志通常也保持在 100 条以内验收标准放宽为 1000 条。裁剪行为还受统一配置约束。config.rs 中的RetentionConfig提供两档默认值快速active模式PruningStrategy::AcknowledgmentBasedpeer_log_max_retention_days: 3force_full_sync_threshold_days: 2保守模式PruningStrategy::Conservative { min_retention_days: 7 }peer_log_max_retention_days: 30force_full_sync_threshold_days: 25。可见除了全对端确认即删这一主策略外系统还引入了保留天数下限与全量同步阈值防止极端情况下如对端长期离线日志无限膨胀。六、正确性验证源码级测试仓库自带两套针对性测试可作为本设计的行为规范PeerLog 测试peer_log.rs 第 518-591 行test_append_and_retrieve写入一条 tag 变更后get_since(None, None)能完整读回验证序列化往返test_ack_and_prune写入 3 条变更对端 A 确认前 2 条、对端 B 确认全部 3 条后执行prune_acked()断言恰好删除 2 条、剩余 1 条——精确刻画了取所有对端最小确认 HLC 为裁剪线的语义。HLC 测试hlc.rs 第 235-397 行test_hlc_generation/test_hlc_generator同毫秒内 counter 递增、跨毫秒 counter 归零test_hlc_ordering同一毫秒内按 counter、跨毫秒按 timestamp 严格全序test_hlc_update_causality本地时钟落后时采纳远端时间戳并counter 1物理时钟超前时重置 countertest_generator_causality_tracking设备 B 收到设备 A 的 HLC 后再生成事件保证 B 的事件因果晚于 Ahlc_b hlc_atest_hlc_string_roundtrip字符串序列化与反序列化往返一致。七、从 sync_log.db 迁移与验收清单7.1 迁移对照维度旧结构新结构文件领导者独占一个sync_log.db每设备一个sync.db排序中心序列号HLC裁剪从不裁剪激进裁剪全对端确认即删迁移还涉及配套变更详见 LSYNC-009-hlc-implementation.md 的 Migration 一节移除devices表中的sync_leadership字段、LeadershipManager结构体与所有is_leader()检查新增HLC类型、HLCGenerator挂在 SyncService 内与shared_changes表中的 HLC 列。7.2 验收标准设计文档原文每库创建sync.db迁移已创建并测试SeaORM 实体/存储封装已实现实际落地为PeerLog原生 SQL 建表 类型化读写基于 HLC 的索引PeerLogSyncDb封装可用裁剪逻辑可用正常使用下日志保持 1000 条文档完备八、相关资源任务文档LSYNC-008-sync-log-schema.mdHLC 任务文档LSYNC-009-hlc-implementation.md库同步总纲LSYNC-000-library-sync.md核心实现peer_log.rs、hlc.rs基础设施导出与配置mod.rs、config.rs服务层调用mod.rs、peer.rs一句话总结这套设计的精妙之处用每设备自产自销的小型 HLC 日志替换全局唯一的序列号日志用全对端最小确认线做裁剪从而在零协调、可离线的前提下同时解决了排序、去重与体积三个问题——这正是 Spacedrive 无领导leaderless同步架构能够落地的数据基石。【免费下载链接】spacedriveSpacedrive is an open source cross-platform file explorer, powered by a virtual distributed filesystem written in Rust.项目地址: https://gitcode.com/gh_mirrors/sp/spacedrive创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考