并不简单的 Chromium Sync
2026-01-21
Chrome 有一个功能不显眼,却又很符合直觉:你在一个端上使用浏览器的体验,可以平滑地过渡到其他设备。
比如密码填充,它会帮你记住,然后下一次使用时自动填充,即便你换了设备。
背后起作用的,就是 Sync(同步)系统。
只要你登陆,便可将几乎所有的使用状态记录,并迅速“告诉”其他已登陆设备。理论上,在 Sync 眼里,所有设备的状态都是对齐的。
像是分布式系统里的主从复制和共识算法(Consensus)。
关于 Chromium Sync,网络上相关的技术文章不多,此外官方文档相对艰涩、拗口,阅读门槛较高。因此今天这篇文章,打算聊聊 Sync 这个子系统,怎么工作,背后的原理,对我的启示。
尽可能通俗易懂。
1. Sync 面临的问题
Chromium Sync 要解决的问题只有一个:跨端用户状态一致。
这种状态,分时间和空间两个维度。
时间上,Sync 满足最终一致。这是一种弱一致性模型,带来的是整个系统高度可用,架构更灵活。这可以理解,毕竟“不能因为没有醋,连饺子都不吃了”。
空间上,Sync 保证逻辑一致。字节层面的一致,结果一定逻辑一致,反之不然。同样,这是一种弱约束,但够用。
尽管已经放松了约束,但这两个一致,在残酷的现实面前,依然难以满足:
- 糟糕的网络条件,导致用户的修改,或其他端的更新不能及时传递
- 多端同时修改,顺序的不确定带来潜在冲突
这些问题,客观存在,且不可避免。需要作为“最坏复杂度”,纳入后续 Sync 系统的设计考量中,而不能假装问题不存在。
2. 设计哲学
让我们简单总结,Sync 系统踩中了哪些“雷区”:
- 多主写入(Multi-Writer)。每个客户端都可以平等地修改状态,没有主节点,或者说,都是主节点。
- 离线优先。客户端离线时,该有的功能几乎都有,只不过修改暂存本地。一旦联网,那些修改应立即参与到与其他节点的同步中。
- 用户感知敏感。Sync 对于状态的维护,直接体现在 UI 上,丢数据、乱序、重复,都会带来糟糕的体验,甚至造成用户流失。
Sync 采取了三个措施,尽可能规避以上问题:
- “无知”的客户端。客户端并不“相信”自己的状态是最新的,需要频繁地向服务器“确认”。
- 服务端不理解 Sync 语义。服务端只关心,并尽可能做好三件事:存储,版本管理,冲突检测。
- 增量更新。除初始化 Sync 时需要一次全量下载外,后续所有更新都是增量,兼顾性能、正确和可回退,十分符合敏捷开发中“小步快跑”的思想。
3. 架构细节
3.1 数据结构
在服务端与客户端通信过程中,有三个共识概念:
-
Model Type
水平区分不同的同步类型,例如书签(Bookmarks)、密码(Passwords)等,类型间各自独立,互不干涉。
-
Entity
垂直区分同一类型内部不同版本。每一个同步对象,例如一个书签,会包含如下几个关键属性:
- server id,服务端分配的随机字符串
- client tag hash,基于数据内容和类型生成的唯一标识,跨设备一致,是防止数据冲突和重复的重要参考
- specifics,包含实际的 payload,是一个 Protocol Buffer 对象
- timestamp,创建和修改时间,可用于后续冲突解决(Conflict Resolution)和增量更新决策
-
Metadata
跟踪同步状态的重要抓手。尽管不像 Entity 包含具体内容,但是它记录着数据修改时间、是否与服务器同步等关键信息。
- server id,如上
- client tag hash,如上
- sequence number,客户端的本地序列号,每次修改都会递增
- acked sequence number,服务器确认过的最新序列号
- 显然,如果 sequence number > acked sequence number,意味着本地有未提交的更新
- server version,服务器上该数据最新的版本号,如果本地是旧的,则需要更新
- specifics hash,用于快速判断数据内容是否真的发生变更,减少无效的数据上传
3.2 更新上传
以本地新增一个书签为例。
1️⃣ 本地模型变化
- BookmarkModel 发出通知
2️⃣ Sync 捕获变更
- 生成 Entity change
- 标记为 unsynced
3️⃣ Commit 阶段
- 打包变更
- 发给 Sync Server
4️⃣ Server Ack
- 返回新的 version
5️⃣ 本地 Apply Ack
- 更新 metadata
- 标记为已同步
3.3 反客为主的同步
与服务端保持同步,有“推”和“拉”两种模型,各有优缺。
Sync 综合了“推”和“拉”两种方式。
客户端和服务端之间保持双向通信。服务端有更新时,会向所有当前时刻与它保持连接的客户端广播变更。记住!只通知有变更,并不会把变更数据一股脑推出去。
因为,要不要获取更新,由客户端决定。
1️⃣ 服务端产生更新
- 其他设备提交新的数据
- 服务端记录更高的 version / progress marker
2️⃣ 服务端通知“有更新”
- 通过 invalidations / hints
- 只告诉客户端: 👉「某个 type 可能有新变更」
📌 不会携带具体数据
3️⃣ 客户端触发同步
- 客户端收到 hint
- 或在下一个周期性 sync 中
- 决定发起一次
GetUpdates
4️⃣ 客户端主动拉取更新
-
带上自己当前的 progress marker
-
请求:
“把我没见过的更新给我”
-
服务端返回 Update 列表
5️⃣ 客户端校验并应用
- 校验合法性、排序
- 冲突处理
- Apply 到本地模型
- 更新本地 metadata
4. 总结
如果让我设计一个 Sync 系统,我可能会沿袭中心化的思路:服务端拥有最新数据,所有客户端与之对齐。
架构简单,正确性容易保证。
这样以来,客户端逻辑会变得异常简单,复杂的操作,像去重、冲突解决,交给服务端。
Chromium 为什么不这么做?
原因可能有这么几个。
- 隐私与端到端加密。用户数据在客户端以外都是加密的,服务端不可见。
- 服务端性能。Google 和 Edge 服务的用户数以亿记,如果能将部分运算下放到客户端,从经济上,节省下来的成本无疑是巨大的。
- 客户端优先。再好的冲突解决算法,都不能违背用户的真实意图,作为离用户最近的客户端,掌握的信息理应是最全的,也更容易做出符合用户想法的决策。
因此,Chromium Sync 被设计为一个以客户端为中心、多主、弱一致、最终收敛的状态协调系统。
相比于严格的 CRDT(Conflict-free Replicated Data Types),Chromium Sync 更宽松,更务实。
软件工程领域,没有所谓的万金油,任何选择都有代价。
和生活一样,这是一门关于平衡的艺术。
(完)
参考
- 本文作者:Plantree
- 本文链接:https://plantree.me/blog/2026/chromium-sync/
- 版权声明:所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
最后更新于: 2026-01-21T06:10:05+08:00