结构化与非结构化网络
非结构化的P2P网络是指网络节点之间不存在组织关系,节点之间完全是对等的,比如第一代P2P网络Napster。
结构化的P2P网络与非结构化恰好相反,我们认为网络在逻辑上存在一个人为设计的结构,比如Chord假定网络是一个环,Kadelima则假定为一颗二叉树。有了这些逻辑结构,就给我们资源查找引入了更多的算法和思路。
引言
我们在 计算机网络–详解P2P对等网络(一)—BitTorrent协议 这一篇博客中讲述了BT下载的过程:在对等用户拿到种子文件的时候,首先会联系tracker服务器,然后加入用户集群,并在用户集群中寻找自己所需的内容,最后与拥有内容的对等用户进行联系。
从BT下载的过程中引出本节所要讨论的问题:如何高效的从用户集群中找出哪些对等用户拥有你正在寻求的具体内容?
在历史中有三种比较典型的模型来解决这个问题:
Napster:使用一个中心服务器接收所有的查询,服务器告知去哪下载其所需要的数据。存在的问题是中心服务器单点失效导致整个网络瘫痪。
Gnutella:使用消息洪泛(message flooding)来定位数据。一个消息被发到用户集群内每一个节点,直到找到其需要的数据为止。存在的问题是消息数与节点数成线性关系,导致网络负载较重。
SN型:超级节点(Super Node),SN保存网络中节点的索引信息,这一点和中心服务器类型一样,但是网内有多个SN,其索引信息会在这些SN中进行传播,所以整个系统的崩溃几率就会小很多。尽管如此,网络还是有崩溃的可能。
关于P2P网络拓扑结构更详细的内容,请参考:P2P网络的拓扑结构。
现在的研究结果中,Chord、Pastry、CAN和Tapestry等常用于构建结构化P2P的分布式哈希表系统。
Chord算法是麻省理工学院(MIT)提出的一种基于DHT技术的结构化P2P路由协议,具有完全分布式、负载均衡、可用性及可扩展性好、命名方式灵活等特点。本文主要对Chord算法展开分析。
分布式哈希表(DHT)
对于本节问题的思考,我们可以给出一种基本的解决方案:每个对等节点维护了一张路由表(索引),这张路由表只保存了少量有关其他节点的信息,这个特点意味着它保持最新索引的代价不会很昂贵。其次,每个节点可以快速的查看索引中的表项,否则,它就不是个有效的索引。最后,每个节点可以同时使用索引,即使其他节点来来去去,这个属性意味着索引的性能随着节点数量的增长反而越来越好~
该解决方案就被称为分布式哈希表,因为对等节点所维护的路由表就是一张索引表,而索引的基本功能就是将一个关键字映射到一个值。这简直就是一张哈希表,但是我们的解决方案是分布式版本。我们可以再看一下维基对于DHT的定义:
分布式哈希表(distributed hash table,缩写DHT):分布式计算系统中的一类,用来将一个关键值(key)的集合分散到所有在分布式系统中的节点,并且可以有效地将消息转送到唯一一个拥有查询者提供的关键值的节点(Peers)。这里的节点类似散列表中的存储位置。分布式散列表通常是为了拥有极大节点数量的系统,而且在系统的节点常常会加入或离开(例如网络断线)而设计的。在一个结构性的覆盖网络(overlay network)中,参加的节点需要与系统中一小部分的节点沟通,这也需要使用分布式散列表。
上述我特意加粗的语句,正是对P2P网络架构的描述。
如果对于DHT的概念还抱有一定的疑惑,可以在网上搜寻更白话的说明,博主不再进行贴出。
DHT与一致性哈希
如上所述,DHT的主要想法是把网络上资源的存取像哈希表一样,可以简单而快速地进行put、get。与一致性哈希相比,DHT更强调的是资源的存取,而不管添加删除节点时产生的资源震荡的问题。与一致性哈希相同的是,DHT也只是一个概念,具体细节留给各实现。
当前这些P2P实现可以被作为DHT的具体实现,再次列举一些有代表性的实现:
Chord、CAN、Tapestry、Pastry、Apache Cassandra、Kadelima、P-Grid、BitTorrent DHT
Chord算法
Chord是什么?
Chord是一个算法,也是一个协议。作为一个算法,Chord可以从数学的角度严格证明其正确性和收敛性;作为一个协议,Chord详细定义了每个环节的消息类型。当然,Chord之所以受追捧,还有一个主要原因就是Chord足够简单,3000行的代码就足以实现一个完整的Chord。
Chord概述
Chord的实现方式如下:给定一个关键字Key,将其映射到某个节点。为此,采用相同哈希函数(SHA-1)为每个节点和关键字产生一个m bit的ID,并按照ID大小构成环形拓扑。节点所产生的ID被称为节点标识符,关键字所产生的ID我们称它为关键字ID。运行Chord的主机相互连接构成Chord网络,这是一个建立在IP网络之上的覆盖(overlay)网络。每个节点N有2个邻居:以顺时针为正方向排列在N之前的第1个节点称为N的前继(predecessor),在N之后的第1个节点称为N的后继(successor)。如下图(蓝色节点为节点ID,白色节点为关键字ID):
同一致性哈希一样,资源放置在关键字ID的后继节点上,如上图,资源2被放置在节点3中。
Finger表
我们在本篇博客 分布式哈希表(DHT) 一节中已经讲过,每个对等节点都会维护一张路由表,以便在用户集群中寻找拥有所需资源的其他对等节点。这张路由表就被称为Finger表,Finger表的表项大小为m,由两列数据项组成,如下:
ID+2的i次方 | successor |
---|
其中ID就代表节点标识符,i表示Finger表中表项的下标,从0开始,successor则表示存储资源的后继节点。
举个例子:我们现在有一个m = 3
的Chord环,它可以容纳2的3次方,也就是8个节点。现在有4台机器,假设它们经过哈希之后所产生的ID为0,1,2,6,那么机器1中将要维护的Finger表如下:
i | ID+2的i次方 | successor |
---|---|---|
0 | 2 | 2 |
1 | 3 | 6 |
2 | 5 | 6 |
其中ID+2的i次方表示的是关键字ID。
对于上表的解释,由一致性哈希可知:
机器1本地存储着关键字ID为1的数据,机器2本地存储着关键字ID为2的数据,机器6本地存储着关键字ID为3,4,5,6的数据,机器0本地存储着关键字ID为7,0 的数据。
与此同时,如上表,机器1上,还存储着关键字ID为2,3,5的数据所在的机器地址。比如,机器1知道,关键字ID为5的数据存储在机器6上面。
Chord的查找
Chord采取幂次逼近查询法。任何一个节点收到查询关键字ID为“K”的请求时,首先检查K是否落在该节点标识和它的后继节点标识之间,如果是的话,这个后继节点就是存储目标(K, V)对的节点。否则,节点将查找它的Finger表,找到表中节点标识符最大但不超过K的节点,并将这个查询请求转发给该节点。通过重复这个过程,最终可以定位到K的后继节点,即存储有目标(K, V)对的节点。
比如,当机器1接收到查询关键字ID为7的数据在哪台机器上时,它发现关键字ID“7”并不在该节点标识符和它的后继节点标识符之间,因此它查找节点标识符最大但没有超过7的节点,为6,于是将查询请求转发到机器6上。
机器6的路由表按照上述规则进行生成,如下(环形拓扑):
i | ID+2的i次方 | successor |
---|---|---|
0 | 7 | 0 |
1 | 0 | 0 |
2 | 2 | 2 |
机器6上的路由表指出:关键字ID为7的数据在机器0上… …重复这个过程,最终可找到保存关键字ID为7的资源的节点。
通过在每台机器上保存m项的路由信息,上面的方式可以做到O(logN)的查询时间复杂度。另外,比如Amazon Dynamo在论文中所说:通过在每台机子上保存足够多的路由信息,理论上可以做到O(1)时间的查询(相应的,节点间冗余信息也会更多)。
节点的加入
新节点的加入需要一个称为向导的已知节点(n0)进行协助,任何一个运行在Chord网络中的节点都可以充当这个角色。加入过程包括新节点本身的Join操作和被其他节点发现2个阶段。如下图所示,假设np和ns是Chord网络中相邻两节点,n为新节点,它加入网络后应该位于np和ns节点之间。
新节点的加入有三个操作:
- Join() :n加入一个Chord环,已知其中有一个向导节点n0;
- Stabilize(): 每个节点在后台周期性的进行此项操作,查询自身节点的后继节点的前序节点是否是自身,如果不是自身,说明有新加入的节点,此时将自身的后继节点修改为新加入的节点;
- Notify(n): n通知其他节点它的存在,若此时其他节点没有前序节点或n比其现有的前序节点更加靠近自身,则将n设置为前序节点。
在了解了上述三个操作之后,我们讨论一下n节点加入的具体过程:
- n请求向导为它查找后继 (即ns),并初始化自身Finger表,按照Finger表的定义,此时只有n对自身属性进行了设置,其他节点并不知道新节点的加入(如上图a);
- 在n节点将自身的后继节点修改为ns后,会对ns进行Notify(n)操作,即n节点通知ns它的存在,此时ns标记n成为自己的前序节点;
- 所有节点会在后台周期性的进行Stabilize操作,此时np发现ns的前序结点已不是自身,则np将自己的后继节点修改为n;
- np对n进行Notify(np)操作,n接到通知,将np修改为自己的前序节点。
在这里有一个问题:向导节点如何帮助新加入的节点寻找它的后继节点以及新加入的节点如何初始化其Finger表?
第一点:对于新节点n,通过向向导节点提交查找n自身节点标识符的请求,向导节点检索其后继;
第二点:新节点通过向向导节点请求ID + 2的m次方
从而构建Finger表。
节点的失效
节点的失效是节点没有通知其他节点而突然离开网络,这通常由主机崩溃或IP网络断开等意外原因造成,此时失效节点的前继保存的后继信息变得不可用,从而造成Chord环的断裂。为了处理这个问题,需要周期性的对节点的前序和后继进行探测。如果节点n发现其后继或前序已经失效,则从Finger表中顺序查找第1个可用节点进行替换,并重建Finger表。对前序节点失效的处理仍需要借助于Notify消息。考虑上图中的例子,ns虽然能够感知n的失效却无法进行修复。由于上述对后继失效的处理过程能够保证Chord环后继链的正确性,因此np通过在Stabilize中向新后继ns发送Notify,把ns的前继改成np。值得注意的是,其他节点也可能在Finger表项中保存有失效节点的记录,因此需要多次Stabilize,把失效信息扩散到Chord网络中。虽然这种方法最终能够保证Chord网络的完整性,但在节点频繁进出的情况下,其效率仍须更深入地研究。
节点的退出
由于节点失效处理方法是稳定的,因此节点的退出可看作为失效而不采取其他附加措施。但基于效率的考虑,节点n退出时进行如下操作:1. 把n后继节点的前继改成n的前继;2. 把n前继节点的后继改成n的后继;3. 从n前继的Finger表中删除n。
总结
- 熟悉DHT、一致性哈希、Chord算法之间的概念及联系;
- 熟悉Chord算法的思想(Finger表的构建、Chord查询、节点的加入等);
- 了解P2P网络的一些其它拓扑结构。
PS:关于Chord算法数学角度上的证明与分析,有兴趣的同学可以自行查阅相关资料~
参考阅读
计算机网络(第五版) — Andrew S. TanenBaum/David J. Wetherall