一致性哈希与数据分片
数据分片的动机
随着互联网应用的快速发展,数据量和访问量呈现爆炸式增长。单机存储系统面临着两个根本性的瓶颈:存储容量和访问性能。
存储容量方面,单台服务器的磁盘空间是有限的。当数据量超过单机容量时,必须寻找横向扩展的解决方案。垂直扩展通过升级硬件配置可以在一定程度上缓解问题,但成本高昂且存在物理上限。更重要的是,单机无法无限扩展,当数据量达到TB甚至PB级别时,垂直扩展已经无法满足需求。
访问性能方面,单机的处理能力同样有限。随着用户数量和请求量的增加,CPU计算能力、内存带宽、磁盘I/O和网络带宽都会成为性能瓶颈。即使单机能够处理当前的负载,也无法保证未来业务的持续增长。
数据分片提供了一种水平扩展的解决方案。通过将数据分散到多台服务器上,每台服务器只存储部分数据并处理部分请求,从而实现存储容量和访问性能的线性扩展。这种架构具有更好的可扩展性,可以根据业务增长灵活地增加服务器节点。
朴素哈希分片
朴素哈希分片是最直观的数据分片方案。其核心思想是:对于给定的数据键key,通过哈希函数计算其哈希值,然后对节点数量N取模,确定该数据应该存储在哪个节点。
1 | |
这种方案实现简单,能够较好地均匀分布数据。在节点数量固定的情况下,可以保证相同的数据总是被路由到同一个节点。
然而,朴素哈希分片存在一个严重的问题:当节点数量发生变化时,几乎所有数据都需要重新映射。假设有N个节点,现在需要增加一个节点变成N+1个节点。根据取模运算的性质,只有hash(key) % (N+1) == hash(key) % N的数据才能保持在原节点,其余数据都需要迁移。
具体来说,当节点数量从N变为N+1时,数据需要迁移的比例约为N/(N+1)。当N较大时,这个比例接近100%。这意味着每次扩容或缩容都需要移动大量数据,不仅造成巨大的网络开销和存储开销,还会严重影响系统的可用性。
一致性哈希
一致性哈希是Karger等人在1997年提出的一种分布式哈希技术,专门用于解决节点增减时的数据迁移问题。
哈希环的概念
一致性哈希将整个哈希空间组织成一个环,通常使用2^32作为哈希空间的大小,因此哈希环的范围是[0, 2^32-1]。这个环是首尾相连的,即0和2^32-1是相邻的。
节点映射到环上
对于每个存储节点,使用哈希函数计算节点的标识符(如IP地址和端口号)的哈希值,然后将节点映射到哈希环上。假设有节点A、B、C,它们的哈希值分别为hash(A)、hash(B)、hash©,这些值在环上按顺时针方向排列。
数据的存储规则
对于每个数据项,同样使用哈希函数计算键的哈希值hash(key),然后在环上找到第一个顺时针方向遇到的节点,该节点就是该数据的存储节点。这种查找方式可以保证每个数据项都有唯一确定的存储节点。
1 | |
节点增减的影响分析
当增加一个新节点D时,计算hash(D)并将其插入到环上。这个新节点只影响那些哈希值位于新节点和其逆时针方向相邻节点之间的数据。这些数据需要从原来的节点迁移到新节点,而其他数据的位置保持不变。
同样,当删除一个节点时,只有该节点负责的数据需要重新分配,这些数据会被迁移到顺时针方向的下一个节点。
通过这种机制,一致性哈希将节点变化的影响范围限制在最小范围内。在理想情况下,增加或删除一个节点时,只有约1/N的数据需要迁移,其中N是节点总数。这极大地减少了数据迁移的开销。
1 | |
虚拟节点
虽然一致性哈希解决了节点变化时的数据迁移问题,但在实际应用中仍然存在数据倾斜的隐患。数据倾斜是指某些节点存储了远超平均水平的数据量,导致负载不均衡。
数据倾斜的主要原因是节点在哈希环上的分布可能不均匀。如果节点的哈希值过于集中,某些节点会承担过多的数据。此外,即使节点分布均匀,如果数据的哈希值分布不均匀,也会导致负载不均。
虚拟节点的解决方案
虚拟节点是解决数据倾斜问题的有效方案。其核心思想是:每个物理节点在哈希环上映射多个虚拟节点。例如,可以为每个物理节点创建100个虚拟节点,通过在物理节点标识符后添加编号来区分。
对于物理节点A,创建虚拟节点A#1、A#2、…、A#100。每个虚拟节点都有独立的哈希值,它们在环上均匀分布。数据的存储规则保持不变:计算hash(key),找到顺时针方向最近的虚拟节点,该虚拟节点对应的物理节点就是数据的存储位置。
1 | |
虚拟节点的优势
虚拟节点的主要优势在于:
- 负载均衡:通过增加虚拟节点的数量,可以使物理节点在哈希环上分布更加均匀,从而实现更好的负载均衡。
- 容错能力:当某个物理节点失效时,其虚拟节点负责的数据会分散到多个其他物理节点,而不是集中到单个节点。
- 灵活调整:可以根据物理节点的性能差异,为不同的物理节点配置不同数量的虚拟节点,实现加权负载均衡。
通常情况下,每个物理节点对应150-200个虚拟节点可以获得较好的均衡效果。
一致性哈希的实际应用
一致性哈希因其优秀的特性,被广泛应用于各种分布式系统中。
Memcached客户端
Memcached是一个高性能的分布式内存缓存系统。为了将数据均匀分布到多个Memcached服务器上,许多Memcached客户端(如Spymemcached、Xmemcached)都采用了一致性哈希算法。
客户端通常配置虚拟节点的数量来控制负载均衡的效果。当Memcached服务器集群扩容或缩容时,一致性哈希确保只有少量缓存失效,提高了缓存命中率。
Amazon Dynamo
Amazon Dynamo是Amazon的高可用键值存储系统,为Amazon的电子商务平台提供底层存储服务。Dynamo采用一致性哈希进行数据分片,并结合虚拟节点实现负载均衡和容错。
Dynamo的创新之处在于引入了"偏好列表"(preference list)的概念。每个数据项不仅存储在主节点上,还存储在顺时针方向的后续N-1个节点上,实现数据复制。当节点失效时,Dynamo可以自动将数据复制到其他节点,保证数据的可用性。
Cassandra
Cassandra是一个分布式的NoSQL数据库,继承了Dynamo的分布式架构。Cassandra使用一致性哈希进行数据分片,每个节点负责哈希环上的一个令牌范围。
Cassandra的令牌分配策略更加灵活,支持自动和手动两种方式。在自动模式下,Cassandra使用虚拟节点概念,每个节点被分配多个令牌,实现负载均衡。在手动模式下,管理员可以显式指定每个节点的令牌范围,以便根据硬件配置优化数据分布。
负载均衡器
一致性哈希也被广泛应用于负载均衡器中。在HTTP负载均衡场景中,一致性哈希可以根据请求的特征(如URL、用户ID)将请求路由到固定的后端服务器,实现会话保持(Session Persistence)。
当后端服务器数量变化时,一致性哈希确保大部分请求的路由关系保持不变,只有少量请求需要重新分配。这避免了因服务器变化导致的会话丢失,提高了用户体验。
有状态长连接的会话亲和
前述几个应用场景的共同点是:被路由的对象是一次性的请求或一份可被任意节点读写的数据。但在另一类系统中,被路由的对象是一条已经建立的长连接,或一段绑定在节点本地内存里的会话状态。这类系统也面临扩缩容与故障迁移的问题,一致性哈希同样适用,但需要回答两个新问题:已建立的本地状态如何迁移,客户端如何感知新的路由表。
把连接当作数据
WebSocket、QUIC、长轮询、TCP 自定义协议等长连接在服务端会保留一组本地状态:连接句柄、用户身份、订阅的房间或主题、未推送的消息缓冲、心跳计时器。这些状态绑定在某一台具体的服务器进程上,无法像无状态请求那样被任意节点处理。从分片的视角看,连接本身就是一种数据——只不过它的键通常是用户 ID 或设备 ID,值是连接对象与会话上下文,分片的目标是把同一个键的连接稳定地路由到同一个节点。
朴素方案是在前端负载均衡器上做随机分配,再用一张全局连接表记录“用户 X 当前在节点 N 上”,所有消息投递都先查这张表。这种方案与目录分片同病:全局表本身成为单点和瓶颈,而且每次节点变动都要扫表清理失效条目。一致性哈希提供了一条不依赖全局表的路径:路由规则就是函数,输入键即可算出节点,节点列表的微小变化不会引起全表重算。
案例:WebSocket 即时通讯网关
设想一个面向千万级在线用户的即时通讯系统。客户端通过 WebSocket 与若干台网关节点保持长连接,节点上保存 userId -> connection 的映射,用户登录、状态变更、消息收发都经过这条连接。系统有两类核心路由:上行路由决定一个用户的新连接落到哪台网关,下行路由决定一条发往某用户的消息要送到哪台网关。
朴素做法是接入层随机选节点,再把 userId -> nodeId 写入全局路由表(如 Redis)。发消息时先查表,未命中则广播询问,命中后转发到对应节点。这种做法在节点稳定时尚可工作,但每次扩容、缩容、宕机都要刷新表中大量条目,且查表本身在每条消息路径上引入了一次额外的网络往返。
一致性哈希方案把 userId 作为键映射到哈希环上的网关节点。上行路由层(通常是一层四层负载均衡或一个轻量的路由代理)以 userId 计算环上位置,将客户端引导到对应的网关。下行路由层由消息处理服务执行同样的哈希函数,直接 RPC 投递到目标网关,无需查全局表。每台网关只需对外暴露“我在哪个环位置”和一组 RPC 接口,路由表退化为节点列表加一个哈希函数。
1 | |
扩容场景下,新增一台网关后,整个用户群中只有大约 1/N 的用户的哈希区间被新节点接管。这部分用户已经建立的旧连接并不会立即被强制迁走——它们仍然挂在原节点上正常收发消息——而是采取惰性迁移策略:原节点在感知到自己不再是该键的归属节点后,在合适的时机(例如下次客户端心跳、下次空闲窗口、或一段宽限期结束)主动关闭连接,客户端因连接断开触发重连,重连请求经过新的路由规则后落到新节点。已建立的连接在迁移完成前由旧节点继续服务,已投递到旧节点的消息也由旧节点继续转发,不会出现消息丢失。
1 | |
缩容或宕机场景下,归属于退出节点的连接被迫断开,客户端重连后按新的环映射落到顺时针方向的下一个节点。如果引入了虚拟节点,单台节点退出影响的连接会分散到多个剩余节点,避免某一台节点被“接班”的连接洪峰打垮。
下行消息投递在迁移窗口内可能遇到短暂的路由不一致:消息服务按新环算出节点 A,但用户连接还挂在旧节点 B 上。常见的解决办法是在网关之间维护一份带版本号的环视图,当 A 收到不属于自己的连接的消息时,按上一个版本回退一次转发到 B,由 B 直接投递。一致性哈希的关键收益就在这里:节点变化只会让“少量键在两个候选节点之间漂移一小段时间”,而不是让全局路由表整体失效。
多人在线游戏的房间路由
大型多人在线游戏中,玩家加入的每个房间、副本或地图分片是一段强本地状态,包含场景对象、玩家位置、AI 状态、伤害计算结果。同房间内玩家的所有交互需要在同一台游戏服务器进程内完成,跨节点的状态同步成本远高于让玩家连接全部聚到同一节点。
路由键自然是 roomId 而非 playerId。哈希环上的节点是游戏服务器进程,新房间创建时按 roomId 哈希挑选节点并建立房间状态,玩家加入房间的请求由匹配服务或网关按同样的规则转发到该节点。新增服务器时,新房间会按概率落到新节点上自然填充负载,已存在的活跃房间通常继续运行到结束,而不必中途迁移——MMO 与战术竞技类游戏的房间生命周期本来就有限,与一致性哈希的惰性迁移天然契合。
实时音视频的 SFU 路由
WebRTC SFU(Selective Forwarding Unit)服务器为音视频会议做媒体流转发,每个会议室的所有参与者必须连到同一台 SFU 节点,否则跨节点转发会引入额外编码、带宽和延迟成本。路由键是会议室 ID,路由规则按一致性哈希将会议室映射到 SFU 集群中的某个节点。会议结束后状态自然释放,新建的会议室按新的环映射重新分配,扩缩容因此可以做到对正在进行的会议零干扰。
部分 SFU 实现会在哈希基础上叠加负载感知策略:当哈希挑中的节点负载已经过高时,按预定义的退让规则路由到次优节点,并把这条特殊路由记入元数据,确保后续加入者能找到同一台节点。这种“哈希为主、元数据为辅”的混合路由是有状态长连接架构的常见做法。
物联网 MQTT 集群的设备会话
物联网平台中,海量设备通过 MQTT 长连接上报数据并接收下发指令,每台 broker 节点维护设备的订阅关系与待推送消息队列。当某设备需要被下发消息时,调度服务需要找到该设备当前所在的 broker。
一致性哈希按 deviceId 把设备会话分片到 broker 集群,设备连接被前端代理路由到归属 broker,下发指令的发起方按同样规则找到 broker 后投递。EMQX、HiveMQ 等主流 MQTT 集群产品都在路由层提供了类似策略。设备掉线重连或 broker 故障后的 session takeover 也借助一致性哈希完成——新接管节点按持久化的订阅信息重建会话,已断开的连接由设备侧自动重连。
分布式 Actor 模型的实体路由
Akka Cluster Sharding、Microsoft Orleans 等分布式 Actor 框架引入了“虚拟 Actor”或“实体分片”概念。每个 actor 实例由全局唯一的实体 ID 标识,调用方仅持有实体 ID,框架在内部解析出实体当前所在的节点并完成调用。
实体到节点的映射正是一致性哈希的典型用法。框架在集群成员变化时重新计算环,被新接管的实体在第一次被访问时由新节点惰性激活,旧节点上的状态通过持久化层(Event Sourcing 或快照)恢复。这种“实体 ID 唯一、节点位置可变、状态可重建”的三层解耦让有状态服务获得了接近无状态服务的弹性。
共同模式
上述场景的架构选择可以提炼为同一个模式:把绑定到节点的本地状态视为一种特殊的数据,用一致性哈希解决“键到节点”的稳定映射,再用三个辅助机制处理一致性哈希本身无法解决的两个问题——
为什么“键到节点”这一层的迁移代价是 1/N?把节点视为均匀分布在环上的点,N 个节点把环切成 N 段,每段长度大致是环长的 1/N,对应该节点负责的键空间。新增一个节点时,它只切开包含自己哈希值的那一段,从原归属节点夺走其中一小部分,其余 N-1 段的位置与归属完全不变。
1 | |
| 维度 | 一致性哈希解决的 | 必须额外解决的 |
|---|---|---|
| 键到节点 | 节点变化时的最小重映射 | — |
| 节点到状态 | — | 旧状态如何迁移或重建(惰性迁移、持久化恢复、副本同步) |
| 客户端到节点 | — | 客户端如何感知新映射(重连、服务发现推送、网关侧透明转发) |
1 | |
辅助机制有几种通用形态:客户端在感知断连后按新路由规则重新建立连接(WebSocket IM、MQTT);新节点在第一次访问被路由的实体时按持久化数据重建本地状态(Actor 模型、SFU 重启);网关层维护带版本号的环视图,对短暂不一致的请求做一次回退转发,避免在迁移窗口里丢消息。
其中惰性迁移是整套模式的关键。它不试图“在节点切换的瞬间把状态原子搬家”,而是把迁移拆成多个低风险的小步骤,让旧节点继续服务直到客户端自己感知并重连。这套流程对几乎所有有状态长连接架构都适用:
1 | |
这种“广播新视图 → 旧节点惰性退出 → 客户端被动重连 → 新节点按需重建”的四段式流程把状态迁移从“在线搬家”问题降级为“持久层重新加载”问题,避开了分布式系统中最棘手的状态原子搬迁。代价是接受了一个有限的迁移窗口期,在窗口内允许新旧路由短暂共存。
朴素的全局路由表方案把这三个维度耦合到一张表里,每次节点变化都要同步全表;一致性哈希将三者解耦,节点列表是唯一需要全局同步的数据,其余信息按需在访问时计算或重建。这正是有状态长连接架构能扩展到千万级在线规模的核心原因。
其他分片策略
除了哈希分片,还有其他几种常见的数据分片策略,各有其适用场景。
范围分片
范围分片根据数据的键值范围将数据分配到不同的节点。例如,可以按照用户ID的范围分片:节点1负责ID为1-1000000的用户,节点2负责ID为1000001-2000000的用户,以此类推。
范围分片的优点是范围查询效率高,因为相关数据通常存储在同一个节点上。缺点是容易出现数据倾斜,某些范围内的数据量可能远超其他范围。此外,当数据分布不均匀时,可能导致某些节点负载过高。
目录分片
目录分片使用一个独立的目录服务来维护数据到节点的映射关系。当需要访问数据时,首先查询目录服务确定数据所在的节点,然后直接访问该节点。
目录分片的优势在于灵活性高,可以动态调整数据分布。缺点是目录服务成为单点故障和性能瓶颈,需要额外的元数据管理开销。此外,每次数据访问都需要先查询目录,增加了访问延迟。
哈希槽
哈希槽是Redis Cluster采用的一种分片方案。Redis Cluster将整个哈希空间划分为16384个槽位(slot),每个槽位负责一部分数据。节点通过分配槽位来承担数据存储任务。
与一致性哈希不同,哈希槽方案将槽位均匀分配到各个节点,每个节点负责的槽位数量大致相同。当需要扩容时,可以将部分槽位从一个节点迁移到另一个节点,实现平滑扩容。
哈希槽方案的优势在于数据分布可控,易于实现数据迁移和负载均衡。缺点是槽位数量固定,调整灵活性相对较低。
分片带来的挑战
数据分片虽然解决了单机系统的容量和性能瓶颈,但也引入了一系列新的挑战。
跨分片查询
当数据分散到多个节点后,涉及多个分片的查询变得复杂。例如,要查询所有年龄大于30岁的用户,如果数据按用户ID分片,则需要在所有分片上执行查询,然后合并结果。
跨分片查询不仅增加了网络开销,还可能导致结果排序和分页困难。为了优化跨分片查询,可以采用以下策略:
- 异步并行查询:同时在多个分片上执行查询,并行获取结果。
- 结果合并优化:在合并结果时进行必要的排序和过滤。
- 二级索引:为常用查询条件建立全局索引,但需要维护额外的索引数据。
分布式事务
在分片环境下,事务处理变得复杂。当事务涉及多个分片时,需要保证这些分片上的操作要么全部成功,要么全部失败。传统的ACID事务模型难以直接应用到分布式环境。
分布式事务的解决方案包括:
- 两阶段提交(2PC):通过协调者和参与者确保所有节点达成一致,但存在阻塞和单点故障问题。
- 三阶段提交(3PC):在2PC基础上增加预准备阶段,降低阻塞概率,但实现复杂。
- Saga模式:将长事务拆分为多个本地事务,每个本地事务都有对应的补偿操作。
- TCC模式:Try-Confirm-Cancel模式,通过预留资源、确认操作和取消操作保证最终一致性。
数据再平衡
随着数据的增长和访问模式的变化,可能需要重新分配数据以保持负载均衡。数据再平衡是一个复杂的过程,需要考虑以下因素:
- 最小化数据迁移:只迁移必要的数据,减少网络和存储开销。
- 在线迁移:在迁移过程中保持服务可用,避免影响业务。
- 一致性保证:确保迁移过程中数据的一致性,避免数据丢失或重复。
- 渐进式迁移:逐步迁移数据,避免短时间内大量数据迁移影响系统性能。
热点问题
热点是指某些数据项的访问频率远高于平均水平。在分片环境下,热点可能导致某些节点负载过高,成为性能瓶颈。
解决热点问题的策略包括:
- 热点检测:监控数据访问模式,识别热点数据。
- 热点分裂:将热点数据复制到多个节点,分散访问压力。
- 缓存优化:在热点数据前面增加缓存层,减少对后端节点的访问。
- 动态分片:根据访问模式动态调整分片策略,将热点数据分散到多个节点。
分片与复制
分片和复制是分布式系统中两个重要的概念,它们解决不同的问题,但在实际系统中通常需要结合使用。
分片的作用
分片的主要作用是解决容量和性能问题。通过将数据分散到多个节点,分片可以实现:
- 水平扩展:通过增加节点线性扩展存储容量和处理能力。
- 负载均衡:将访问请求分散到多个节点,提高系统吞吐量。
- 地理分布:将数据分布到不同地域,降低访问延迟。
分片关注的是如何将数据分散到多个节点,每个节点只存储部分数据。
复制的作用
复制的主要作用是解决可用性和数据持久性问题。通过在多个节点上保存相同的数据副本,复制可以实现:
- 高可用性:当某个节点失效时,可以从其他节点获取数据。
- 数据安全:多副本存储降低了数据丢失的风险。
- 读取性能:可以从多个副本并行读取,提高读取性能。
复制关注的是如何将相同的数据保存到多个节点,每个节点存储完整的数据副本。
分片与复制的结合
在实际系统中,分片和复制通常结合使用。每个分片都有多个副本,这些副本分布在不同节点上。这种架构既提供了水平扩展的能力,又保证了高可用性和数据安全。
例如,一个包含N个分片、每个分片有M个副本的系统,需要N×M个节点。当某个节点失效时,系统可以从其他节点获取数据。当需要扩容时,可以增加新的分片或增加现有分片的副本数量。
分片和复制的结合需要考虑数据一致性、副本分布、故障恢复等多个方面的问题,是分布式系统设计中的核心挑战。
总结
一致性哈希是一种优雅的数据分片解决方案,通过将节点和数据映射到哈希环上,实现了节点变化时的最小化数据迁移。虚拟节点进一步解决了数据倾斜问题,使负载分布更加均匀。
除了哈希分片,范围分片、目录分片和哈希槽等策略各有其适用场景。在实际应用中,需要根据业务需求选择合适的分片策略。
数据分片虽然带来了扩展性优势,但也引入了跨分片查询、分布式事务、数据再平衡和热点问题等挑战。这些挑战需要通过合理的设计和优化来解决。
分片解决容量问题,复制解决可用性问题,两者结合可以构建高性能、高可用的分布式系统。随着云计算和大数据技术的发展,数据分片技术将继续在分布式系统中发挥重要作用。




