前两天在网上看到世界知名的电骡服务器Razorback 2被查封、4人被拘禁的消息,深感当前做eMule / BitTorrent等P2P文件交换软件的不易。以分布式哈希表方式(DHT,Distributed Hash Table)来代替集中索引服务器可以说是目前可以预见到的为数不多的P2P软件发展趋势之一,比较典型的方案主要包括:CAN、CHORD、Tapestry、Pastry、Kademlia和Viceroy等,而Kademlia协议则是其中应用最为广泛、原理和实现最为实用、简洁的一种,当前主流的P2P软件无一例外地采用了它作为自己的辅助检索协议,如eMule、Bitcomet、Bitspirit和Azureus等。鉴于Kademlia日益增长的强大影响力,今天特地在blog里写下这篇小文,算是对其相关知识系统的总结。


1. Kademlia简述

Kademlia(简称Kad)属于一种典型的结构化P2P覆盖网络(Structured P2P Overlay Network),以分布式的应用层全网方式来进行信息的存储和检索是其尝试解决的主要问题。在Kademlia网络中,所有信息均以的哈希表条目形式加以存储,这些条目被分散地存储在各个节点上,从而以全网方式构成一张巨大的分布式哈希表。我们可以形象地把这张哈希大表看成是一本字典:只要知道了信息索引的key,我们便可以通过Kademlia协议来查询其所对应的value信息,而不管这个value信息究竟是存储在哪一个节点之上。在eMule、BitTorrent等P2P文件交换系统中,Kademlia主要充当了文件信息检索协议这一关键角色,但Kad网络的应用并不仅限于文件交换。下文的描述将主要围绕eMule中Kad网络的设计与实现展开。


2. eMule的Kad网络中究竟存储了哪些信息?

只要是能够表述成为字典条目形式的信息Kad网络均能存储,一个Kad网络能够同时存储多张分布式哈希表。以eMule为例,在任一时刻,其Kad网络均存储并维护着两张分布式哈希表,一张我们可以将其命名为关键词字典,而另一张则可以称之为文件索引字典。

a. 关键词字典:主要用于根据给出的关键词查询其所对应的文件名称及相关文件信息,其中key的值等于所给出的关键词字符串的160比特SHA1散列,而其对应的value则为一个列表,在这个列表当中,给出了所有的文件名称当中拥有对应关键词的文件信息,这些信息我们可以简单地用一个3元组条目表示:(文件名,文件长度,文件的SHA1校验值),举个例子,假定存在着一个文件“warcraft_frozen_throne.iso”,当我们分别以“warcraft”、“frozen”、“throne”这三个关键词来查询Kad时,Kad将有可能分别返回三个不同的文件列表,这三个列表的共同之处则在于它们均包含着一个文件名为“warcraft_frozen_throne.iso”的信息条目,通过该条目,我们可以获得对应iso文件的名称、长度及其160比特的SHA1校验值。

b. 文件索引字典:用于根据给出的文件信息来查询文件的拥有者(即该文件的下载服务提供者),其中key的值等于所需下载文件的SHA1校验值(这主要是因为,从统计学角度而言,160比特的SHA1文件校验值可以唯一地确定一份特定数据内容的文件);而对应的value也是一个列表,它给出了当前所有拥有该文件的节点的网络信息,其中的列表条目我们也可以用一个3元组表示:(拥有者IP,下载侦听端口,拥有者节点ID),根据这些信息,eMule便知道该到哪里去下载具备同一SHA1校验值的同一份文件了。


3. 利用Kad网络搜索并下载文件的基本流程是怎样的?

基于我们对eMule的Kad网络中两本字典的理解,利用Kad网络搜索并下载某一特定文件的基本过程便很明白了,仍以“warcraft_frozen_throne.iso”为例,首先我们可以通过warcraft、frozen、throne等任一关键词查询关键词字典,得到该iso的SHA1校验值,然后再通过该校验值查询Kad文件索引字典,从而获得所有提供“warcraft_frozen_throne.iso”下载的网络节点,继而以分段下载方式去这些节点下载整个iso文件。

在上述过程中,Kad网络实际上所起的作用就相当于两本字典,但值得再次指出的是,Kad并不是以集中的索引服务器(如华语P2P源动力、Razorback 2、DonkeyServer 等,骡友们应该很熟悉吧)方式来实现这两本字典的存储和搜索的,因为这两本字典的所有条目均分布式地存储在参与Kad网络的各节点中,相关文件信息、下载位置信息的存储和交换均无需集中索引服务器的参与,这不仅提高了查询效率,而且还提高了整个P2P文件交换系统的可靠性,同时具备相当的反拒绝服务攻击能力;更有意思的是,它能帮助我们有效地抵制FBI的追捕,因为俗话说得好:法不治众…看到这里,相信大家都能理解“分布式信息检索”所带来的好处了吧。但是,这些条目究竟是怎样存储的呢?我们又该如何通过Kad网络来找到它们?不着急,慢慢来。


4. 什么叫做节点的ID和节点之间的距离?

Kad网络中的每一个节点均拥有一个专属ID,该ID的具体形式与SHA1散列值类似,为一个长达160bit的整数,它是由节点自己随机生成的,两个节点拥有同一ID的可能性非常之小,因此可以认为这几乎是不可能的。在Kad网络中,两个节点之间距离并不是依靠物理距离、路由器跳数来衡量的,事实上,Kad网络将任意两个节点之间的距离d定义为其二者ID值的逐比特二进制和数,即,假定两个节点的ID分别为a与b,则有:d=a XOR b。在Kad中,每一个节点都可以根据这一距离概念来判断其他节点距离自己的“远近”,当d值大时,节点间距离较远,而当d值小时,则两个节点相距很近。这里的“远近”和“距离”都只是一种逻辑上的度量描述而已;在Kad中,距离这一度量是无方向性的,也就是说a到b的距离恒等于b到a的距离,因为a XOR b==b XOR a


5. 条目是如何存储在Kad网络中的?

从上文中我们可以发现节点ID与条目中key值的相似性:无论是关键词字典的key,还是文件索引字典的key,都是160bit,而节点ID恰恰也是160bit。这显然是有目的的。事实上,节点的ID值也就决定了哪些条目可以存储在该节点之中,因为我们完全可以把某一个条目简单地存放在节点ID值恰好等于条目中key值的那个节点处,我们可以将满足(ID==key)这一条件的节点命名为目标节点N。这样的话,一个查找条目的问题便被简单地转化成为了一个查找ID等于Key值的节点的问题。

由于在实际的Kad网络当中,并不能保证在任一时刻目标节点N均一定存在或者在线,因此Kad网络规定:任一条目,依据其key的具体取值,该条目将被复制并存放在节点ID距离key值最近(即当前距离目标节点N最近)的k个节点当中;之所以要将重复保存k份,这完全是考虑到整个Kad系统稳定性而引入的冗余;这个k的取值也有讲究,它是一个带有启发性质的估计值,挑选其取值的准则为:“在当前规模的Kad网络中任意选择至少k个节点,令它们在任意时刻同时不在线的几率几乎为0”;目前,k的典型取值为20,即,为保证在任何时刻我们均能找到至少一份某条目的拷贝,我们必须事先在Kad网络中将该条目复制至少20份。

由上述可知,对于某一条目,在Kad网络中ID越靠近key的节点区域,该条目保存的份数就越多,存储得也越集中;事实上,为了实现较短的查询响应延迟,在条目查询的过程中,任一条目可被cache到任意节点之上;同时为了防止过度cache、保证信息足够新鲜,必须考虑条目在节点上存储的时效性:越接近目标结点N,该条目保存的时间将越长,反之,其超时时间就越短;保存在目标节点之上的条目最多能够被保留24小时,如果在此期间该条目被其发布源重新发布的话,其保存时间还可以进一步延长。


6. Kad网络节点需要维护哪些状态信息?

在Kad网络中,每一个节点均维护了160个list,其中的每个list均被称之为一个k-桶(k-bucket),如下图所示。在第i个list中,记录了当前节点已知的与自身距离为2^i~2^(i+1)的一些其他对端节点的网络信息(Node ID,IP地址,UDP端口),每一个list(k-桶)中最多存放k个对端节点信息,注意,此处的k与上文所提到的复制系数k含义是一致的;每一个list中的对端节点信息均按访问时间排序,最早访问的在list头部,而最近新访问的则放在list的尾部。

转贴: Kademlia详解_第1张图片

k-桶中节点信息的更新基本遵循Least-recently Seen Eviction原则:当list容量未满(k-桶中节点个数未满k个),且最新访问的对端节点信息不在当前list中时,其信息将直接添入list队尾,如果其信息已经在当前list中,则其将被移动至队尾;在k-桶容量已满的情况下,添加新节点的情况有点特殊,它将首先检查最早访问的队首节点是否仍有响应,如果有,则队首节点被移至队尾,新访问节点信息被抛弃,如果没有,这才抛弃队首节点,将最新访问的节点信息插入队尾。可以看出,尽可能重用已有节点信息、并且按时间排序是k-桶节点更新方式的主要特点。从启发性的角度而言,这种方式具有一定的依据:在线时间长一点的节点更值得我们信任,因为它已经在线了若干小时,因此,它在下一个小时以内保持在线的可能性将比我们最新访问的节点更大,或者更直观点,我这里再给出一个更加人性化的解释:MP3文件交换本身是一种触犯版权法律的行为,某一个节点反正已经犯了若干个小时的法了,因此,它将比其他新加入的节点更不在乎再多犯一个小时的罪……-_-b

由上可见,设计采用这种多k-bucket数据结构的初衷主要有二:a. 维护最近-最新见到的节点信息更新;b. 实现快速的节点信息筛选操作,也就是说,只要知道某个需要查找的特定目标节点N的ID,我们便可以从当前节点的k-buckets结构中迅速地查出距离N最近的若干已知节点。


7. 在Kad网络中如何寻找某特定的节点?

已知某节点ID,查找获得当前Kad网络中与之距离最短的k个节点所对应的网络信息(Node ID,IP地址,UDP端口)的过程,即为Kad网络中的一次节点查询过程(Node Lookup)。注意,Kad之所以没有把节点查询过程严格地定义成为仅仅只查询单个目标节点的过程,这主要是因为Kad网络并没有对节点的上线时间作出任何前提假设,因此在多数情况下我们并不能肯定需要查找的目标节点一定在线或存在。

整个节点查询过程非常直接,其方式类似于DNS的迭代查询:
a. 由查询发起者从自己的k-桶中筛选出若干距离目标ID最近的节点,并向这些节点同时发送异步查询请求;
b .被查询节点收到请求之后,将从自己的k-桶中找出自己所知道的距离查询目标ID最近的若干个节点,并返回给发起者;
c. 发起者在收到这些返回信息之后,再次从自己目前所有已知的距离目标较近的节点中挑选出若干没有请求过的,并重复步骤1;
d. 上述步骤不断重复,直至无法获得比查询者当前已知的k个节点更接近目标的活动节点为止。
e. 在查询过程中,没有及时响应的节点将立即被排除;查询者必须保证最终获得的k个最近节点都是活动的。

简单总结一下上述过程,实际上它跟我们日常生活中去找某一个人打听某件事是非常相似的,比方说你是个Agent Smith,想找小李(key)问问他的手机号码(value),但你事先并不认识他,你首先肯定会去找你所认识的和小李在同一个公司工作的人,比方说小赵,然后小赵又会告诉你去找与和小李在同一部门的小刘,然后小刘又会进一步告诉你去找和小李在同一个项目组的小张,最后,你找到了小张,哟,正好小李出差去了(节点下线了),但小张恰好知道小李的号码,这样你总算找到了所需的信息。在节点查找的过程中,“节点距离的远近”实际上与上面例子中“人际关系的密切程度”所代表的含义是一样的。

最后说说上述查询过程的局限性:Kad网络并不适合应用于模糊搜索,如通配符支持、部分查找等场合,但对于文件共享场合来说,基于关键词的精确查找功能已经基本足够了(值得注意的是,实际上我们只要对上述查找过程稍加改进,并可以令其支持基于关键词匹配的布尔条件查询,但仍不够优化)。这个问题反映到eMule的应用层面来,它直接说明了文件共享时其命名的重要性所在,即,文件名中的关键词定义得越明显,则该文件越容易被找到,从而越有利于其在P2P网络中的传播;而另一方面,在eMule中,每一个共享文件均可以拥有自己的相关注释,而Comment的重要性还没有被大家认识到:实际上,这个文件注释中的关键词也可以直接被利用来替代文件名关键词,从而指导和方便用户搜索,尤其是当文件名本身并没有体现出关键词的时候。


8. 在Kad网络中如何存储和搜索某特定的条目?

从本质上而言,存储、搜索某特定条目的问题实际上就是节点查找的问题。当需要在Kad网络中存储一个条目时,可以首先通过节点查找算法找到距离key最近的k个节点,然后再通知它们保存条目即可。而搜索条目的过程则与节点查询过程也是基本类似,由搜索发起方以迭代方式不断查询距离key较近的节点,一旦查询路径中的任一节点返回了所需查找的value,整个搜索的过程就结束。为提高效率,当搜索成功之后,发起方可以选择将搜索到的条目存储到查询路径的多个节点中,作为方便后继查询的cache;条目cache的超时时间与节点-key之间的距离呈指数反比关系。


9. 一个新节点如何首次加入Kad网络?

当一个新节点首次试图加入Kad网络时,它必须做三件事,其一,不管通过何种途径,获知一个已经加入Kad网络的节点信息(我们可以称之为节点I),并将其加入自己的k-buckets;其二,向该节点发起一次针对自己ID的节点查询请求,从而通过节点I获取一系列与自己距离邻近的其他节点的信息;最后,刷新所有的k-bucket,保证自己所获得的节点信息全部都是新鲜的。