Spinnaker:用 Paxos 建数据存储
这是一篇来自生产的工程总结,价值不在协议本身(用的是 Paxos),而在它回答了那个反复被问的问题:把 Paxos 真的用进数据存储,代价有多大? 答案有具体数字:相对最终一致的数据存储,读一样快甚至更快,写只慢 5% 到 10%,节点恢复耗时不到半秒。
Spinnaker 是 IBM 与 LinkedIn 合作的实验性 datastore,设计目标是单个数据中心内的大规模商品服务器集群。它的三个特性是:基于 key 的范围分区、3 路复制、带事务的 get-put API,读上可选强一致或 timeline 一致(后者允许返回可能过期的数据以换性能)。
先看它在 CAP 里选了哪一格
定位说得很直白:在 CAP 术语里 Spinnaker 是一个 CA 系统。
这个选择的依据来自 Stonebraker 的论证:在单个数据中心内、网络分区罕见时,为强一致与可用性而设计是更好的取舍,也就是选 CA。 跨数据中心的容错交给另一套机制(推测是异步复制),于是单集群内的 Paxos 只需要面对崩溃故障。
对照一下隔壁那条路线:Dynamo 那类最终一致的存储走的是 AP —— 故障、分区、冲突写会让副本分叉,应用必须自己做冲突检测与解决,而且不支持 ACID 的隔离保证。态度是:少数有极端可用性要求的应用能忍受最终一致的这些细节,但多数应用会希望有更强的一致性保证和一定的事务支持。
为什么不用 master-slave:一段具体的失效时序
一条四步失效序列,说明只挂一个节点就能让数据库对读写都不可用。
先看前提。传统 2 路同步复制里:一个节点是主,所有写都路由到它;主的日志发给从;主只有在从先把提交记录落盘之后,才强制把提交记录落盘。这个设计下:
- 从挂了 → 主继续跑,不影响;
- 主挂了 → 从有最新状态,可以接管。
看起来能容忍一个节点。但失效的先后顺序能把它破掉:
| 步骤 | 状态 |
|---|---|
| (a) | 两节点都从 LSN = 10 开始 |
| (b) | 从挂了 |
| (c) | 主继续接受写一直到 LSN = 20,然后主也挂了 |
| (d) | 从先恢复,主仍挂着 |
问题出在 (d):此时从既不能接受读也不能接受写,因为它没有最新的数据库状态。而且如果主是永久失效,LSN 11 到 20 的已提交写就丢了 —— 这些是客户端已经收到成功响应的写。
避免这个问题的唯一办法是"主从对里任一节点挂掉就阻塞写",但这种可用性限制对某些应用不可接受。
顺带解释两件事:
- 为什么"这种例子概率极低"不足以反驳:在大集群里,双盘故障也变得更容易发生;2 路复制且没有 RAID 或特殊硬件时,这会导致灾难性数据丢失。所以3 路复制在商品服务器上成了常见选择。
- 3 路复制的额外好处:单个节点失效不再进入 "panic mode"(再挂一个就丢数据),在线升级也变容易 —— 可以摘掉一个副本升级,另两个保持在线。
但 3 个副本会带来比上面那条更复杂的失效序列。 结论是:朴素解法在简单情形下能工作,但没有被证明在一般情况下正确;Paxos 家族被广泛认为是"有 3 个或更多副本"时唯一有证明的解法,它在
于是就有了那个"为什么以前没人用"的问题:
Paxos 一直没被用于数据库复制,因为它通常被认为太复杂、太慢。
Spinnaker 就是来拆这个假设的。
四步序列画成时间线(从下往上依次推进):
前提:2 路同步复制 —— 主必须先等从把提交记录落盘,才能强制自己落盘
(a) 两个节点都从 LSN = 10 开始
主 ──── 接受写 ────▶
从 ──── 同步落盘 ──▶
(b) 从挂了
从 ──── 停止 ────
主 ──── 继续接受写 ──────────────────────▶ 一直走到 LSN = 20
(c) 主也挂了(此时 LSN = 20)
主 ──── 停止 ──── LSN 11–20 是客户端已经收到成功响应的写
(d) 从先恢复,主仍挂着
从 活着,但缺 LSN 11–20 的状态
⇒ 它既不能接受读、也不能接受写:只挂一个节点,数据库对读写都不可用
⇒ 若主是永久失效,LSN 11–20 的已提交写随之丢失为什么不用 2PC
2PC 也被提出来过做副本一致性,但有三条否决理由:
- 单个节点失效就会导致 abort —— 与"节点失效时保持系统可用"这个目标直接冲突;
- 性能差 —— 典型实现要 2 次磁盘强制落盘(disk force)+ 2 个消息延迟;
- 协调者失效时会阻塞。
非阻塞的三阶段提交也被提出来过,但因为性能差在实践中很少使用。
关键区别在于定位:2PC 把每个参与者当作独立的资源管理器(各有自己的数据库),而副本场景里它们只是同一份数据的拷贝 —— 判断是对复制来说 2PC 是杀鸡用牛刀(overkill),而且还要额外承受上面三条代价。
Spinnaker 的 Paxos 用法
对 Paxos 的使用与"教科书用法"相比有三个不同点,这正是它声称"Paxos 可以更简单"的依据:
- 与提交日志集成:Paxos 复制协议与 Spinnaker 的 commit log 和恢复流程整合在一起,而不是叠在数据库外面。这是它能把开销压下来的主要原因 —— 复制不额外多写一遍日志。
- 使用分布式协调服务:"使用一个分布式协调服务"被列为让 Paxos 实现变简单的设计选择之一。Paxos 方案本身假定"提案号不重复"由实现保证、leader 选举由外部提供,这些在工程上都是要落地的活;交给一个现成的协调服务(例如 Chubby / ZooKeeper 那一类,见 04-Zab 与 ZooKeeper)就省掉了自研选举与成员管理。
- 可用性的判据:只要持有某个数据分区副本的节点多数存活,该分区就可读写。
最后这条是 Paxos 相对 master-slave 的真正收益,一句话点明:
与传统主从复制不同,这一点与故障发生的顺序无关。
这正好回应了上面那条四步失效序列 —— 主从复制的可用性依赖于"先挂谁、后挂谁",Paxos 的可用性只依赖"多数副本是否活着"。
可用性的判据对照:
Spinnaker:可用性只看「多数副本是否存活」,与失效顺序无关
分区 P 的 3 个副本: [A] [B] [C]
挂掉 [A] ⇒ 还剩 B、C 两个 ⇒ 多数存活 ⇒ 该分区仍可读写
再挂 [B] ⇒ 只剩 C ⇒ 不是多数 ⇒ 该分区不可读写(此时本就无从恢复)
谁先挂、谁后挂都不改变这条判据。
master-slave:可用性取决于失效顺序 —— 上面那条四步序列里,
只要「从先恢复、主后恢复」这一个顺序出现,整库就卡死。
恢复成本的一个具体来源:日志恢复靠一个周期性的异步 commit 消息,
提交周期取 1 秒左右时,节点恢复时间落在半秒以内。判据速查
| 问题 | 答案 |
|---|---|
| 为什么不能只用 2 路主从复制 | 存在只挂一个节点就让数据库对读写都不可用的失效顺序(见那四步时序);主永久失效还会丢掉已提交的写 |
| 为什么用 3 路复制 | 大集群里双盘故障更可能发生,2 路复制的数据丢失是灾难性的;3 路还让在线升级可以用"摘一个副本"的方式做 |
| Paxos 相对 master-slave 的核心收益 | 可用性只依赖"多数副本存活",与故障发生的顺序无关 |
| 2PC 为什么不适合做复制 | 单节点失效就 abort;每事务 2 次 disk force + 2 个消息延迟;协调者失效阻塞 |
| Spinnaker 在 CAP 里是哪一格 | CA —— 单数据中心、分区罕见,按 Stonebraker 的论证选强一致 + 可用 |
| 跨数据中心怎么办 | 假定用另一套(异步)复制,不在本篇范围内 |
| Paxos 复制的实测代价 | 相对最终一致的存储:读持平甚至更快,写慢 5%–10%;节点恢复 < 0.5 秒 |
| 让 Paxos 实现变简单的两个设计选择 | 与提交日志集成(不额外写日志)+ 用分布式协调服务接管选举与成员管理 |
| 读的一致性可以选择吗 | 可以 —— 强一致或 timeline 一致(允许返回过期数据换性能) |
相关
- Paxos —— 本篇用的是它的 Multi-Paxos 形态(一串实例对应提交日志)
- 01-分布式事务 —— 本篇「为什么不用 2PC」一节与那篇的 2PC/Paxos Commit 是同一个话题的两个角度:那里用 Paxos 替换 2PC 的 TM,这里直接用 Paxos 做复制而绕开 2PC
- 04-Zab 与 ZooKeeper —— 前面提到的"分布式协调服务"在这一类系统里通常由谁提供
- 01-CAP 定理 —— 「单数据中心选 CA」这条判断的出处与边界
参考
- Jun Rao, Eugene J. Shekita, Sandeep Tata. Using Paxos to Build a Scalable, Consistent, and Highly Available Datastore. Proceedings of the VLDB Endowment 4(4), 2011, pp. 243–254.
YJ