🙏
大家好,我是较长时间没有出现的 muhanstudio,今天来和大家唠唠一个老大难的问题:
- 思源笔记的同步
思源同步的现状
思源的同步,无论是性能还是稳定性,已经被诟病了很久,尽管相比之前有了很大的进步,但是如果我们不区别对待,把它和市面上的云笔记软件相比,以下三大雷区基本上踩了一个遍。
- 冲突合并约等于没有
- 同步进行非常缓慢
- 会使当前编辑丢失
对同步速度的看法
我曾经持有一个粗暴的观点,认为只要同步的够快,用户根本来不及进行什么骚操作,也不会产生很多的冲突,反而是磨磨蹭蹭、畏手畏脚的走独木桥更容易摔下去。
插件开发尝试
同样,我也曾为之做过一点改变,连续开发了两款插件,一个是同步感知插件,一个是加速同步插件,他们的作用一个是尝试模拟官方的效果,在不同设备之间尝试寻找一个沟通点,在其中一个设备更改内容同步后,立即通知其他设备发起同步,一个是为了解除官方自动同步最快要等 30 秒的限制,只要你愿意,可以将同步操作的前置等待时间在保证正常顺序的同时加速到极限,但是,效果远远没有达到我的预期,主要是因为,整个机制依旧建立在思源的同步上,我只是加速了通知每个终端思源开始同步这个过程,而以上的问题,依旧没有得到很好的改善,这让我曾经郁闷过很长一段时间。
同步机制的妥协
但是随着后来深入分析思源的同步机制,会发现,其实思源因为纯本地化,在数据同步上做了很大的妥协。本地化存储(localstore)和自伺服(selfhosted)下的数据私有化完全不是一个概念,云笔记也可以是自己的云,但是本地化笔记就真的只是本地而已。
冲突合并与自伺服系统的差异
体现在什么地方?尤其在冲突合并上,对于自伺服系统,你可以完全和云笔记一样,拥有一个 24h 活跃的数据中心,这个数据中心可以作为唯一的参照基准,对于多个客户端在一个广播周期内发来的数据,数据中心可以自己处理好冲突取舍与合并,合并机制并不是最重要的,重要的是主动广播权和并发接受多版本数据的能力,数据中心拥有最高话语权,可以向所有设备在一个同步响应周期内广播相同的数据,哪怕合并机制做的简单粗暴,最终多端数据依旧是一致性的。数据中心还可以存储多个版本的数据,而不只是单线程的 IO 处理,如果想要回撤,由于数据中心存储了每个客户端的响应,同样可以在所有的版本之间做选择。
思源同步的工作方式
而思源的同步是怎么做的呢?或者说,只能怎么做?作为一个本质上完全本地化的软件,它目前是依赖于一个静态的储存设备,而这种情况下,目前最好的选择是对象存储,我下面会采用一些比喻来避免我的描述变成技术名词大集合,可能有一点不恰当,但是尽量贴切的来描述对象存储在思源同步中的角色。
并发性与一致性问题
每个客户端是一个独立的人,他们互相之间没有通道可以沟通(官方订阅也貌似做了设备显示,可设备之间的沟通也约等于没有),但是都可以走进一个静态的仓库里面,仓库里面本身没有人,只有数据,且没有主动广播消息的能力,仓库一次只能进一个人,进去后可以复制仓库的数据,或者修改仓库里的数据,在这种情况下,要保持所有人的数据都是一样的,光是逻辑上的问题就很多:
- 并发性与一致性问题
- 数据同步滞后
- 缺乏事务控制
并发性与一致性问题
由于每个客户端进入仓库后可以读取或修改数据,但其他客户端在它们进入仓库之前无法得知最新的修改结果,这导致数据的一致性无法得到保障。例如,客户端 A 进入仓库,读取并修改了数据,但此时客户端 B 仍然读取了旧数据,因为可能 A 同样可以在自己同步的过程中持续修改本地数据,或在 A 写入新数据之前,B 已经读取并打算写入自己的修改。这样就可能发生数据的不一致。 即使通过某种机制保证一次只能有一个人进入仓库,也无法确保在下一个人进入仓库前,其他人始终读取到最新的数据,特别是在修改数据的过程中会产生暂时不一致的状态。
数据同步滞后
假设在进入仓库时可以成功锁定资源并避免并发访问,虽然仓库中的数据在每次写入后是最新的,但客户端获取到数据和执行修改之间存在时间差。在这个时间差中,其他客户端可能会基于旧数据做出决策,而不是基于仓库里的最新数据。 如果不设计同步机制(例如读写操作中加入时间戳或版本控制),数据滞后将会导致多个客户端的数据版本不一致。没有全局同步机制,无法确保每次更新的数据能被所有客户端即时获取。
缺乏事务控制
事务控制机制通常用于保证并发环境下的数据一致性,而这个系统缺乏事务控制。假设客户端 A 在仓库中读取数据并进行长时间操作,此时如果没有事务锁定,其他客户端无法获得数据是否已被最终写入或更新的信息。由于缺乏事务回滚与提交机制,任何错误或中断都可能导致仓库中数据的不一致。 数据一致性要求所有客户端在每次操作结束后都能立即看到相同的数据版本,但是这个场景没有实现任何事务处理或两阶段提交协议等方法来管理这一点。
自伺服系统的优势
而换到自伺服系统中,仓库并不再是类似一个静态磁盘一样的地方,而是一个运行中的数据库系统,可以同时处理所有客户端的请求,并且可以缓存下来同一时间不同客户端的修改操作提交,即使它合并冲突或者同步请求的策略非常粗暴,由于最终可以向所有客户端同时广播一份一样的数据,使得它的同步性能依旧要好得多。因为自伺服系统具备并发访问、事务管理、缓存合并和实时数据广播的能力,可以在处理并发请求时确保最终数据一致性,从而避免数据滞后问题。 这也解释了,为什么对象存储已经是目前世界上广泛应用的最高效的网络数据存储,在思源上依旧显得只是不限带宽的同步网盘而已,在我部署过的无数项目中,例如 anytype,affine,appflowy 等等等等,他们同样使用了对象存储作为自己的后端存储,但他们并不是让客户端直接操作对象存储,翻看他们的 docker compose 文件,他们都有刚才我提到的类似于数据中心的实例,会单独起一个服务,在客户端与对象储存之间作为 24h 值班人员,来处理各个客户端的并发请求和一致性广播,并将一致性数据在一个内网或者同域的安全稳定的环境中传入对象存储(除非你使用了分布式部署对象存储)。
思源的同步悖论
说到这里,思源作为一款本地化软件,貌似被披上了一层悖论,永远无法做到优秀的同步性能,那么本地化和 自伺服/拥有数据中心 的同步方式是对立的关系吗,从来都不是,即使拥有以上同步机制,我们依旧可以让客户端默认总是确保所有的完整的数据都被拉取到本地,当失去了云端,也就是数据中心后,我们仅仅失去了我们的云同步功能,但是我们的数据依旧是完整的在本地的。 作为官方订阅的同步方式,思源至今还是主要采用本地化加上静态存储设备的方式来进行同步。社区版的思源其实已经提供了很大的启示,可以直接连接到 docker 部署的思源实例,可以作为一个数据中心来实现以上的操作,但由于是社区二开,稳定性和通用性都存在非常多的问题,无法真正投入使用。之前我还在群里讨论过云内核的实现方法,思路也是类似的,为每一个官方订阅用户提供一个 24 小时运行的云端内核,可以和每个客户端建立长连接,来确保高效的数据传输和数据一致性。同样还设计过一个简单的后端请求转发器,由于思源前后端采用 HTTP 通信, 我尝试拦截所有的后端请求然后转发到另一个客户端上,效果居然意外的不错,可惜最终也是因为必须所有的客户端都全程在线,或者需要做好请求的长时间缓存导致技术难度陡增而放弃。
与成本控制和解
到最后我还是发现,我对思源要求还是太苛刻了,我想我的不满主要来源于官方订阅还提供低效的同步,虽然所有数据还是留存在本地,但是我相信很多人买官方订阅,而不选择功能特性里的 S3 同步,其实有很多一部分都是抱着市面上成熟的云笔记的心态去用,自己看不到自己的数据究竟被放在了哪里,通过什么样的方式进行传输和管理,只知道官方负责我数据的多端一致性,我买的是官方同步,由思源笔记的服务器来负担,理应比自己买对象存储要安全和稳定很多,如果当初的心态只是把官方订阅当做是一个小容量的不计流量包年对象存储,不用自己填相关的配置,一个官方提供的静态备份服务,在思源笔记已经如此开放,营收手段如此少,开发资源也紧张,不得不考虑成本控制的情况下,其实也就还好。可惜这样做对于开发者自己也是有代价的,因为如果想解决,甚至只是优化开头的那几个问题,随之而来要解决的逻辑问题就会几何倍数增加且越发复杂,即使将来尝试引入一些无中心的方案,比如 webrtc 和 P2P,在目前的技术结构上,例如在基于文档/文件级别的粒度上同步,坑也会增加,anytype 对于冲突块的解决方式是自动在两个客户端换行生成新的块来显式全量存储两个冲突块,而思源目前的快照机制,还需要大量的重构才能达到相应的效果,同时,单单它的同步核心 any-sync(包含非常多的子部件)都由两个以上的人员开发,我想它也用自己的复杂度和工程量之大亲身实践告诉我们,点对点方案只适合作为一个新鲜的特性来引入,作为成本控制,也是不适合作为中心化的廉价替代方案的。
展望与建议
这一章其实是 AI 帮我分章节的时候生成的,我个人很难给出什么具体的或者专业的建议(除了加钱),只能在已经做好的成熟的数据一致性系统之间进行肤浅的对比和解读,写这篇文章主要也是让更多的人了解并且理解在很多场景下思源一直以来的同步困境,很多时候,在不断的需求受限中,现实的与成本控制和解,才是解决矛盾的折中办法。