设计分布式 KV 存储:一致性哈希与副本
设计 Key-Value 存储:从单机到分布式
当数据量超出单台服务器的承载能力时,我们需要将数据分散到多台机器上。Key-Value 存储作为最简单的数据模型,天然适合分布式扩展。本文将围绕 一致性哈希 与 副本机制 两个核心概念,手把手教你设计一个可横向扩展的分布式 KV 系统。
为什么需要分布式 KV 存储?
单机限制:
- 内存/磁盘容量有限
- 单点故障导致服务不可用
- 读写吞吐量存在物理瓶颈
分布式带来的优势:
- 容量可横向扩展
- 通过副本实现高可用
- 负载被分摊到多个节点
典型的应用场景包括:缓存系统(Redis Cluster)、NoSQL 数据库(DynamoDB、Cassandra)、配置中心等。
从取模哈希到一致性哈希
传统哈希分区的问题
最简单的分区方式:
node_index = hash(key) % N
其中 N 是节点数量。
问题演示:
假设有 3 个节点,节点 1 宕机后 N 变为 2,几乎所有键的哈希取模结果都会发生变化。这意味着需要迁移 绝大多数数据,引发雪崩式重分配。
一致性哈希的核心思想
将哈希值空间组织成一个 固定大小的环(例如 0 ~ 2^32-1),节点和数据都映射到环上的位置。
- 节点的映射:对节点 IP 或 ID 计算哈希,将其放置在环上。
- 数据的映射:对 Key 计算哈希,沿环顺时针寻找第一个节点,该节点即为存储位置。
当节点加入或离开时,只有相邻节点之间的数据需要迁移,其他数据不受影响。
环上示例:
节点 A 在 100,节点 B 在 300,节点 C 在 600
键 k1 哈希为 150 → 顺时针下一个节点 B,存放在 B
键 k2 哈希为 650 → 顺时针越过 0 点,下一个节点 A,存放在 A
若节点 B 故障:
- k1 将重新分配到 C(B 后面的第一个节点)
- 其它键的映射保持不变
- 仅 A~B 和 B~C 之间的数据需要迁移
虚拟节点:解决负载不均问题
物理节点的倾斜
如果节点数量较少且哈希函数不能完美均匀分布,可能出现某些节点负责范围过大(热点),而另一些节点几乎空闲。
引入虚拟节点(Virtual Nodes)
每个物理节点对应多个虚拟节点(例如 150 个),这些虚拟节点散布在哈希环上。物理节点 A 拥有的虚拟节点越多,它占据的环空间比例越大。
优点:
- 即便物理节点数量少,环也被切成更多小段,数据分布更均匀。
- 节点之间性能不同时,可为高性能节点分配更多虚拟节点(加权)。
虚拟节点到物理节点的映射: 维护一张对照表,例如:
- vnode_1 → Node A
- vnode_2 → Node B
- vnode_3 → Node A
- ...
所有虚拟节点参与哈希环,最后路由到实际物理节点。
副本机制:高可用与数据安全
在分布式系统中,节点故障是常态。为了保证数据不丢失,每个键值对需要在多个节点上保存副本。
复制策略
常见做法:将数据副本写入哈希环上顺时针方向的 连续 N 个物理节点(N 为副本因子,通常为 3)。
需要注意的是,连续节点不能包含同一个物理节点。如果连续虚拟节点恰好属于同一台机器,需要跳过并选择下一个不同物理节点的虚拟节点。
例如(R = 3):
- 数据首先落在虚拟节点 v1(节点 A)
- 继续顺时针找到 v2(节点 B)
- 继续找到 v3(节点 C) 即使环上出现 v4(节点 A),也会跳过,保证副本分布在不同的物理节点。
副本放置的变体
- 机架/数据中心感知:为了容忍机架或区域故障,可以让副本分布在不同机架或可用区。
- Hinted Handoff:当某个副本节点临时不可用时,临时将数据写在另一个节点,待原节点恢复后回传数据(常见于 Dynamo 风格系统)。
节点变更时的数据迁移
加入新节点
新节点加入时,其虚拟节点插入环中,将从原有节点手中“夺走”一部分数据区间。只有落在新区间内的 Key 需要从原宿主节点迁移到新节点。
迁移过程可以增量进行,不影响正常读写。系统通常采用 异步复制 完成同步,并在迁移期间保留原节点数据直到确认传输成功。
节点离开(故障或计划下线)
- 计划下线:管理员主动发起数据迁移,将节点上的数据均衡转移到其它节点,平滑退出。
- 故障宕机:通过副本保证可用性。系统会检测心跳,某个节点失效后,其负责的区间由其他副本暂时接管,同时对失效区间进行重新复制到新的节点,以恢复指定的副本数。
读写流程与一致性模型
协调者(Coordinator)角色
客户端可以向集群中任意节点发送请求,接收到请求的节点充当协调者,计算数据所在的首选节点,并协调读写操作。
写入流程(以 R=3,W=2 为例)
- 协调者根据 Key 哈希找到首选节点,并发向所有副本节点发送写入请求。
- 等待至少 W 个节点确认成功后,向客户端返回成功(W 为写仲裁数)。
- 常见的配置:R + W > N 可实现强一致性(N 为副本总数)。
读取流程(R=2)
- 协调者向副本节点发起读取请求。
- 等待至少 R 个节点返回后,选取最新版本返回(根据时间戳或向量时钟)。
- 并异步修复落后副本。
处理冲突和最终一致性
在分布式环境中,网络分区和并发写入可能导致同一数据的不同副本出现多个版本。
冲突检测与解决:
- 最后写入获胜(LWW):基于时间戳,后到的覆盖。简单但可能丢失数据。
- 向量时钟(Vector Clocks):记录每个节点的逻辑时钟,可以检测并发冲突。当发现冲突后,可以交由客户端或应用层合并。
许多系统采用读修复(Read Repair)和反熵(Anti-Entropy)机制在后台同步副本差异,逐步达到一致性。
架构总览与关键组件
一个完整的分布式 KV 存储通常包含:
- 分区组件:一致性哈希环,虚拟节点管理。
- 成员管理与故障检测:基于 Gossip 协议的心跳,维护集群成员列表,检测节点加入/离开。
- 副本与一致性管理器:处理读写仲裁、提示移交、读修复。
- 存储引擎:每个节点内部的本地存储(如 LSM-Tree、B+ Tree、内存跳表)。
- 客户端路由:为客户端提供初始环信息,使其可以直接找到目标节点,避免每次经过额外跳转。
一个最小化实现的步骤
如果你想动手实现一个教学级别的分布式 KV,可以按以下步骤迭代:
- 单机 Mmap 存储:实现基本的 put/get/delete。
- 一致性哈希分片:引入多节点,每个节点只负责一部分数据。
- 虚拟节点支持:增加节点权重和均匀分布。
- 副本写入:写入时同时复制到后继 2 个节点。
- 读取与仲裁:带 R 和 W 配置的读写。
- 节点加入/离开处理:数据迁移的简单逻辑。
- 故障检测:心跳与临时故障转移。
尽量保持接口简单,例如只支持 GET(key) 和 PUT(key, value),将分布式复杂性隐藏在节点间通信中。
总结
设计一个分布式 Key-Value 存储,一致性哈希 负责决定数据被放在哪台机器上,并让扩容和缩容的迁移量最小化;副本机制 则提供了高可用和容错能力。通过理解哈希环、虚拟节点、复制因子、读写仲裁,你已经掌握了绝大多数现代分布式数据库的基础骨架。
实际工程中还有大量细节,如垃圾回收、数据压缩、流控、多数据中心复制等,但掌握上述核心思想后,你就拥有了进一步深入研究的脚手架。
本教程为“免费在线教程”系列的一部分,持续更新技术架构设计内容。