---
title: 存储系统实例
url: https://doc.liz6.com/distributed-systems/06-distributed-storage/03-storage-system-instances
locale: zh
area: distributed-systems
tags:
- distributed-systems
- 分布式存储
date: 2026-06-30
modified: 2026-07-16
description: Dynamo(leaderless + gossip + quorum)和 Spanner(leader-based + TrueTime + Paxos + 2PC)代表分布式存储的两极。把前五章讲过的共识、复制、分区、成员发现拼成两个完整的系统架构。
---

# 存储系统实例

> Dynamo(leaderless + gossip + quorum)和 Spanner(leader-based + TrueTime + Paxos + 2PC)代表分布式存储的两极。把前五章讲过的共识、复制、分区、成员发现拼成两个完整的系统架构。

## 概述

前面五章分别讲了[共识](/distributed-systems/02-consensus-protocols/01-Raft.md)、[复制](/distributed-systems/03-replication-and-consistency/01-replication-strategies.md)、[分区](/distributed-systems/04-partitioning-and-routing/01-consistent-hashing.md)、[成员发现](/distributed-systems/05-members-and-discovery/02-gossip-protocol.md)、[读写路径与修复](/distributed-systems/06-distributed-storage/01-distributed-read-write-path.md)——这些不是孤立的概念,在真实的分布式存储系统里它们**同时运转、互相咬合**。这篇选两个经典系统,把前面的零件拼成整车:Dynamo(leaderless + gossip + quorum 路线)和 Spanner(leader-based + TrueTime + 外部一致性 路线)。两个系统代表分布式存储的两极,看懂它们就懂了前面每一章为什么要那么讲。

## Dynamo:可用性优先的拼图

Amazon 2007 年的 Dynamo 论文奠定了 leaderless + eventual consistency 这一整条路线(Riak、Cassandra、DynamoDB 都承自它)。它的架构是几个独立机制的**组合**,每个机制前面都已单独讲过——把这几个拼起来,就是 Dynamo:

| 层 | Dynamo 的做法 | 对应前文章节 |
|----|--------------|-------------|
| 分区 | 一致性哈希,每节点负责一段 token range;引入**虚拟节点**(每物理节点 ~100 个 vnode)让负载更均匀 | [一致性哈希](/distributed-systems/04-partitioning-and-routing/01-consistent-hashing.md) |
| 复制 | 每条数据写到 coordinator 后,按顺时针方向写 N 个后继(vnode 所在物理节点),保证副本分散在不同物理节点 | [复制策略 leaderless](/distributed-systems/03-replication-and-consistency/01-replication-strategies.md) |
| 写 | coordinator 并发写 N 个副本,等 W 个 ACK 即返回;W < N 时部分副本暂时不一致 | [分布式读写路径](/distributed-systems/06-distributed-storage/01-distributed-read-write-path.md) |
| 读 | coordinator 并发读 N 个副本,等 R 个响应,取版本最新的(比较向量时钟,不是时间戳);触发 read repair 异步推最新版本给落后副本 | 同上 |
| 成员发现 | gossip 协议传播节点加入/离开/心跳信息,最终每个节点都有完整的路由表 | [Gossip 协议](/distributed-systems/05-members-and-discovery/02-gossip-protocol.md) |
| 反熵 | 后台 Merkle tree 比对, gossip 驱动修复 | [反熵与数据修复](/distributed-systems/06-distributed-storage/02-anti-entropy-and-data-repair.md) |
| 冲突解决 | 向量时钟(vector clock)追踪因果;应用端在读取时合并冲突(购物车:合并不同副本的条目,别丢) | [冲突解决](/distributed-systems/03-replication-and-consistency/02-conflict-resolution.md) |
| hinted handoff | 目标节点不可达时,coordinator 把它的那份暂存在自己的 hints 区,等目标恢复后回放 | [分布式读写路径 hint 段](/distributed-systems/06-distributed-storage/01-distributed-read-write-path.md) |

这几层咬合时有两个全局约束贯穿全栈:

- **始终可写(always writeable)**——即使部分节点不可达,coordinator 也接受写(hinted handoff + sloppy quorum 保证 W 凑够),代价是冲突可能更频繁。
- **最终一致性**——数据最终收敛,但收敛前的窗口内,不同读可能看到不同版本。应用层要处理这种事(Dynamo 把冲突推给应用端解决,这正是它设计哲学的核心:存储层不替应用做冲突决策)。

### 一个 Dynamo 写的完整旅程(把所有层串起来)

<svg viewBox="0 0 720 410" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Dynamo 写请求全流程:客户端请求经 coordinator 并发写三副本,W=2 达标即返回,不可达节点走 hinted handoff,后台反熵补齐">
  <defs>
    <marker id="dyah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="410" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Dynamo 一次写的完整旅程:并发写三副本,W=2 达标即返回</text>

  <rect x="230" y="40" width="260" height="30" rx="6" fill="#e2e8f0"/>
  <text x="360" y="60" text-anchor="middle" font-size="12" fill="#334155">put("cart:alice", {item:"book"})</text>
  <line x1="360" y1="70" x2="360" y2="80" stroke="#475569" stroke-width="1.6" marker-end="url(#dyah)"/>

  <rect x="180" y="82" width="360" height="38" rx="8" fill="#4f46e5"/>
  <text x="360" y="97" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">Coordinator(任意节点)</text>
  <text x="360" y="113" text-anchor="middle" font-size="10.5" fill="#e0e7ff">已有全集群路由表(gossip 传播得来)</text>
  <line x1="360" y1="120" x2="360" y2="130" stroke="#475569" stroke-width="1.6" marker-end="url(#dyah)"/>

  <rect x="140" y="132" width="440" height="46" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="150" text-anchor="middle" font-size="11.5" fill="#3730a3">hash("cart:alice") → token 落在 range [A, B]</text>
  <text x="360" y="167" text-anchor="middle" font-size="11.5" fill="#3730a3">查路由表:N=3 个 vnode 分别在物理节点 P1、P3、P7</text>

  <line x1="360" y1="178" x2="215" y2="188" stroke="#475569" stroke-width="1.6" marker-end="url(#dyah)"/>
  <line x1="360" y1="178" x2="525" y2="188" stroke="#475569" stroke-width="1.6" marker-end="url(#dyah)"/>

  <rect x="60" y="190" width="310" height="56" rx="8" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="215" y="209" text-anchor="middle" font-size="12" font-weight="700" fill="#0f766e">P1、P3:并发写入成功</text>
  <text x="215" y="226" text-anchor="middle" font-size="10.5" fill="#115e59">append WAL → fsync → write MemTable</text>
  <text x="215" y="240" text-anchor="middle" font-size="10.5" fill="#115e59">→ ACK(version v5)</text>

  <rect x="390" y="190" width="270" height="56" rx="8" fill="#ffedd5"/>
  <text x="525" y="209" text-anchor="middle" font-size="12" font-weight="700" fill="#9a3412">P7:不可达</text>
  <text x="525" y="226" text-anchor="middle" font-size="10.5" fill="#c2410c">coordinator 把这份写进本地的</text>
  <text x="525" y="240" text-anchor="middle" font-size="10.5" fill="#c2410c">hinted handoff 区暂存</text>

  <line x1="215" y1="246" x2="360" y2="258" stroke="#475569" stroke-width="1.6" marker-end="url(#dyah)"/>
  <line x1="525" y1="246" x2="360" y2="258" stroke="#475569" stroke-width="1.6" marker-end="url(#dyah)"/>

  <rect x="220" y="260" width="280" height="30" rx="6" fill="#22c55e"/>
  <text x="360" y="280" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">W=2 ACK 收到 → 返回 SUCCESS</text>

  <line x1="360" y1="290" x2="360" y2="300" stroke="#475569" stroke-width="1.6" stroke-dasharray="4 3" marker-end="url(#dyah)"/>

  <rect x="100" y="302" width="520" height="36" rx="8" fill="#e2e8f0" stroke="#cbd5e1" stroke-dasharray="4 3"/>
  <text x="360" y="324" text-anchor="middle" font-size="11" fill="#475569">(后台异步) 下次 gossip 反熵触发 → Merkle tree 比对发现 P7 缺失 → 补齐</text>

  <rect x="60" y="348" width="600" height="52" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="368" font-size="12" fill="#3730a3">始终可写:P7 暂时不可达也不影响写入,hinted handoff + sloppy quorum 保证 W 照样凑够;</text>
  <text x="76" y="387" font-size="12" fill="#3730a3">代价是短暂不一致——收敛靠后台反熵,而不是这次写就让所有副本立刻一致。</text>
</svg>

## Spanner:外部一致性的拼图

Google Spanner(2012)选了另一条路:**强 leader + 外部一致性**,支撑 Google 的广告和 F1 数据库。它同样是一堆机制的组合,但追求的目标完全相反——**宁可牺牲可用性,也要保证"像单机一样"的事务语义**。

| 层 | Spanner 的做法 | 对应前文章节 |
|----|---------------|-------------|
| 分区 | 范围分片(range sharding),key 按字典序切成 tablets,每个 tablet 是一个 Paxos group | [分片策略](/distributed-systems/04-partitioning-and-routing/02-sharding-strategies.md) |
| 共识 | 每个 tablet 内部是一个 **Paxos group**(多副本,Paxos 选主并复制日志) | [Paxos](/distributed-systems/02-consensus-protocols/02-Paxos.md) |
| 复制 | Paxos log 复制到 group 内 majority;所有写必须经 leader;读可以从 follower(但需要**读事务时间戳**) | [复制策略 single-leader](/distributed-systems/03-replication-and-consistency/01-replication-strategies.md) |
| 事务 | 跨 tablet 写走 **2PC**(两阶段提交),协调者是其中一个 Paxos leader;提交时间戳由 **TrueTime** 分配 | [分布式事务](/distributed-systems/03-replication-and-consistency/03-distributed-transactions.md) |
| 时间 | **TrueTime**:每个数据中心有 GPS + 原子钟对时,保证全局时钟偏移 ≤ ε(~7ms)。给每个事务分配一个"晚于所有已提交事务"的时间戳,实现外部一致性 | [时间与时钟](/distributed-systems/01-basic-theory/02-time-and-clocks.md) |

Spanner 的核心创新是 **TrueTime + 2PC + Paxos** 的组合,实现 **外部一致性(external consistency)**——事务按提交时间戳全局有序,任何读都能看到所有早于它的写。这和 Dynamo 的"最终收敛、读可能看到旧数据"完全相反。

### 一个 Spanner 跨表事务(把所有层串起来)

```
BEGIN
  UPDATE users SET balance = balance - 100 WHERE id = 42  -- 在 tablet A (Paxos group G1)
  UPDATE orders SET status = 'paid' WHERE id = 789        -- 在 tablet B (Paxos group G2)
COMMIT

流程:
  1. Client → G1 的 Paxos leader (变成 2PC Coordinator)
  2. Coordinator: 发 prepare 到 G1 和 G2 的 leader
  3. 每个 group 的 Paxos leader: 把 prepare 记录写入 Paxos log → 复制到 majority
  4. 所有 participant ACK → Coordinator 选 commit timestamp:
     - ts = max(各 participant 本地时间) + 保底 > TrueTime.now() + ε
     - 这保证 ts 晚于任何已提交事务的时间戳 (外部一致性)
  5. Coordinator: 把 commit record + ts 写入自己的 Paxos log → 复制 → committed
  6. 通知各 participant 提交 (Paxos log entry with ts)
```

注意:第 4 步 TrueTime 的 `now() + ε` 保证——因为 ε 是时钟偏差上界,所有数据中心"此刻"的真实时间不可能超过 `now() + ε`,所以选 `ts > now() + ε` 保证 ts 在将来,任何读在 ts 后都能看到这个事务。这就是 Spanner 能对 SQL 提供"像单机一样"的序列化隔离——因为时间戳是真实的、物理约束的全局序。

## 两极对比

| | Dynamo | Spanner |
|---|---|---|
| 一致性 | 最终一致性 | 外部一致性(强) |
| 写可用性 | 始终(leaderless,任何节点可接受) | 需要 leader(leader 挂了等选举) |
| 冲突解决 | 应用端 (向量时钟) | 存储层 (时间戳全局序) |
| 事务 | 不支持跨 key 事务 | 完整 ACID (2PC + TrueTime) |
| 延迟 | 低(无多轮协调) | 高(Paxos + 2PC + commit wait) |
| 时钟依赖 | 宽松(只用于版本比较,不要求同步) | 严格(TrueTime 是正确性的前提) |
| 运维复杂度 | 低(无 leader,节点随便挂) | 高(GPS/原子钟对时,leader 选举) |

这两个系统的选择就是**分布式存储系统的根本权衡**——任何时候在做一个存储选型,本质上是在这张表的两列之间找位置。

## 参考

- **Dynamo 论文**: "Dynamo: Amazon's Highly Available Key-value Store"(DeCandia 2007)——所有 leaderless 系统的源头
- **Spanner 论文**: "Spanner: Google's Globally-Distributed Database"(Corbett 2012)——TrueTime + 外部一致性

*Keywords: Dynamo, Spanner, leaderless, leader-based, consistent hashing, virtual nodes, gossip, quorum, Merkle tree, vector clock, eventual consistency, TrueTime, external consistency, Paxos, 2PC, GPS + atomic clock, commit wait, ε, always writeable, timestamp ordering*
