思源的同步悖论

🙏

大家好,我是较长时间没有出现的 muhanstudio,今天来和大家唠唠一个老大难的问题:

  • 思源笔记的同步

思源同步的现状

思源的同步,无论是性能还是稳定性,已经被诟病了很久,尽管相比之前有了很大的进步,但是如果我们不区别对待,把它和市面上的云笔记软件相比,以下三大雷区基本上踩了一个遍。

  1. 冲突合并约等于没有
  2. 同步进行非常缓慢
  3. 会使当前编辑丢失

对同步速度的看法

我曾经持有一个粗暴的观点,认为只要同步的够快,用户根本来不及进行什么骚操作,也不会产生很多的冲突,反而是磨磨蹭蹭、畏手畏脚的走独木桥更容易摔下去。

插件开发尝试

同样,我也曾为之做过一点改变,连续开发了两款插件,一个是同步感知插件,一个是加速同步插件,他们的作用一个是尝试模拟官方的效果,在不同设备之间尝试寻找一个沟通点,在其中一个设备更改内容同步后,立即通知其他设备发起同步,一个是为了解除官方自动同步最快要等 30 秒的限制,只要你愿意,可以将同步操作的前置等待时间在保证正常顺序的同时加速到极限,但是,效果远远没有达到我的预期,主要是因为,整个机制依旧建立在思源的同步上,我只是加速了通知每个终端思源开始同步这个过程,而以上的问题,依旧没有得到很好的改善,这让我曾经郁闷过很长一段时间。

同步机制的妥协

但是随着后来深入分析思源的同步机制,会发现,其实思源因为纯本地化,在数据同步上做了很大的妥协。本地化存储(localstore)和自伺服(selfhosted)下的数据私有化完全不是一个概念,云笔记也可以是自己的云,但是本地化笔记就真的只是本地而已。

冲突合并与自伺服系统的差异

体现在什么地方?尤其在冲突合并上,对于自伺服系统,你可以完全和云笔记一样,拥有一个 24h 活跃的数据中心,这个数据中心可以作为唯一的参照基准,对于多个客户端在一个广播周期内发来的数据,数据中心可以自己处理好冲突取舍与合并,合并机制并不是最重要的,重要的是主动广播权和并发接受多版本数据的能力,数据中心拥有最高话语权,可以向所有设备在一个同步响应周期内广播相同的数据,哪怕合并机制做的简单粗暴,最终多端数据依旧是一致性的。数据中心还可以存储多个版本的数据,而不只是单线程的 IO 处理,如果想要回撤,由于数据中心存储了每个客户端的响应,同样可以在所有的版本之间做选择。

思源同步的工作方式

而思源的同步是怎么做的呢?或者说,只能怎么做?作为一个本质上完全本地化的软件,它目前是依赖于一个静态的储存设备,而这种情况下,目前最好的选择是对象存储,我下面会采用一些比喻来避免我的描述变成技术名词大集合,可能有一点不恰当,但是尽量贴切的来描述对象存储在思源同步中的角色。

并发性与一致性问题

每个客户端是一个独立的人,他们互相之间没有通道可以沟通(官方订阅也貌似做了设备显示,可设备之间的沟通也约等于没有),但是都可以走进一个静态的仓库里面,仓库里面本身没有人,只有数据,且没有主动广播消息的能力,仓库一次只能进一个人,进去后可以复制仓库的数据,或者修改仓库里的数据,在这种情况下,要保持所有人的数据都是一样的,光是逻辑上的问题就很多:

  1. 并发性与一致性问题
  2. 数据同步滞后
  3. 缺乏事务控制

并发性与一致性问题

由于每个客户端进入仓库后可以读取或修改数据,但其他客户端在它们进入仓库之前无法得知最新的修改结果,这导致数据的一致性无法得到保障。例如,客户端 A 进入仓库,读取并修改了数据,但此时客户端 B 仍然读取了旧数据,因为可能 A 同样可以在自己同步的过程中持续修改本地数据,或在 A 写入新数据之前,B 已经读取并打算写入自己的修改。这样就可能发生数据的不一致。 即使通过某种机制保证一次只能有一个人进入仓库,也无法确保在下一个人进入仓库前,其他人始终读取到最新的数据,特别是在修改数据的过程中会产生暂时不一致的状态。

数据同步滞后

假设在进入仓库时可以成功锁定资源并避免并发访问,虽然仓库中的数据在每次写入后是最新的,但客户端获取到数据和执行修改之间存在时间差。在这个时间差中,其他客户端可能会基于旧数据做出决策,而不是基于仓库里的最新数据。 如果不设计同步机制(例如读写操作中加入时间戳或版本控制),数据滞后将会导致多个客户端的数据版本不一致。没有全局同步机制,无法确保每次更新的数据能被所有客户端即时获取。

缺乏事务控制

事务控制机制通常用于保证并发环境下的数据一致性,而这个系统缺乏事务控制。假设客户端 A 在仓库中读取数据并进行长时间操作,此时如果没有事务锁定,其他客户端无法获得数据是否已被最终写入或更新的信息。由于缺乏事务回滚与提交机制,任何错误或中断都可能导致仓库中数据的不一致。 数据一致性要求所有客户端在每次操作结束后都能立即看到相同的数据版本,但是这个场景没有实现任何事务处理或两阶段提交协议等方法来管理这一点。

自伺服系统的优势

而换到自伺服系统中,仓库并不再是类似一个静态磁盘一样的地方,而是一个运行中的数据库系统,可以同时处理所有客户端的请求,并且可以缓存下来同一时间不同客户端的修改操作提交,即使它合并冲突或者同步请求的策略非常粗暴,由于最终可以向所有客户端同时广播一份一样的数据,使得它的同步性能依旧要好得多。因为自伺服系统具备并发访问、事务管理、缓存合并和实时数据广播的能力,可以在处理并发请求时确保最终数据一致性,从而避免数据滞后问题。 这也解释了,为什么对象存储已经是目前世界上广泛应用的最高效的网络数据存储,在思源上依旧显得只是不限带宽的同步网盘而已,在我部署过的无数项目中,例如 anytype,affine,appflowy 等等等等,他们同样使用了对象存储作为自己的后端存储,但他们并不是让客户端直接操作对象存储,翻看他们的 docker compose 文件,他们都有刚才我提到的类似于数据中心的实例,会单独起一个服务,在客户端与对象储存之间作为 24h 值班人员,来处理各个客户端的并发请求和一致性广播,并将一致性数据在一个内网或者同域的安全稳定的环境中传入对象存储(除非你使用了分布式部署对象存储)。

思源的同步悖论

说到这里,思源作为一款本地化软件,貌似被披上了一层悖论,永远无法做到优秀的同步性能,那么本地化和 自伺服/拥有数据中心 的同步方式是对立的关系吗,从来都不是,即使拥有以上同步机制,我们依旧可以让客户端默认总是确保所有的完整的数据都被拉取到本地,当失去了云端,也就是数据中心后,我们仅仅失去了我们的云同步功能,但是我们的数据依旧是完整的在本地的。 作为官方订阅的同步方式,思源至今还是主要采用本地化加上静态存储设备的方式来进行同步。社区版的思源其实已经提供了很大的启示,可以直接连接到 docker 部署的思源实例,可以作为一个数据中心来实现以上的操作,但由于是社区二开,稳定性和通用性都存在非常多的问题,无法真正投入使用。之前我还在群里讨论过云内核的实现方法,思路也是类似的,为每一个官方订阅用户提供一个 24 小时运行的云端内核,可以和每个客户端建立长连接,来确保高效的数据传输和数据一致性。同样还设计过一个简单的后端请求转发器,由于思源前后端采用 HTTP 通信, 我尝试拦截所有的后端请求然后转发到另一个客户端上,效果居然意外的不错,可惜最终也是因为必须所有的客户端都全程在线,或者需要做好请求的长时间缓存导致技术难度陡增而放弃。

与成本控制和解

到最后我还是发现,我对思源要求还是太苛刻了,我想我的不满主要来源于官方订阅还提供低效的同步,虽然所有数据还是留存在本地,但是我相信很多人买官方订阅,而不选择功能特性里的 S3 同步,其实有很多一部分都是抱着市面上成熟的云笔记的心态去用,自己看不到自己的数据究竟被放在了哪里,通过什么样的方式进行传输和管理,只知道官方负责我数据的多端一致性,我买的是官方同步,由思源笔记的服务器来负担,理应比自己买对象存储要安全和稳定很多,如果当初的心态只是把官方订阅当做是一个小容量的不计流量包年对象存储,不用自己填相关的配置,一个官方提供的静态备份服务,在思源笔记已经如此开放,营收手段如此少,开发资源也紧张,不得不考虑成本控制的情况下,其实也就还好。可惜这样做对于开发者自己也是有代价的,因为如果想解决,甚至只是优化开头的那几个问题,随之而来要解决的逻辑问题就会几何倍数增加且越发复杂,即使将来尝试引入一些无中心的方案,比如 webrtc 和 P2P,在目前的技术结构上,例如在基于文档/文件级别的粒度上同步,坑也会增加,anytype 对于冲突块的解决方式是自动在两个客户端换行生成新的块来显式全量存储两个冲突块,而思源目前的快照机制,还需要大量的重构才能达到相应的效果,同时,单单它的同步核心 any-sync(包含非常多的子部件)都由两个以上的人员开发,我想它也用自己的复杂度和工程量之大亲身实践告诉我们,点对点方案只适合作为一个新鲜的特性来引入,作为成本控制,也是不适合作为中心化的廉价替代方案的。

展望与建议

这一章其实是 AI 帮我分章节的时候生成的,我个人很难给出什么具体的或者专业的建议(除了加钱),只能在已经做好的成熟的数据一致性系统之间进行肤浅的对比和解读,写这篇文章主要也是让更多的人了解并且理解在很多场景下思源一直以来的同步困境,很多时候,在不断的需求受限中,现实的与成本控制和解,才是解决矛盾的折中办法。

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    22384 引用 • 89626 回帖
1 操作
muhanstudio 在 2024-10-16 10:54:49 更新了该帖

相关帖子

优质回帖
  • 我再插一句,实际上并不只是多端修改同一个文件会导致这样的问题,我亲身测试,甲设备修改 A 文档,乙设备修改 B 文档,然后甲点击同步,甲同步完成之后,乙点击同步,如果乙没有同步完成,甲上线开始打开 B 文档,开始编辑修改,同时乙同步完成了,触发甲的同步,思源目前会直接拿乙修改的 B 覆盖甲的 B,导致编辑内容丢失,且是瞬间发生的,不会有数据快照,如果乙同步完了,甲上线开始打开 B 文档,开始编辑修改,然后点击同步,会直接触发冲突,可怕的是,我们甚至没有在同一时间编辑同一个文档,甚至是在编辑完成之后点击的同步

    这是其中一个悖论,也就是文章中提到的一个本质上无关技术的逻辑问题,而要解决这个逻辑问题,你需要更加复杂的技术去勉强解决

  • zxhd86 1 赞同

    你可以认为是原理上没有问题,实现上有问题。但是事实上来说,因为平台的差异性和边界条件导致的实现有问题,这永远是难以完整考虑的。你说完善,至少我所提到的这几个问题都有在进行完善,比如说在开启的时候先给你设置为只读状态,时间戳只保留到秒,而不是毫秒,这几个都是我看着跟进的。而其他在我不清楚的情况下进行完善的实现更多,那到现在都已经完善了两三年了,可依然还会有人遇到问题,就说明了这个实现的完善有多难了。

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 同步时需要用代理吗

  • 其他回帖
  • Akkuman

    关于你说到的有一个中心服务器来协调同步,但其实不可避免还是会遇到你说的 "A 设备和 B 设备当前版本持有不一致,然后同时修改" 的问题,即使有一个中心服务器,如果不对这种冲突情况做处理,还是会出现覆盖的问题。

    从这个角度上来说,s3 和 中心服务器区别并不大,只是基于 s3 的互斥锁并不是完全并发安全的,某些极端情况下会有并发问题出现,这个问题我之前尝试研究过,参见 修正并发情况下锁的正确性 by akkuman · Pull Request #4 · siyuan-note/dejavu (github.com),但是发现无法实现 100% 的并发安全,如果基于中心服务器是可以实现 100% 并发安全的同步互斥锁的

    官方对于你提出的问题目前的方案是依靠快照,来靠用户手动恢复解决冲突,可能看起来有些刀耕火种,不过虽说手动比较麻烦,但是是有效的。

    如果考虑到更现代的,可能需要引入一些冲突合并算法,类似于现代的一些协同编辑软件,使用 OT 或者 CRDT,这样即使是基于 s3,也是能实现协同编辑的效果的。我搜索了一下 issue,好像之前作者已经提出过,但由于开发成本的问题,并没有进行下去。

    2 回复
  • 关于锁的问题,我其实觉得锁的问题并不是 s3 或者说非中心服务器同步的问题,而是思源独有的问题。因为思源要达成一个在其他备份软件都根本没有考虑到的问题:只下载需要的、最新的快照。

    两个要求共同导致了这种复杂局面:

    第一个导致了思源不能把全部快照全下载下来,这样就不用考虑锁的问题。

    第二个要求导致思源必须要有某种手段得知当前最新的快照是哪一个。

    那很自然的方法是加个元文件,然后围绕这个元文件的读写就变成复杂的并发加锁问题,而且哪怕你的实现毫无问题,一个 s3 cdn 就能让你的设计沦为空谈。

    那有没有绕过的方案呢?其实也是有的,除了每次都下载全部索引,然后挨个确认哪个是最新的,其实一个最简单的方案就是在索引的名称上下功夫,在索引的命名上加入时间戳和 hash 信息,然后想办法获取索引的列表,这样就能在不下载索引的情况下获取最新的索引是那一个了。

    但是这有个新的问题:列出的索引太多怎么办?本地处理肯定没有问题,但云端问题很大,s3 是有最大列出数量限制的,而且数量大了响应也很慢。

    我想出来的处理方案有两个:削减索引的数量,只列出需要的索引。

    我只要保留需要用的索引,那么索引的数量是可以很小的,全部列出来都不会造成很大压力。比如可以按照 restic 的这种方案保留:保留最近的 30 个快照,1 个星期内每天 3 个,3 个月内每天一个,1 年内每周一个,等等。这样能在保证快照可用的情况下,极大削减快照数量,得以保证列出速度。

    不过这个已经 over 了,第一步想要增加删除指定快照能力的 pr 已经被否了。目前来说,下一步也不用考虑了。

    第二个就是只列出指定的索引。s3 是有前缀搜索的 api 的,因此我们只需要把索引的命名中时间戳放在前面,然后比对上一次上传快照的时间戳与当前时间戳的相同部分作为前缀,就能轻松的找到这段时间中云端更新的快照。

    然后这个也 over 了,原因是思源还有 webdav 的支持。webdav 不支持前缀搜索,只支持列出文件夹。要实现类似 s3 前缀搜索的效果,就要修改索引的文件夹结构,把时间戳的一部分作为文件夹,然后列出对应的文件夹。考虑到目前官方对于同步机制的保守意见,我不认为这个 pr 能通过。

    但其实抽象来看,官方的保守态度,其实正是这种情况的具象化:任何对同步的改进都必须面对对当前同步同步效果满意并且不希望有任何不稳定性的用户,而鉴于同步问题的复杂性,在大多数情况下这个问题的答案都是 no。

    综上所述,大抵思源的同步机制在现有情况下,确实没法做到更好了。

    1 操作
    zxhd86 在 2024-10-16 11:58:32 更新了该回帖
  • git 有一个优势:操作的基本上是纯文本文件。也就是内容判断上按照行、字符进行比较就可以。

    思源采用了“JSON”进行存储,对比的时候,还有层级结构上的差异之类的。

    看你这段的时候,我想到了思源能不能直接上下显示冲突内容。比如:

    <======(提示的段落)

    这是旧内容

    =======

    这是新内容

    ======>

    用户如果需要,直接删掉多余的就好。

    但是这样还有 id 重复的问题。

    另外,思源的对比逻辑,应该没到文件内部吧,工作量也不小。


    所以 git 能实现,很大一部分得益于纯文本格式。

    1 回复
  • 查看全部回帖