元数据卡
- 前置知识:Vol 5 数据库(B+ 树、LSM-Tree)、第3章(分布式共识)
- 预计时间:45 分钟
- 核心难度:进阶
- 阅读模式:高度专注
- 完成标志:能说清一致性哈希如何应对节点变化,理解 HDFS 的分块+副本设计,掌握 Cassandra 的 Ring 和读写路径,知道 Spanner 用了哪些技术实现全球级分布式数据库
你的进度
3 个节点的 etcd 集群已经能稳定地用共识协议管理配置和锁了。但战报系统产生的日志数据越来越多——侦查报告每天 5GB,指挥部每天会商记录 3GB,加上各堡垒的兵力调动记录。这些数据不能塞进 etcd(它只适合小数据量、强一致性的元数据)。
林将军说:"数据是粮草。粮草不能全堆在一个仓库里,一烧就全没了。分开放,放多远?怎么找?"
你面对的是分布式存储的核心问题:海量数据放在多台机器上,怎么分,怎么找,怎么保证可靠。
你的任务
掌握分布式存储的三种核心范式:一致性哈希(确定数据在哪台机器上)、HDFS(大文件的块存储)、Cassandra(列式存储 + 分区 + 最终一致性)。最后用 Google Spanner 作为例子,看看这些技术的综合应用能达到什么高度。
破局 · 溯源
不能只靠"取模"
最简单的数据分片是取模:数据 key 的 hash 值对节点数 N 取模,决定数据去哪台机器。
node = hash(key) % N问题:当你增加一台机器(N -> N+1)或减少一台机器时,几乎所有的 key 都会迁移到新节点。对于 TB 级数据,这意味着灾难性的数据搬运。
更糟糕:你不能优雅地扩容。加一台机器意味着整个集群要重新平衡。
你需要一种方案:节点增删时,尽量少的数据需要迁移。这就是一致性哈希(Consistent Hashing)。
一致性哈希
一致性哈希的思路出奇简单:把哈希值空间看作一个环,范围从 0 到 2^32 - 1。每个节点在这个环上有一个位置(对 node ID 取 hash)。每个 key 也在这个环上有一个位置,然后顺时针找到第一个节点。
节点 A (hash = 100)
|
|
key X ----> | ← key X 顺时针找到的第一个节点是 A
|
|
节点 C | 节点 B
(900) | (400)
|
|
key Y (hash=700) → 顺时针找到节点 C当加入一个新节点 D(hash=350)时,只有 D 到顺时针下一个节点之间的 key 需要迁移到 D。也就是说,只有 [350, 400) 这一段的数据从 B 搬到了 D——其他 key 不受影响。
虚拟节点优化
一致性哈希有一个缺陷:当节点数量少时(3、5、7 台),哈希在环上的分布不均匀。引入虚拟节点:每台物理节点在环上占有多个位置(例如 100 个虚节点),让每个物理节点负责的环段数量趋于均匀。
Cassandra 使用了这个技术——每个物理节点默认拥有 256 个 token(虚拟节点),大幅改善数据分布的均匀程度。
| 物理节点 | 物理节点数量 | 无虚节点时的数据倾斜 | 256 虚节点时 |
|---------|-------------|-------------------|-------------|
| 3 | 3 | ~45% 差异 | <3% 差异 |
| 10 | 10 | ~20% 差异 | <1% 差异 |深入:HDFS —— 大文件往哪放
HDFS(Hadoop Distributed File System)是分布式文件系统的经典设计。它解决的核心问题:单个文件大到单机放不下(TB 级)时,怎么存储和访问?
基本架构:
+-------------+ +-------------+ +-------------+
| NameNode | | NameNode | | NameNode |
| (Active) |<----| (Standby) | | (Standby) |
+------+------+ +-------------+ +-------------+
|
| 元数据操作(文件列表、块位置)
|
+------+---------+---------+---------+
| | | | |
v v v v v
+---+ +---+ +---+ +---+ +---+
|DN1| |DN2| |DN3| |DN4| |DN5|
+---+ +---+ +---+ +---+ +---+
|blk1 |blk1_rep|blk2 |blk2_rep|blk3 |
|blk2 |blk3_rep|blk4 |blk5 |blk4_rep
+-----+--------+-----+--------+-----+- NameNode:管理文件系统元数据。文件名→块列表、每块的副本位置。NameNode 是 HDFS 的单点故障(虽然有 Active/Standby 方案)。
- DataNode:实际存储数据的节点。数据以块(Block)为单位存储,默认 128MB。
- 每个文件被切分为若干块,每个块在多个 DataNode 上保存副本(默认 3 副本)。
读流程:
客户端: "读 /reports/daily-2026-06-24"
|
v
NameNode: 文件有 8 个块
块 1: DN1, DN3, DN5
块 2: DN2, DN4, DN6
...
|
v
客户端: 从离自己最近的 DataNode(网络拓扑最近)读取每个块
本地 DN → 同机架 DN → 跨机架 DN写流程:
客户端: "写 /reports/daily-2026-06-24"
|
v
NameNode: 分配 8 个块,每个块选 3 个 DataNode
|
v
客户端: 数据流经过"管线式"复制
客户端 → DN1 → DN2 → DN3
(客户端写完一个 packet 后,再写下一个 packet,
不再等前一个被确认的 ACK)HDFS 适合顺序读写大文件,不适合随机写入和低延迟访问。它是 MapReduce 的底层存储,但不是通用目的文件系统。
Cassandra:写优化的分布式数据库
2019 年,Netflix 运行着超过 2,000 个 Cassandra 节点集群,每个集群承载 数 PB 数据。Cassandra 是分布式存储中"AP 派"的代表——面向写优化、最终一致性、无单点故障。
数据分布:Ring + Partition
Cassandra 的数据分布基于一致性哈希(虚节点模式)。每行数据通过 Partition Key 决定落在哪个节点上。
Cassandra Ring(简化):
+----+ +----+ +----+ +----+ +----+
| N1 | | N2 | | N3 | | N4 | | N5 |
|t=1 | |t=2 | |t=4 | |t=7 | |t=9 |
+----+ +----+ +----+ +----+ +----+
key "user_1001" hash=3 → 落在 N3 (范围 2-4)
key "user_2002" hash=5 → 落在 N4 (范围 4-7)写路径:
客户端写 key="user_1001" value={name:"张",score:95}
|
v
协调器(Coordinator, 可以是任何节点)
|--- 计算 key 的 token → N3
|--- 根据一致性级别(QUORUM = 大多数)
|--- 同时写入 N3, N4, N5(三个副本)
|--- 写入 MemTable(内存表)+ CommitLog(追加写)
v
当大多数节点确认写入后,返回客户端成功读路径:
客户端读 key="user_1001"
|
v
协调器:
|--- 向 N3, N4, N5 发送读请求
|--- 带读修复(read repair):检查数据版本
|--- 返回最新版本的数据给客户端
v
如果副本间存在数据不一致,后台进行修复Cassandra 的写性能极高(追加写,CommitLog 是顺序 IO),适合写入密集场景。它的最终一致性模型意味着并发读写可能出现不一致——业务层需要处理这种可能性。
Cassandra 分区与二级索引
Cassandra 的查询模式极度依赖主键(Partition Key + Clustering Columns)。没有 JOIN,不能用非主键列做范围扫描——这些限制迫使你在建表阶段就设计好查询模式。
这是 Cassandra 与关系型数据库最本质的区别:查询驱动表设计,而不是表驱动查询设计。
Spanner:Google 的全球级数据库
Spanner 不是一个新的存储引擎,它是多种技术的综合:一致性哈希(分片)、Paxos(副本内共识)、TrueTime(全局时间戳)、以及 SQL 层的分布式查询优化。
Spanner 架构(概念):
SQL 层(F1)
|
+-------+-------+
| |
分布层(分片 + 路由)
| |
+---+---+ +---+---+
| Paxos | | Paxos | 每个分片是一个 Paxos 组
| 副本 | | 副本 |
| Z1 Z2 Z3 | Z4 Z5 Z6
+-------+ +-------+Spanner 的核心突破:
外部一致性(External Consistency):通过 TrueTime 保证事务的提交时间戳与真实时间对齐。事务 T1 在 T2 之前提交,T2 一定能看到 T1 的结果。
读写事务:对跨分片事务,Spanner 使用两阶段提交(2PC),协调者通过 Paxos 保证高可用。
无锁只读事务:利用 TrueTime 实现快照隔离,读操作不阻塞写操作。
Spanner 证明了:分布式共识(Paxos)+ 全局精准时钟(TrueTime)+ 两阶段提交(2PC)的组合可以在全球范围内实现外部一致性。代价是毫秒级的事务延迟——但在全球部署的场景下,这是可接受的。
对比:三种范式
| 维度 | HDFS | Cassandra | Spanner |
|---|---|---|---|
| 模型 | 文件系统 | 宽列存储 | 关系型 + SQL |
| 一致性 | 强一致(单 NameNode) | 最终一致(可调) | 外部一致性 |
| 分片 | 块(固定 128MB) | 一致性哈希(Token) | 目录分片(用户自定义) |
| 写方式 | 追加 + pipeline | 追加 MemTable + CommitLog | 追加 + Paxos 确认 |
| 容错 | 多副本 + NameNode HA | Gossip + hinted handoff | Paxos + 分片迁移 |
| 适用 | OLAP / 批处理 | 写入密集 / 时序 / IoT | 全球 OLTP / 金融 |
| 单机存储 | 文件系统 | LSM-Tree | Colossus (GFS 升级版) |
常见陷阱
HDFS NameNode 是瓶颈但无法绕开。 所有文件元数据操作走 NameNode,100 万文件的元数据存储在几十 GB 内存中。超过 1 亿文件,NameNode 内存成为瓶颈。HDFS Federation 可以缓解但没有根治。
Cassandra 的查询只能预先设计。 你无法在 Cassandra 上写"where age > 30 and name like '%张%'"这样的灵活查询。次级索引性能差到不建议使用。建表前想清楚所有查询路径。
一致性哈希的"均衡"不是自动的。 节点加入/退出后,数据迁移需要时间。这段时间内某些节点负担加重。"虚节点"可以改善但不消除问题。
分布式存储的"写后读自己"不保证读到。 即使在 Cassandra 的 QUORUM 级别下,如果你写入后立刻读取,可能因为 read repair 尚未完成而读取到旧值。需要使用一致性级别 ONE/QUORUM + 加时间戳版本对比。
通关挑战
热身:用一致性哈希实现一个简单的 key-value 分发器。支持添加/移除节点,验证节点变化时只有少量 key 迁移。用 1000 个 key 和 5 个基础节点测试,然后增加一个节点观察迁移比例。
挑战:在本地启动 Cassandra 容器,用 CQL 创建一个表模拟侦察日志存储(侦察员 ID、时间戳、坐标)。写入 10 万条记录,观察分片分布情况。
CREATE TABLE scout_logs (
scout_id text,
recorded_at timestamp,
lat double,
lng double,
report text,
PRIMARY KEY (scout_id, recorded_at)
) WITH CLUSTERING ORDER BY (recorded_at DESC);- 观察:用
nodetool ring查看 Casssandra 的 token 分布。杀死一个节点,观察 hint 和修复过程。再恢复节点,观察流式传输。
旅人笔记
分布式存储没有银弹。一致性哈希解决了"增删节点少搬家"的问题,但选择查询模式时要靠你对数据访问路径的提前设计。HDFS 是批处理的最佳搭档,Cassandra 适合写入密集的在线服务,Spanner 用极高的工程复杂度换来了全球级的外步一致性。选型时,先看清你的读写模型、一致性需求、和延迟容忍度。
下一站预告
数据存好了,分好了,副本有了。但数据不只是存储——数据需要被计算。你在 1000 台机器上存了 10PB 的日志,现在需要统计"每个前哨站每小时的警戒日志数量"。传统 SQL 在这个规模上跑不动。下一章,分布式计算上场。