Swift 中 Partition的奥秘--一致性哈希
用最通俗的方式解释 Swift 的 Partition 设计以及它如何减少数据迁移。
核心比喻:大型图书馆的固定书架系统
想象一下,Swift 集群就像一个巨大的图书馆,里面有很多书架(存储节点),书上放着无数本书(对象)。
- 问题:如果图书馆新买了一个书架,或者一个旧书架坏了要搬走,管理员就需要把很多书重新摆放,让所有书架上的书数量差不多(负载均衡)。这个过程(数据迁移)非常累人(消耗资源),而且在此期间图书馆可能无法正常借阅(影响服务)。
笨办法:直接根据书名分配书架(无分区)
- 管理员规定:书名的第一个字母是
A-M的,放在 1号区;N-Z的放在 2号区。 - 新增一个3号区时,管理员需要把 1号区里
G-Z的书和 2号区里所有的书都翻出来,重新分配一遍。工作量巨大!
Swift 的聪明办法:引入“固定分区”的概念
Swift 的设计者非常聪明,他们想出了一个办法:引入一个永远不会改变的中间层——固定分区(Partition)。
-
建造固定数量的“书槽”(Partition)
- 在图书馆建馆之初,就先规划好,把整个图书馆划分成 10000 个大小固定的、带编号的**“书槽”**(Partition)。比如 1号槽、2号槽…10000号槽。
- 关键:一旦建好,这10000个书槽的数量就永远不变了! 不管以后图书馆新增或减少多少书架,书槽的数量都不变。
-
分配书籍到书槽(对象 -> Partition)
- 每本书进来,管理员都用一個固定的算法(比如哈希函数)根据书名(对象名) 算出一个数字,这本书就永远放在这个数字对应的书槽里。
- 例如,《三体》通过计算永远属于
第1234号书槽。 - 因为书槽总数不变,算法不变,所以任何一本书和书槽的关系是永久不变的。
-
分配书槽到书架(Partition -> 节点)
- 现在,管理员只需要决定哪几个书槽放在哪个书架上就行了。
- 一开始,有 10 个书架。管理员可以决定:1号书架放 1-1000号书槽,2号书架放 1001-2000号书槽,以此类推。
magic 时刻:如何减少迁移量?
现在,图书馆要新增一个第11号书架。
- 旧方法(无分区):可能需要重新整理一半的书。
- 新方法(有分区):管理员的活儿变得非常简单:
- 他只需要从之前每个书架上,都拿出少量几个“书槽”(比如从1号书架拿最后50个书槽:951-1000号)。
- 把这几个完整的书槽(连同里面的所有书)整体搬迁到新的11号书架上。
- 更新一下“账本”(映射表),记录现在951-1000号书槽放在11号书架了。
结果:
- 迁移量极小:只需要移动少量几个书槽,而不是移动大量零散的书。书槽是整体搬迁的,效率极高。
- 影响范围小:只有被移动的那些书槽里的书暂时不可借阅,其他绝大多数书完全不受影响。
- 操作简单:管理员的工作变得非常有规划,非常轻松。
减少节点(搬走一个书架) 的过程完全一样,只是反过来操作:把这个书架上的所有书槽,平均地分配给其他剩余的书架。
总结:为什么能减少迁移?
| 概念 | 比喻 | 作用 |
|---|---|---|
| 对象 (Object) | 书 | 要存储的数据本身。 |
| 分区 (Partition) | 书槽 | 一个固定的中间层。它的数量不变,像一个个容器,把大量零散的对象分组装起来。 |
| 节点 (Node) | 书架 | 真正的物理存储设备,可以动态增减。 |
核心思想:
通过引入一个固定不变的中间层(Partition),将“对象”和“节点”的动态映射关系解耦。
- 对象 -> Partition 的映射是静态的、永久的(因为分区数量不变)。
- Partition -> 节点 的映射是动态的、可调整的。
当增删节点时,你不需要动“对象”本身,只需要调整“Partition”和“节点”之间的映射关系,并以Partition为单位进行数据迁移。这就像搬家用打包好的箱子(Partition)而不是零散物品(Object),效率自然大大提高。
为了更加具体,下面用一个完整的例子再次进行举例说明
场景设定
- 对象: 1,000,000 个文件(文件1, 文件2, …, 文件1000000)。
- 初始集群: 3 个存储节点(节点A, 节点B, 节点C)。
- 分区: 我们将整个集群划分为 1000 个固定的分区(Partition 0 到 Partition 999)。这是集群创建时就定好的,永不改变。
第一步:分配文件到分区(对象 -> Partition)
我们用一个简单的哈希函数来决定文件属于哪个分区:
分区号 = hash(文件名) % 1000
hash(文件1) % 1000-> 假设结果是 12。那么文件1就永远属于 分区12。hash(文件2) % 1000-> 假设结果是 785。那么文件2就永远属于 分区785。- … 以此类推,100万个文件被均匀地散列到1000个分区中。平均每个分区有1000个文件。
第二步:分配分区到节点(Partition -> 节点)
初始状态下,我们将1000个分区大致平均地分配给3个节点:
- 节点A: 负责托管 分区 0 - 332 (共333个分区)
- 节点B: 负责托管 分区 333 - 665 (共333个分区)
- 节点C: 负责托管 分区 666 - 999 (共334个分区)
此时,每个节点存储着大约 333,000 个文件。
第三步:扩容 - 新增一个节点D
现在,集群压力变大,我们需要加入**第四个节点(节点D)**来分担负载。
目标:将负载从原有的3个节点上重新均衡到4个节点上。
做法:不是重新计算所有文件的位置,而是重新分配分区。
-
重新规划分区映射:
- 现在有4个节点了,我们希望每个节点负责 1000 / 4 = 250个分区。
- 新的映射规划可以是:
- 节点A: 分区 0 - 249 (250个分区) 【从A身上拿走83个分区】
- 节点B: 分区 250 - 499 (250个分区) 【从B身上拿走83个分区】
- 节点C: 分区 500 - 749 (250个分区) 【从C身上拿走84个分区】
- 新节点D: 分区 750 - 999 (250个分区) 【接手从ABC身上拿走的共250个分区】
-
数据迁移:
- 系统通知节点A:“请你将 分区250-332(共83个分区)的所有文件传输给节点D。”
- 系统通知节点B:“请你将 分区500-665(共83个分区?这里应为333-499?我们重新规划一下)”
- (让我们修正一下以确保清晰)
- 实际上,迁移是针对需要改变映射的分区。例如,原本由节点C负责的分区750-999(共250个分区),现在被划归新节点D负责。
- 因此,需要迁移的数据是且仅是:分区750-999上的所有文件。
- 节点C开始将这250个分区(包含约 250,000 个文件)拷贝到节点D。
-
更新环(Ring): 集群更新它的“地图”(称为Ring),通知所有组件:“从现在起,分区750-999由节点D负责”。
-
迁移完成:
- 迁移完成后,节点C释放了250个分区的空间。
- 每个节点现在负责250个分区,存储着约250,000个文件,完美均衡。
关键分析:迁移量有多大的减少?
-
如果没有分区(笨办法):
新增一个节点(容量增加33%),需要重新计算所有100万个文件的位置,并将大约 250,000 个文件(100万 / 4)随机地从ABC节点抽取出来,迁移到D节点。这是一个全局的、混乱的、低效的过程。 -
有分区(Swift的聪明办法):
迁移不再是基于单个文件,而是基于分区。我们只移动了 250个分区(1000 / 4)。
虽然这250个分区里也正好包含了约250,000个文件,但迁移的粒度完全不同:- 效率极高:系统只需要处理250个“搬家指令”(移动分区X到节点Y),而不是处理250,000个“搬家指令”(移动文件A到节点Y)。管理开销大大降低。
- 性能优化:可以对一个分区内的所有文件进行批量、顺序的读写操作,速度远快于随机地挑选和移动单个文件。
- 影响最小:在迁移过程中,只有涉及到的分区(750-999)会暂时有读写影响,其他750个分区上的服务完全不受干扰。
- 确定性:非常清楚哪些数据需要移动,移动多少。而不是一个模糊的“大概要移动25%的数据”。
结论
通过引入固定分区这个中间层,Swift 将数据的组织单位从海量的、零散的单个文件,提升为数量固定的、成组的分区。
当集群扩容或缩容时,数据再平衡的操作单元从“文件”变成了“分区”。虽然移动的分区可能包含同样数量的文件,但前者是“撒芝麻”,后者是“搬箱子”。搬箱子的策略在规划、执行和效率上,远远优于撒芝麻。
这就是为什么说 Partition 机制能极大地减少数据迁移带来的开销和复杂性,其实归根到底就是将原本散落的哈希聚集到了固定区域,从而可以更高效的移动多个文件。
更多推荐
所有评论(0)