ARTICLE · 1041562
从“云端”回到“本地”:聊聊本地优先软件
大部分人使用软件时很少去想“数据到底放在哪里”这个问题。写文档用 Google Docs,记笔记用 Notion,照片放在 iCloud 或 Google Photos,换一台手机或者电脑,只要登录账号,原来的内容就会重新出现。这样的体验已经自然到几乎不会去思考背后的系统是怎么工作的,直到有一天服务突然中断。你可能会觉得 Google 这样的公司足够大,Google Docs 这类服务应该永远都可以访问,我以前也这么想。后来 Google Buzz 突然下线,那是一个有点像朋友圈和讨论区结合的服务,我和一些朋友曾经在上面留下不少内容。Google 最后提供了内容导出,但这件事还是让我第一次认真意识到:数据虽然是我写的,但它能不能继续存在,并不完全由我决定。也是从那以后,我逐渐变成一个本地部署的拥趸。

回头看,以前的软件并不是这样。过去在电脑上写一个 Word 文件,文件就在自己的硬盘里,没有网络照样可以打开,软件公司的服务器出了问题也不会影响文件本身。现在很多云软件则相反,服务器上的数据才是主要版本,电脑和手机更多只是访问这些数据的客户端。只要服务正常,这种方式非常方便,它解决了多设备同步、自动备份和多人协作这些以前很麻烦的问题,但同时也把软件、账号、网络和服务商都变成了使用自己数据的前提。我们常常为了方便接受这种交换,直到服务关闭、账号失效或者平台改变规则时,才会发现数据虽然名义上属于自己,但实际控制权一直掌握在别人手里。
“本地优先”,也就是 local-first,就是从这个问题出发的一种软件设计理念。它并不是简单地反对云,也不是鼓励所有人自己维护服务器,而是想重新调整设备和云之间的关系:数据首先属于用户自己的设备,云可以帮助同步、备份和协作,但不应该成为软件能够工作的唯一前提。
1. 我们已经习惯把数据交给云
云软件之所以成为今天的主流,并不是偶然的。传统本地软件最大的麻烦之一,就是每台设备都像一个孤岛。在办公室电脑上写了一半的文档,回家以后想在另一台电脑继续写,就需要先把文件带回去;手机上记了一条笔记,希望平板上马上也能看到,需要自己想办法同步;如果几个人同时修改同一个文件,情况就会更复杂。云软件把这些问题统一成了一个比较简单的模型:大家都连接同一台服务器,服务器保存最新的数据,各个设备再从这里获取和更新内容。
这样做的好处很明显。手机修改了一段文字,变化先发到服务器,电脑随后就可以拿到最新版本;两个人同时编辑,服务器可以决定操作的先后顺序,再把统一结果发给所有人。Google Docs、Notion 以及大量现代 SaaS 软件,基本都建立在这种“服务器是中心”的思路上。从软件开发的角度看,这种架构也比较直接,因为所有设备最终都围绕同一个数据源工作,不需要每台机器自己判断谁的版本才是正确的。
问题也正是从这里开始。没有网络时,一些软件只能部分工作;服务停止以后,客户端往往并没有完整能力继续运行;如果平台修改收费、关闭产品、冻结账号或者改变数据导出规则,用户通常只能被动接受。大型云服务当然很稳定,但稳定和控制权是两件不同的事。一个服务运行十年,并不意味着它一定会再运行十年,更不意味着用户对自己的数据有最终决定权。
Local-first 并不是说云服务不应该存在,而是在反过来问一个问题:既然今天的电脑和手机本身已经有很强的存储和计算能力,为什么它们一定只能作为服务器的客户端?如果数据本来就可以完整地保存在自己的设备上,网络是不是只需要负责同步,而不是决定软件能不能工作?
2. 本地软件、自托管和“本地优先”不是一回事
第一次看到“本地优先”这个词,很容易把它理解成“软件安装在本地”,或者“自己搭一台服务器”。这几个概念确实有关系,但并不是一回事。
最传统的本地软件很好理解。一个文本编辑器直接读写电脑上的文件,文件就在自己的硬盘里。Git 也是一个很典型的例子。把一个 Git 仓库复制到电脑以后,即使完全断网,仍然可以查看历史、修改文件、创建提交,网络只是用来和别人交换变化,并不是软件能够工作的前提。这类软件的核心特点是,用户设备本身就保存着完整的数据和主要功能。
Self-hosted,也就是自托管,解决的是另一个问题。原本运行在别人的服务器上的软件,现在搬到自己的 NAS、VPS 或家庭服务器上运行。这样做以后,服务器由自己管理,数据也不再交给 Google、Microsoft 或其他平台。从控制权上看,这是很大的变化,但它本质上仍然可能是一个服务器中心的软件架构。
比如我在自己的 NAS 上运行一个笔记系统,所有电脑和手机仍然通过浏览器访问 NAS。如果 NAS 关机,整个软件就不能用了,那么只是把“别人的云”换成了“自己的云”,数据和服务仍然依赖一台中心服务器。Local-first 想做的事情不同,它希望电脑、手机和平板这些真正被用户使用的设备,本身就保存一份完整可用的数据。软件首先在本地读取和修改这些数据,等网络恢复以后,再把不同设备上的变化同步起来。服务器仍然可以存在,但它更像一个同步和备份节点,而不是唯一的权威来源。
所以,自托管更关心“服务器是谁的”,而本地优先更关心“服务器是不是必须存在”。这两个方向并不冲突,一个软件完全可以既 self-hosted 又 local-first,但它们解决的并不是同一个问题。
3. 为什么不能简单地“把数据放回本地”
如果只是单机软件,把数据放回本地其实一点也不困难。真正麻烦的是,一旦想保留云软件最重要的优点,也就是多设备同步和多人协作,问题马上就出来了。
假设一份文档同时存在于电脑和手机上。早上电脑没有联网,我在第三段里增加了一句话,与此同时,手机上也离线修改了标题。到了下午,两台设备重新联网,这两次修改应该怎么合并?如果只是简单地比较哪个文件更新时间更晚,就很可能把另一边的修改直接覆盖掉。很多传统同步软件遇到这种情况,会生成一个“冲突副本”,让用户自己判断应该保留哪一个版本。
这种办法用于偶尔同步文件还勉强可以,但如果是多人实时编辑,就完全不够用了。再复杂一点,两个人同时修改同一段文字,一个人在句子中间增加一个词,另一个人删除了前面的几个字;两边都离线工作了一段时间,重新连接以后,软件不仅要尽量保留双方合理的修改,还必须保证最后每个人看到的结果一致。否则不同设备会越来越偏离,最终根本不知道哪一个版本才算正确。
云软件处理这件事相对容易,是因为服务器一直在那里。所有操作都可以先经过服务器,服务器决定哪个操作先发生、哪个操作后发生,再把统一的结果发给所有客户端。它事实上承担了一个中央裁判的角色。Local-first 则不能依赖这个前提,设备可能离线几分钟,也可能离线几个月,两台设备甚至可能从来没有直接联系过,却分别修改了同一份数据。
所以,local-first 真正难的地方并不是“把文件存在硬盘上”,而是如何让多个独立的数据副本在没有中央裁判的情况下继续修改,最后还能自动重新合并。这也是为什么 CRDT 会成为这个领域最重要的一类技术。
4. CRDT 是怎么解决这个问题的
CRDT 的全称是 Conflict-free Replicated Data Type,通常翻译成“无冲突复制数据类型”。名字听起来比较抽象,但它解决的其实正是前面那个问题:多个设备各自修改数据以后,怎样重新同步,而且尽可能不需要人工处理冲突。
可以先从一个最简单的计数器来理解。假设几台设备一起统计某件事情发生了多少次,现在总数是 10,两台设备在断网以后各自加 1,于是双方都变成了 11。等重新同步时就会出现问题,因为真实结果应该是 12,但两边保存的都是 11。如果换一种设计,每台设备不去修改同一个总数,而只记录“自己增加了多少次”,例如电脑增加 4 次,手机增加 3 次,平板增加 2 次,那么最终结果就是三者之和。无论这些设备什么时候同步、按什么顺序同步,最后都能得到相同结果。
CRDT 的基本思路与此类似。它不是等冲突发生以后再想办法解决,而是在设计数据结构时,就让来自不同设备的修改可以按照明确的规则组合起来。简单计数器当然很容易,但一旦换成文字、列表、表格、树形结构和富文本,复杂度就会迅速增加。两个人同时在一句话中间插入文字,最终谁排在前面;一个人删除了一段内容,而另一个人恰好在这段内容中继续输入,最后结果应该是什么;如果文档里还有标题、表格、嵌套列表和各种格式,这些变化又该怎样组合,都会变成实际问题。
这也是为什么 CRDT 的理论出现以后,真正做到工程可用仍然花了很长时间。早期实现经常遇到严重的性能问题。为了保存每一次修改以及它和其他修改之间的关系,系统需要记录大量额外信息,文档本身可能并不大,历史操作和元数据却会持续增长。另一个问题是,数学上“能够合并”并不等于人类看起来“合并得合理”。早期一些文本算法虽然可以保证最终一致,却可能产生人类完全不希望看到的字符交错结果。
过去十多年里,Yjs、Automerge、Loro 这些项目一直在解决这些工程问题。Yjs 很早就把 CRDT 做到了实际可用,在协同编辑领域形成了比较成熟的生态;Automerge 更强调通用的数据模型,希望 map、list、text 等复杂结构都能用统一方式表达;Loro 出现得更晚,在性能、历史压缩和复杂数据结构方面继续做了很多改进。普通用户并不需要了解这些库,就像浏览网页不需要知道 TCP 是怎么工作的,但这些底层技术逐渐成熟以后,上层软件才有可能同时做到本地立即响应、离线工作、多设备同步和多人协作。
5. 本地优先真正想改变的是什么
2019 年,Martin Kleppmann 等人在 Ink & Switch 发表了《Local-first software: You own your data, in spite of the cloud》,系统地提出了 local-first 这个概念。这个理念背后有一段很有意思的历史。Ink & Switch 的一些核心成员此前长期参与云平台和服务器软件,Heroku 的联合创始人 Adam Wiggins、James Lindenbaum 和 Orion Henry 后来共同成立了 Ink & Switch,Peter van Hardenberg 早年参与 Heroku PostgreSQL,也后来加入这个实验室。
也就是说,local-first 并不是由一群拒绝使用云的人提出的,恰恰相反,它来自一批非常熟悉云计算的人。他们过去做的事情,就是让越来越多应用和数据搬到服务器上,后来却开始反过来思考:服务器是不是在现代软件里承担了太多责任。问题并不在于服务器有没有价值,而在于它是否一定要成为整个软件体系中唯一可信的数据中心。
现在大部分云软件的默认逻辑,是服务器保存真正的数据,客户端只是访问它。Local-first 希望把这个关系翻转过来:设备上的数据首先是完整和可用的,服务器负责帮助设备之间同步、备份和交换变化。这样一来,离线能力并不是额外功能,而是自然结果;服务商停止运营以后,数据也不会立刻跟着消失;如果同步服务器只保存端到端加密的数据,那么服务器本身甚至不需要知道用户保存了什么。
不过我觉得 local-first 更有意思的地方,并不是其中任何一个具体功能,而是它重新讨论了软件和用户之间的关系。过去二十年,软件越来越方便,账号、数据、计算、同步、权限却也越来越集中到平台手里。设备还是我们自己买的,内容也是我们自己写的,但能不能继续访问这些内容,有时却取决于一家公司的服务器和商业决定。
Local-first 并不是要求所有人回到没有互联网的时代,也不是要求普通用户自己维护 NAS 和服务器。它真正想做的,是保留云带来的同步和协作,同时让个人设备重新成为软件架构里一个真正独立的主体。软件应该先在用户自己的设备上正常工作,然后再利用网络获得更多能力,而不是先依赖网络,设备只是服务器的一块屏幕。
现在 local-first 还远远没有成为主流,CRDT 的性能、权限管理、大规模协作、设备发现、加密和备份都还有很多工程问题,很多已经存在的产品也只实现了其中一部分。但这个方向至少提出了一个值得重新考虑的问题:如果一个软件背后的公司和服务器有一天都不存在了,我今天创造的数据还剩下什么?
如果答案仍然是一份完整、可以继续使用的数据,那么这大概就是本地优先真正想实现的软件。