Kademlia算法,一种分布式哈希表(DHT)的实现

DHT简介

哈希表可看作一个kv数据库,分布式哈希表就是不同的kv在不同的机器上。下文称存储部分kv的一台机器为一个节点。

对于一个DHT,最基础的是要提供两个功能:1、存储一个kv;2、输入一个key,返回对应的value。对于第2个功能,最重要的是实现输入一个key,返回哪些节点拥有对应的value的功能,这样客户端就可以通过连接这些节点来获取value。这可以通过一个中心化的索引实现,但是本文讨论的是去中心化的实现方式,必须通过访问数个节点来获取这些信息。为了实现第2个功能,在实现第1个功能时,需要按照一定规则来决定一个kv需要存在哪些节点,这样查询时就可以应用同样规则获取这些节点。除了这两个功能外,还要考虑节点如何加入DHT,以及如何处理节点挂了等问题。

Kademlia算法

Kademlia算法为DHT的一种实现,应用于IPFS、eMule、BitTorrent等。下面我们按照上文提到的DHT功能来介绍Kademlia算法。

如何决定一个kv存在哪些节点

一个简单的思路是,把节点也映射到key的值域上,即节点也通过一定哈希规则算出一个哈希值,称为node id,值域与key的值域相同,然后规定每个kv存储在node id=key的节点上,这样查询时就能直接得到node id,再查询这个node id对应的节点信息即可。这个方法有两个问题,一个是节点数一般不会覆盖整个key的值域,node id=k可能不存在,另一个是一个value只发给一个节点不安全,一旦节点挂了这个value就找不到了。于是我们稍作改进,引入距离的概念,每个value发给node id离key最近的k个节点,这样问题就解决了。Kademlia算法就是这么干的。

输入一个key,如何获取value

根据上面的讨论,我们只需要解决输入一个key,如何找到node id离key最近的k个节点及其节点信息(ip、port)就行了。由于这个操作是通过查询若干节点完成的,由此看到一个节点除了存储一些value,也需要存储一些其他节点的节点信息。Kademlia算法也不要求一个节点存储全局节点信息,只需要存储部分就可以了。

有了节点信息之后怎么办呢?我们引入一个条件。

条件1: 对于任意节点a与任意key j,如果存在节点b使得b与j的距离小于a与j的距离,那么一定存在一个节点c,使得a存储了c的节点信息,并且c与j的距离小于a与j的距离。

如果距离的值域为自然数,且所有节点满足条件1,当一个节点a收到一个输入key j时,我们可以想到一个办法:找出a自身存储的距离j最近的k个节点及节点信息,选取α个,问它们它们存储的,距离j最近的且距离小于它们自身到j的距离的k个节点分别是谁,地址是哪里,节点a收集结果后再选出现在知道的距离j最近的k个节点,选取没问过的α个,继续问它们,重复这个过程,直到所有前k个节点都问过了。由于条件1的存在,算法会结束,并且结果保证全局最优,证明略。Kademlia算法就是这么干的。

那么如何满足条件1呢?Kademlia算法提出一个有意思的方法。Kademlia算法使用的key为有穷整数。首先Kademlia算法找了一个很有意思的距离函数 \oplus ,即位运算中的异或,两key a, b的距离为 a b a \oplus b 。我们可以把距离的值域按2的幂分成多份: { 0 } , { 2 0 } , [ 2 1 , 2 2 ) , , [ 2 i , 2 i + 1 ) , \{0\}, \{2^0\}, [2^1,2^2),\dots, [2^i,2^{i+1}), \dots Kademlia算法认为每个节点在上述每个距离范围内都要有存储的节点,而且要有k个,不足k个的有多少存多少。这样就满足条件1了。为什么呢?因为异或有一个特别的性质。

性质1: 假设节点a与key j的距离落在区间 [ 2 i , 2 i + 1 ) [2^i,2^{i+1}) ,那么对于任意与a的距离落在区间 [ 2 i , 2 i + 1 ) [2^i,2^{i+1}) 的节点c,都有c与j的距离小于a与j的距离。

证明: 为表述方便,我们用a,j与c代表相应节点或key对应的哈希值,用 a i a_i 表示a写成二进制数的第i位(最低位为 a 0 a_0 ),即 a i a / 2 i m o d 2 a_i\equiv a/2^i \mod 2 。设 a j [ 2 i , 2 i + 1 ) a\oplus j \in [2^i,2^{i+1}) ,且 a c [ 2 i , 2 i + 1 ) a\oplus c \in [2^i,2^{i+1}) 。 可得对任意 l > i l>i ,有 a l j l = a l c l = 0 a_l\oplus j_l=a_l\oplus c_l=0 ,且 a i j i = a i c i = 1 a_i\oplus j_i=a_i\oplus c_i=1 。 根据异或的性质: x y = x z y z = 0 x\oplus y = x\oplus z \Rightarrow y\oplus z=0 ,我们有 对任意 l i l\geq i c l j l = 0 c_l\oplus j_l=0 ,即 c j < 2 i a j c\oplus j<2^i\leq a\oplus j 。得证。

性质1除了能证明条件1外,还能得出算法的时间复杂度。因为 c j < 2 i c\oplus j<2^i c j c\oplus j 最多属于 [ 2 i 1 , 2 i ) [2^{i-1},2^{i}) 区间,最少比 a j [ 2 i , 2 i + 1 ) a\oplus j \in [2^i,2^{i+1}) 左移了一个区间。因此当α与k均为1时,一次请求最多需要询问log N个节点,N为key的最大值。

新节点如何加入DHT

新节点a想要加入一个DHT,首先需要通过外部方法获得DHT中的一个节点b的节点信息,然后计算自己的node id,然后向b询问离a最近的k个节点。Kademlia算法规定当收到其他节点的信息时,会尝试存储这个节点的节点信息,当该节点所在的距离区间内的节点数少于k个时,一定要存下来,当节点数大于等于k个时,节点会ping一下最久没见过的那个节点,如果ping不通,则换成这次请求的节点,如果ping得通,则不存这次的节点。因此a的这次询问既收集到一些节点信息,又向一些节点宣告了自己的存在。一次询问很可能达不到每个区间存k个节点的要求,哪个区间达不到要求,就随机生成一个这个区间的值,并且询问离这个值最近的k个节点,直到满足要求。

我们称上述“一个节点在每个距离范围内都要有存储的节点,而且要有k个,不足k个的有多少存多少”为条件2。那么上面的过程能否维护条件2呢?即下面命题1是否成立?

命题1: 假设新节点a加入DHT前,DHT中的每个节点均满足条件2,而且节点a通过上述方法加入DHT。那么对于任意DHT中的其他节点b,如果a与b的距离 [ 2 i , 2 i + 1 ) \in [2^i,2^{i+1}) ,且在a加入前,b存储的这个范围内的节点数小于k,那么在a加入后,b一定会存储a。

答案是否定的。可能存在节点b查不到节点a的情况。但是作者说取个大点的k和让node id均匀分布会让这个概率很小。

新加入的节点是没有value的,为了维护kv存储规则,Kademlia算法规定每个节点要定时把自身存储的kv发给离key最近的k个节点要求它们存下。理论上,新节点a会存下a在离key最近的k个节点之中的所有kv。原离key最近的第k个节点所存的value将会变成浪费。a的加入仅会影响部分kv的分布。

如何处理节点挂了

如上文所述,节点删除一个挂掉节点的节点信息是在收到其他节点的信息时进行的,这可能不及时。同时新节点可能还没存储到需要存的kv,因此距离key最近的k个节点中并不保证每个节点都有这个kv。但是Kademlia算法认为只要k个节点中有一个节点有这个value就能成功获得这个value,所以k能提供一定的容错。

参考文献

本文简要介绍了Kademlia算法,还有很多细节都没有介绍,大家可以看看论文或其他资料。

Petar Maymounkov, David Mazières: Kademlia: A Peer-to-Peer Information System Based on the XOR Metric. IPTPS 2002: 53-65 en.wikipedia.org/wiki/Kademl… www.yeolar.com/note/2010/0…

猜你喜欢

转载自juejin.im/post/7119009232006938631