Kademlia是一种分布式哈希表(DHT),是第三代对等网络的节点动态管理和路由协议。
构建网络拓扑
Kad网络中的每个节点都会被分配唯一的节点ID,一般是160bit的二进制数。节点之间可以计算距离,节点距离以节点ID的XOR值度量:
因此,节点之间的距离越近,意味着节点ID的公共前缀越长。节点之间的距离以节点的最长公共前缀(cpl)为度量,cpl越大,表示两个节点越接近,例如节点
基于此,一个完整的网络空间可以被表示成为一颗如下图所示的二叉树,树的叶子节点代表网络节点,下图演示了使用3bit作为节点ID位数的节点树结构。
下图展示了从节点 [公式] 视角来分割上面的网络树的结果:
- 节点<A, B, C, D>与M的公共前缀长度为0,将其归为一个单元
- 节点<F, G>与M的公共前缀长度为1,将其归为单元2
- 节点与M的公共前缀长度为2,将其归为单元3
需要说明的是:距离越长,代表节点之间越接近,千万不要弄反了。而且可以总结发现,从任一节点来看,与其距离为0的节点占据网络节点总数的1/2,距离为1的节点占据网络节点总数的1/4,
构建路由表
假如当前节点ID为 M ,X 距离 M上维护的节点Y的距离为:
其中:
这个证明也很简单:
于是:上面的问题就转化为:
当 M 收到询问距离 X 更近的节点请求时, M 首先计算自身距离目标节点的距离
然后再从自己维护的节点列表中选择出距离M为的d1的所有节点(翻译一下:即从M的路由表中找到与有最长公共前缀的所有节点)。
Kademlia协议中,每个节点按照与自己的距离来切割节点网络树:被切割的子树称之为 Bucket。整个路由表本质上便是一个Bucket数组,Kademlia协议以聚类网络节点:每个Bucket 中的节点必然与本节点具有相同的最长公共前缀。
由于节点只有160bit,最长公共前缀长度最大只有160,因此,路由表中的Bucket 数量最多也就160。但是每个 Bucket 内节点数量可能会非常多,根据之前的计算,与节点最长公共前缀长度为0的内节点数占据网络总节点数量的1/2,内节点数占网络总节点数的1/4…
Kademlia协议对每个Bucket 内维护的节点数设置了一个上限,称之为K值,在一般的实现中 。一旦 Bucket 内节点数超过,便根据一定的淘汰算法进行更新。
根据该基本原理,节点构建的路由表如下图所示:
分裂
在一些实现Kademlia协议实现中,每个节点初始时只有一个Bucket ,感知到网络上有节点时,直接将远程节点信息添加至该,直到该内节点数量超过,此时开始分裂 Bucket 。
所谓分裂是指创建一个新的 Bucket ,然后将原来 Bucket 内的部分节点迁移至新 Bucket 。因为原 Bucket 内的节点与本节点的距离不尽相同,所以,迁移的原则是:将与本地节点更近(即更大)节点迁移至新建 Bucket ,迁移完成后再判断新建 Bucket 内节点数是否超过限制,如果是,继续对该新建 Bucket 进行分裂。
上面提到迁移的过程中会将部分节点迁移至新 Bucket ,那么如何选择这些需要被迁移的节点呢?答案是根据内节点与本节点之间的cpl决定:
初始状态时,本地只有1个,此时分裂的目标是:
newBucket := bucket.Split(len(rt.Buckets)-1, rt.local)
在原 Bucket 中保留与本节点为0(无任何公共前缀的节点),将其他节点迁移至新 Bucket 中。
一次分裂后,第一个 Bucket 中保留的全部是与当前节点无任何公共前缀的节点,第二个 Bucket 中保留的全部是与当前节点公共前缀大于等于1的节点。
接下来判断第二个 Bucket 是否需要再次分裂,如果分裂,再次创建新 Bucket ,然后将第二个 Bucket 中与本地节点公共前缀超过1的节点迁移至新 Bucket ,与本地节点公共前缀长度为1的节点依然保留在第二个 Bucket 中。
路由算法
路由算法要解决的是如何根据目标ID找到地址或者找到与该ID最节点的目标节点地址。
在一个对等网络中,某个节点要查询其他节点的信息时,它可依赖的信息只有两个:
- 目标节点ID;
- 当前节点维护的路由表;
其查询的核心思想是:逐步迭代,递近查找。其基本过程如下:
路由表更新
Kademlia网络中节点是动态变化的,节点可新接入网络,也可从网络离线。这也意味着每个节点的路由表也是一直变化着的。
新节点上线
流程:
节点离线
节点离线在Kademlia协议中无需做特殊处理,如果某个节点离线,那么其离线事件最终会反馈到网络节点的路由表中,将其从路由表中剔除即可,相比于Chord协议有了极大的简化。
用Kademlia网络存储对象
使用Kademlia网络构建大规模分布式存储系统,需要解决以下两个核心问题:
-
建立对象与网络节点之间的映射
-
节点动态变化时保证对象的可访问
对象与节点映射
建立对象与节点的映射,一般有两种方法:
-
查表:维护全局<对象,节点>映射表
-
计算:直接根据对象特征,通过数学运算得到目标节点
方法1需要维护庞大的全局映射表,且其很明显会成为系统瓶颈,且违背了对等网络的原则。
方法2必须将对象映射至节点空间,即将对象根据其唯一特征计算160bit的指纹,根据该指纹找到网络中与其指纹最接近的个节点,这些节点将成为对象的最终存储目的地。一般这个指纹会选取对象内容的hash,便于对象去重和对象的唯一性保证。而之所以选择个节点存储对象是为了提高对象数据的可靠性。