并不简单的 Chromium Sync

2026-01-21 pv

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 为什么不这么做?

原因可能有这么几个。

  1. 隐私与端到端加密。用户数据在客户端以外都是加密的,服务端不可见。
  2. 服务端性能。Google 和 Edge 服务的用户数以亿记,如果能将部分运算下放到客户端,从经济上,节省下来的成本无疑是巨大的。
  3. 客户端优先。再好的冲突解决算法,都不能违背用户的真实意图,作为离用户最近的客户端,掌握的信息理应是最全的,也更容易做出符合用户想法的决策。

因此,Chromium Sync 被设计为一个以客户端为中心、多主、弱一致、最终收敛的状态协调系统

相比于严格的 CRDT(Conflict-free Replicated Data Types),Chromium Sync 更宽松,更务实。

软件工程领域,没有所谓的万金油,任何选择都有代价。

和生活一样,这是一门关于平衡的艺术。

(完)

参考

  1. 共識機制 - 维基百科,自由的百科全书🔗
  2. Sync🔗
  3. components/sync/protocol/entity_data.h🔗
  4. components/sync/protocol/entity_metadata.proto🔗
  5. 无冲突复制数据类型 - 维基百科,自由的百科全书🔗
在 GitHub 上编辑本页面

最后更新于: 2026-01-21T06:10:05+08:00