设计分布式 KV 存储:一致性哈希与副本

FreeGuideOnline 最新 2026-06-19

设计 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 存储通常包含:

  1. 分区组件:一致性哈希环,虚拟节点管理。
  2. 成员管理与故障检测:基于 Gossip 协议的心跳,维护集群成员列表,检测节点加入/离开。
  3. 副本与一致性管理器:处理读写仲裁、提示移交、读修复。
  4. 存储引擎:每个节点内部的本地存储(如 LSM-Tree、B+ Tree、内存跳表)。
  5. 客户端路由:为客户端提供初始环信息,使其可以直接找到目标节点,避免每次经过额外跳转。

一个最小化实现的步骤

如果你想动手实现一个教学级别的分布式 KV,可以按以下步骤迭代:

  1. 单机 Mmap 存储:实现基本的 put/get/delete。
  2. 一致性哈希分片:引入多节点,每个节点只负责一部分数据。
  3. 虚拟节点支持:增加节点权重和均匀分布。
  4. 副本写入:写入时同时复制到后继 2 个节点。
  5. 读取与仲裁:带 R 和 W 配置的读写。
  6. 节点加入/离开处理:数据迁移的简单逻辑。
  7. 故障检测:心跳与临时故障转移。

尽量保持接口简单,例如只支持 GET(key)PUT(key, value),将分布式复杂性隐藏在节点间通信中。


总结

设计一个分布式 Key-Value 存储,一致性哈希 负责决定数据被放在哪台机器上,并让扩容和缩容的迁移量最小化;副本机制 则提供了高可用和容错能力。通过理解哈希环、虚拟节点、复制因子、读写仲裁,你已经掌握了绝大多数现代分布式数据库的基础骨架。

实际工程中还有大量细节,如垃圾回收、数据压缩、流控、多数据中心复制等,但掌握上述核心思想后,你就拥有了进一步深入研究的脚手架。


本教程为“免费在线教程”系列的一部分,持续更新技术架构设计内容。