現有的P2P實現可以分爲三種類型。它們分別是:基於目錄服務器P2P,非結構化P2P和結構化P2P。基於目錄服務器這一類系統中設置目錄服務器,用於保存用戶節點的地址信息和該節點上共享文件的描述信息,文件本身是分散存貯在各個節點上的,實際的文件傳輸也是在對等節點之間進行,目錄服務器僅僅起到中介作用,爲節點提供發佈和查詢文件索引服務。鑑於集中式目錄服務器不僅可能成爲系統的瓶頸,而且還可能引發法律糾紛,因此出現了以Gnutella爲代表的非結構化P2P系統,在這種P2P結構中,文件索引信息不再由集中式的目錄服務器存儲和管理,而是分散到網絡中,由節點自己保存,該類系統採用分佈式的索引查找策略,爲了查找網絡中的文件,節點要隨機地維護網絡中的其他一些節點作爲鄰居,以便通過鄰居節點廣播查詢報文。非結構化P2P系統中由於不存在目錄服務器,所以沒有單點瓶頸問題,不存在單一故障點。然而其缺點也是明顯的:在網絡中廣播查詢報文加重了網絡通信負擔,其查詢機制在系統規模擴大時不具有可擴展性。另外,由於查詢報文被限制在特定的範圍內,所以並不能保證一定可以找到網絡中存在的目的數據。上面介紹的兩類P2P系統都缺乏有效的、可擴展的索引查找機制。爲此,近年來許多研究小組在設計可擴展的查找機制方面做了大量的研究工作,提出了Chord、Pastry、CAN和Tapestry等用於構建結構化P2P的分佈式哈希表系統(Distributed Hash Table,DHT)。DHT的主要思想是:首先,每條文件索引被表示成一個(K, V)對,K稱爲關鍵字,可以是文件名(或文件的其他描述信息)的哈希值,V是實際存儲文件的節點的IP地址(或節點的其他描述信息)。所有的文件索引條目(即所有的(K, V)對)組成一張大的文件索引哈希表,只要輸入目標文件的K值,就可以從這張表中查出所有存儲該文件的節點地址。然後,再將上面的大文件哈希表分割成很多局部小塊,按照特定的規則把這些小塊的局部哈希表分佈到系統中的所有參與節點上,使得每個節點負責維護其中的一塊。這樣,節點查詢文件時,只要把查詢報文路由到相應的節點即可(該節點維護的哈希表分塊中含有要查找的(K,V)對)。這裏面有個很重要的問題,就是節點要按照一定的規則來分割整體的哈希表,進而也就決定了節點要維護特定的鄰居節點,以便路由能順利進行。這個規則因具體系統的不同而不同,CAN,Chord,Pastry和Tapestry都有自己的規則,也就呈現出不同的特性。基於分佈式哈希表(DHT)的分佈式檢索和路由算法因爲具有查找可確定性、簡單性和分佈性等優點,正成爲國際上結構化P2P網絡研究和應用的熱點。自2002年起,美國國家科學基金會(NSF)提供了1200萬美元的資金啓動了一個爲期5年的研究項目IRIS,該項目集中了MIT和UC Berkeley等5所著名高等院校的強大科研力量,爲下一代大規模分佈式應用研製基於DHT的新型基礎設施。
Chord是UC Berkeley和MIT共同提出的一種分佈式查找算法,目的是爲了能在P2P網絡中查找數據。給定一個關鍵字,Chord可以有效地把該關鍵字映射到網絡中某個節點上。因而在P2P網絡中只要給每個數據V都賦予一個關鍵字K,就可以利用Chord在該關鍵字映射的節點上存儲或提取相應的(K, V)對。Chord的突出特點是算法簡單,而且可擴展 - 查詢過程的通信開銷和節點維護的狀態隨着系統總節點數增加成指數關係。Chord的路由性能優於CAN,而節點加入過程和維護開銷又優於Tapestry和Pastry。
上圖給出了一個m=6的Chord環,環中分佈了10個節點,存儲了5個關鍵字,節點標識前加上N而關鍵字前加上K以示區別。因爲successor(10)=14,所以關鍵字10存儲到節點14上。同理,關鍵字24和30存儲到節點32上,關鍵字38存儲到節點38上,而關鍵字54則存儲到節點56上。當網絡中的參與節點發生變動時,上面的映射規則仍然要成立。爲此,當某節點n加入網絡時,某些原來分配給n的後繼節點的關鍵字將分配給n。當節點n離開網絡時,所有分配給它的關鍵字將重新分配給n的後繼節點。除此之外,網絡中不會發生其他的變化。以上圖爲例,當標識爲26的節點接入時,原有標識爲32的節點負責的標識爲24的關鍵字將轉由新節點存儲。顯然,爲了能在系統中轉發查詢報文,每個節點要了解並維護chord環上相鄰節點的標識和IP地址,並用這些信息構成自身的路由表。有了這張表,Chord就可以在環上任意兩點間進行尋路。
Chord的路由:Chord中每個節點只要維護它在環上的後繼節點的標識和IP地址就可以完成簡單的查詢過程。對特定關鍵字的查詢報文可以通過後繼節點指針在圓環上傳遞,直到到達這樣一個節點:關鍵字的標識落在該節點標識和它的後繼節點標識之間,這裏的後繼節點就是存儲目標(K, V)對的節點。
上圖給出了一個示例,節點8發起的查找關鍵字54的請求,通過後繼節點依次傳遞,最後定位到存儲有關鍵字54的節點56。在這種簡單查詢方式中,每個節點需要維護的狀態信息很少,但查詢速度太慢。若網絡中有N個節點,查詢的代價就爲O(N)數量級。因而在網絡規模很大時,這樣的速度是不能接受的。
爲了加快查詢的速度,Chord使用擴展的查詢算法。爲此,每個節點需要維護一個路由表,稱爲指針表(finger table)。如果關鍵字和節點標識符用m位二進制位數表示,那麼指針表中最多含有m個表項。節點n的指針表中第i項是圓環上標識大於或等於n+2i-1的第一個節點(比較是以2m爲模進行的)。例如若s=successor(n+2i-1), 1≤i≤m,則稱節點s爲節點n的第i個指針,記爲n.finger[i]。n.finger[1]就是節點n的後繼節點。指針表中每一項既包含相關節點的標識,又包含該節點的IP地址(和端口號)。
上圖給出了節點8的指針表,例如節點14是環上緊接在(8+20) mod 26=9之後的第一個節點,所以節點8的第一個指針是節點14;同理因爲節點42是環上緊接在(8+25) mod 26=40之後的第一個節點,所以節點8的第6個指針是節點42。維護指針表使得每個節點只需要知道網絡中一小部分節點的信息,而且離它越近的節點,它就知道越多的信息。但是,對於任意一個關鍵字K,節點通常無法根據自身的指針表確定的K的後繼節點。例如,下圖中的節點8就不能確定關鍵字34的後繼節點,因爲環上34的後繼節點是38,而節點38並沒有出現在節點8的指針表中。
節點加入和退出:爲了應對系統的變化,每個節點都週期性地運行探測協議來檢測新加入節點或失效節點,從而更新自己的指針表和指向後繼節點的指針。新節點n加入時,將通過系統中現有的節點來初始化自己的指針表。也就是說,新節點n將要求已知的系統中某節點爲它查找指針表中的各個表項。在其他節點運行探測協議後,新節點n將被反映到相關節點的指針表和後繼節點指針中。這時,系統中一部分關鍵字的後繼節點也變爲新節點n,因而先前的後繼節點要將這部分關鍵字轉移到新節點上。當節點n失效時,所有指針表中包括n的節點都必須把它替換成n的後繼節點。爲了保證節點n的失效不影響系統中正在進行的查詢過程,每個Chord節點都維護一張包括r個最近後繼節點的後繼列表。如果某個節點注意到它的後繼節點失效了,它就用其後繼列表中第一個正常節點替換失效節點。
Pastry的設計:Pastry是自組織的重疊網絡,每個節點都被分配一個128位的nodeId。nodeId用於在圓形的節點空間中(從0到2128-1)標識節點的位置,它是在節點加入系統時隨機分配的,隨機分配的結果是使得所有的nodeId在128位的節點號空間中均勻分佈。nodeId可以通過計算節點公鑰或者IP地址的哈希函數值來獲得。
nodeId爲10233102的Pastry節點維護的狀態示意圖
上圖給出了一個節點維護的數據示意圖,b取值爲2,所有的數均是4進制的。其中路由表的最上面一行是第0行。路由表中每行的陰影項表示當前節點號中相應的數位。路由表中每項節點的nodeId表示格式是“相同前綴 + 下一數位 + nodeId的剩餘位”。圖中沒有列出相關節點的IP地址。
新節點加入時需要初始化自身的狀態表,並通知其他節點自己已經加入系統。假定新加入節點的nodeId爲X,同時假定X在加入Pastry之前知道系統中和自己距離相近的節點A。新節點X首先請求A路由一條“加入”消息,消息的關鍵字就是X。這條消息最終會到達nodeId和X最接近的節點Z。作爲應答,節點A、節點Z以及從A到Z的路徑上所有經過的節點都會把自己的狀態表發送給節點X。節點X利用這些信息初始化自己的狀態表,然後節點X再通知其他節點它已經加入了系統。從交換的消息數量上說,節點加入操作的複雜度爲O(log2bN)。
下圖給出了一個2維的[0, 1]×[0, 1]的笛卡兒座標空間劃分成五個節點區域的情況。虛擬座標空間採用下面的方法保存(K, V)對。當保存(K1, V1)時,使用統一的哈希函數把關鍵字K1映射成座標空間中的點P。那麼這個值將被保存在該點所在區域的節點中。當需要查詢關鍵字K1對應的值時,任何節點都可以使用同樣的哈希函數找到K1對應的點P,然後從該點對應的節點取出相應的值V1。如果此節點不是發起查詢請求的節點,CAN將負責將此查詢請求轉發到P所在區域的節點上。因此,有效的路由機制是CAN中的一個關鍵問題。
五個節點維護的CAN虛平面
CAN中的路由很簡單,沿着座標空間中從發起請求的點到目的點之間的一條路徑轉發即可。爲此,每個CAN節點都要保存一張座標路由表,其中包括它的鄰居節點的IP地址和其維護的虛擬座標區域。兩個節點互爲鄰居是指:在d維座標空間中,兩個節點維護的區域在d-1維的座標上有重疊而在剩下的一維座標上相互鄰接。例如,圖2.6中D和E是鄰接節點,而D和B就不是鄰接節點, 因爲D和B在X軸和Y軸上都鄰接。每條CAN消息都包括目的點座標。路由時節點只要朝着目標節點的方向把消息轉發給自己的鄰居節點即可。下圖給出了查找過程的一個簡單的例子。
因爲整個CAN空間要分配給系統中現有的全部節點,當一個新的節點加入網絡時必須得到自己的一塊座標空間。CAN通過分割現有的節點區域實現這一過程。它把某個現有節點的區域分裂成同樣大小的兩塊,自己保留其中的一塊而另一塊分給新加入的節點。整個過程分爲以下三步:
1. 新節點首先找到一個已經在CAN中的節點。
2. 新節點使用CAN的路由機制找到一個區域將要被分割的節點。
3. 執行分割操作,然後原有區域的鄰接區域必須被告知發生了分割,這樣新節點才能被別的節點路由到。
當節點離開CAN時,必須保證它的區域被系統中剩餘的節點接管,也即分配給其他仍然在系統中的節點。一般是由某個鄰居節點來接管這個區域和所有的索引數據(K,V)對。如果某個鄰居節點負責的區域可以和離開節點負責的區域合併形成一個大的區域,那麼將由這個鄰居節點執行合併操作。否則,該區域將交給其鄰居節點中區域最小的節點負責。也就是說,這個節點將臨時負責兩個區域。
Tapestry中的每個節點都保存有鄰居映射表。鄰居映射表可以用於把消息按照目的地址一位一位地向前傳遞,比如從4***=>42**98=>42A*=>目的節點42AD(這裏*表示通配符)。這種方式類似於IP分組轉發過程中的最長前綴匹配。節點N的鄰居映射表分爲多個級別,每個級別包含的鄰居節點的數量等於標識符表示法的基數,而每個級別中鄰居節點標識符和本節點標識符的相同前綴都比前一級別多一個數位。也就是說,第j級鄰居表的第i項是標識符以prefix(N, j-1) + “i”爲前綴而且離當前節點最近的鄰居節點。例如,節點325AE的鄰居映射表中第4級第9項是系統中標識符以325 + “9” =3259爲前綴的某個節點。
Tapestry採用的基本查找和路由機制:當一條查找消息到達傳遞過程中的第n個節點時,該節點和目的節點的共同前綴長度至少大於n。爲了進行轉發,該節點將查找鄰居映射表的第n+1級中和目的標識符下一數位相匹配的鄰居節點。轉發過程將在每個節點中依次進行直到到達目的節點。這種方法可以保證路由至多經過logbN個節點就可以到達目的節點,這裏N是節點標識符名字空間的大小,而b是標識符使用的基數。同樣,由於每個節點的鄰居映射表的每個級別只需要保存b個表項,因此,鄰居映射表的空間爲blogbN。
上圖給出了Tapestry中一個查詢消息轉發的例子。圖中節點標識符的基數是4,查詢消息從5230發出,目的節點是42AD。
Tapestry中的節點在共享數據時被稱爲服務器,請求數據時被稱爲客戶,轉發消息時被稱爲路由器。也就是說每個節點可以同時具有客戶、服務器和路由器的功能。
服務器S通過向對象O(GUID爲OG)的根節點OR定期的發送消息來報告S保存有對象O。在這條發佈路徑上的每個節點都保存關於這個對象O的位置信息指針<OG, S>,這裏的位置信息只是一個指向S的指針,而並不是對象O的拷貝。當多個都存有同一對象拷貝的服務器分別向根結點發布消息時,路徑上的每個節點按各個服務器離自己的網絡時延遞增的順序保存這些位置指針列表。
當需要定位一個對象O時,客戶向對象O的根節點發出查詢消息,查詢消息轉發路徑上的每個節點都檢查自己是否存有對象O的位置指針,如果有,該節點直接把查詢消息轉發個服務器S,否則,消息將到達O的根節點,然後由根節點把查詢消息轉發給服務器。
節點加入和退出:Tapestry的節點加入算法和Pastry類似。節點N在加入Tapestry網絡之前,也需要知道一個已經在網絡中的節點G。然後N通過G發出路由自己的節點ID的請求,根據經過的節點的對應的鄰居節點表構造自己的鄰居節點表。構造過程中還需要進行一些優化工作。構造完自己的數據結構後,節點N將通知網絡中的其他節點自己已經加入網絡。通知只針對在N的鄰居映射表中的主鄰居節點和二級鄰居節點進行。