背景
Chord算法是DHT(Distributed Hash Table)的一種經典實現。下面從網上無節操盜了一段介紹性文字:
Chord是最簡單。最精確的環形P2P模型。“Chord”這個單詞在英文中是指“弦”,在分布式系統中指“帶弦環”,在P2P領域則指基於帶弦環拓撲結構的分布式散列表(DHT)或者構建與其上的P2P網絡。盡管MIT和UC Berkeley的研究早在2001年之前就開發了Chord及其應用系統,但有關Chord的正式論文[Stoica et al., 2001]發表在計算機通信領域著名會議ACM SIGCOMM’01 上,其它刊物和會議后來也有刊登Chord的論文的,如[Stoica et al., 2003]。Chord是結構化的P2P領域最著名的理論模型,到2006年已經被引用超過3000次。能夠說。凡是研究過P2P的人。沒有不知道Chord的。
假設只將Chord看成一個分布式散列表,那么它只支持結構化P2P最簡單的功能:將節點和數據對象映射到覆蓋網中。Chord基於安全的一致性散列函數來分配節點ID和對象ID。在一個有N個節點的網路中。每一個Chord節點保存O(logN)個其它節點的信息。查詢數據對象須要的覆蓋網絡的路由跳數也是O(logN),當節點離開或者增加網絡時,為了維持網絡結構。保持自適應性所須要的的消息數在O(log2N)。作為分布式的散列表。Chord具有差點兒最優的路由效率,確定性的對象查詢,負載均衡。高可擴展性以及良好的容錯性與自適應性;但最重要的一點是:Chord簡單,優美,這是它最為經典的直接原因。
Chord的開源實現主要有兩個,一個是單機版的jchord。還有一個是集群形式的open chord項目。下面描寫敘述都是參考開源項目代碼展開的。
單線程版
在單個線程里,Chord環類似是一個內存數據結構。Chord環的節點上能夠存儲數據,也能夠起服務。這取決於你利用Chord來做什么。
下面的實現主要側重於新建Chord環,並能夠添加節點。在添加節點的過程中,依照Chord的算法描寫敘述,怎么樣影響相關節點。怎么維護Finger表內容。
首先,Chord類維護ChordNode的一個List。createNode方法依據nodeId創建一個新的ChordNode。為其生成好NodeKey,為全部的ChordNode排好序(TreeMap)。
ChordNode表示環上的節點,包括這些元素:
nodeId, ChordKey,successor, predecessor和 FingerTable。
依據nodeId在生成ChordKey,即環上的位置,然后賦予后繼和前繼(順時針方向為向后)。
FingerTable維護的是m個<index, ChordNode>,m為所選hash函數的hash值位數(比方選擇SHA1,m等於hash位數160,即hash環最大值為2160-1),index為key+2i。i取0, 1, … ,m-1。
對於小型p2p網絡來說,m的取值還是比較大的,導致節點的finger表冗余度可能會有90%以上(可能前50個值都指向N1,后100個都指向N2,最后的10個指向N5),只是這部分冗余倒不會對查找/路由性能帶來什么明顯影響。
FingerTable能夠形象理解為,以本節點出發。將整個環切為m份,切的方式是按2的等比增長切,即1。2,4。8。…,2159,每一個index值為finger表里的一條記錄的key,選擇該index值為起點順時針近期的后繼節點作為該finger項的value。如此的話,每一個環上的節點都維護了一張全局的二分節點路由表。
然后。在建立新的Chord哈希環的時候,
1. 生成Chord,並創建若干個節點。
每一個節點在創建的時候,都相當於以自身為第一個節點創建了環。
2. 把創建好的節點逐個增加到某個節點的環上,增加過程僅僅會影響某幾個節點
2.1 在指定的節點N0上join新節點Nk。join過程是借助已有節點N0的finger表為新的節點Nk找到后繼Nsuc =N0.findSuccessor(Nk.key),此時Nk的前繼為null
2.2 確認新節點Nk與后繼節點Nsuc的相互關系。須要注意的是。這個確認過程指的是待確認節點的后繼是不是如今的后繼,以及,后繼節點的前繼
會不會因待確認節點的增加而更新。
由於可能后繼節點Nsuc和新節點Nk之間還有節點Nbet。則須要更新Nk的后繼節點為Nbet,且后繼節點Nsuc和原前繼之間增加了Nk之后,
原前繼的后繼要改變。
2.3 確認剛剛的后繼節點Nsuc的原前繼節點Npre =Nsuc.getPre()的后繼節點。
因后繼節點Nsuc的原前繼節點Npre可能會由於新節點Nk的增加
而改變其后繼節點。
2.2和2.3都是確認過程。僅僅是基於的點不一樣,能夠體會一下,思路是一樣的。
3. 刷新每一個節點的finger表。該過程能夠理解為,在各個節點都更新了各自的前繼和后繼節點之后。每一個節點再把自己的finger表過一遍,更新某些后繼節點。
總結來說,如上圖,Nk的增加僅僅會影響其后繼節點Nx的前繼節點選擇,及Nx的前繼節點Ny的后繼節點選擇,即這三個節點之間的互相指認可能會變化。
但前提是Nx和Ny必須是最接近Nk的直接前繼和直接后繼。如右側圖中在Ny前面,會不會有Nz更接近Nk,而應該是Nk的后繼呢?答案是否定的。從原理角度看,Chord算法保證了這件事(Finger表和直接前/后繼的定義有一定的特殊性)。還有一方面,事實上上面的圖中的N0節點可能並非處在那個位置。為了幫助理解,以下具體說明當N0在join這步為Nk第一次找后繼的過程。
在第一次選擇Nx的時候。是通過N0的findSuccessor(key)找的(即2.1步驟做的事)。
給定一個key位置。查找successor:
1. 推斷key位置是否在本節點和本節點后繼節點之間,假設是,那么就把本節點的后繼節點返回。否則進入2。
2. 為該key找本節點近期的前繼節點(不一定是本節點的直接后繼節點),利用本節點前繼節點的findSuccessor方法來找該key的后繼節點,即進入遞歸,回到1。
要注意的是找近期的前繼節點這一步。依賴的是本節點的finger表。按finger表倒序找。找到那個節點滿足finger節點在目標key位置和本節點之間。就ok。
從直觀上理解,finger表里的倒序第一個點距離本節點的距離最多是半個環。即2m-1。倒序第二個的距離是2m-1+2m-2。也就是說,這樣的倒序找是非常大范圍的找,以這樣的方式找到節點再調用findSuccessor方法找key的后繼,能夠腦補一下這樣的掃環的方式。(為幫助理解。畫了一個餅)

所以N0第一次幫Nk找的后繼Nx,以及Nx本身的前驅Ny,它們這四個點之間的位置關系,事實上出現情況非常復雜。不一定像我上面畫的那樣分布,我畫的圖甚至可能是錯的,比例也全然不是那樣的。
怎樣證明Nz的不存在性,相信在了解Chord數學原理后應該能夠比較好地理解。
上述描寫敘述了Chord環的生成過程和新增節點具體實現。
集群版
集群版指節點之間可能是存在於local的一個JVM內的Chord網絡,也能夠是存在於多個JVM之間的remote的Chord網絡。
假設是Remote情況下的話,對照上述實現,節點的join步驟會添加節點之間通信環節。總體實現思路是一致的,僅僅是集群形式要處理的內容會更豐富些。
針對通信這點,Open chord提供chord node之間的幾種通信協議:Local的話是在單個JVM內。Remote的話包含rmi和socket。
Open chord還提供了在每一個節點上同意存儲序列化后的java對象。實際上是在Chord算法基礎上對節點的一種利用。在此不展開。
ChordImpl
ChordImpl包括元素:
NodeImpl(localNode),Entries,線程池,HashFunction,References,URL,ID
NodeImpl代表着Chord環上的節點,具備一些能夠被遠程節點調用的操作。
Entries是數據K,V對;
References里包含了一個前繼。一串后繼和數據Entries;
URL和ID是localNode的url和id;
findSuccessor(key)過程:
首先檢查本節點有沒有后繼,假設沒有,說明是唯一的環上的點,返回本節點就能夠了。
然后檢查key是不是在本節點和后繼之間,假設是的話,返回后繼節點。在這一步里,本來在返回后繼之前。會進行一次ping來檢查后繼是否正常存活,只是這段被凝視掉了。彌補的做法是返回后繼過程中假設出異常,就覺得后繼節點聯系不上,那么把它從reference表里刪除,隱式效果就是后繼list里會有新的后繼節點占領list首位。所以繼續遞歸findSuccessor(key)操作。
假設以上情況都不是,那么就找一個最接近該key的前繼節點。調用那個節點的findSuccessor(key)遞歸查找,和單線程版里的實現是一致的。這個查找最接近該key的前繼節點的過程詳細是Refereces類的getClosestPrecedingNode(key)方法。不會檢測存活性,是個同步方法。這種方法邏輯比較復雜。
節點操作
create
多個create方法,終於使用createHelp(),作用是創建一個新的ring,前提是localURL和localID都已經設置好了。
create的過程是,把entries(存數據),references(引用node)。localNode(為了通信)都初始化出來。然后調用createTasks操作,createTasks操作利用線程池。定時運行三種任務,各自是周期性穩固與后繼的關系(StabilizeTask)、周期性更新finger表(FixFingerTask)、周期性檢測前繼節點健康情況(CheckPredecessorTask)。最后,調用localNode的acceptEntries方法接收其它node傳來的處理entries的通信操作。
周期性穩固與后繼的關系(StabilizeTask):
檢測后繼節點的前繼節點有沒有由於本節點發送變化。在單線程實現版本號里,這個非常好了解,可是open chord里的實現沒怎么看明確。
周期性更新finger表(FixFingerTask):
採用生成隨機數的方式,隨機檢測一個值相應的successor,假設這個節點不在references內部維護的內容(finger表或前繼或后繼列表)里。則添加該節點的引用
周期性檢測前繼節點健康情況(CheckPredecessorTask):
從References里得到前繼,不為空的話則進行一次ping操作。Ping事實上什么也沒做,但能返回不出異常的話。說明節點正常存活着。
join
多個join方法,終於調用joinHelp(bootstrapURL)方法,用於把本ChordNode增加到指定的URL的Chord ring里。
前提是localURL和localID都已經設置好了。
join的過程是,把entries(存數據),references(引用node),localNode(為了通信)都初始化出來。然后通過Proxy.createConnection(localUrl,bootstrapUrl)來連接本節點和目標URL,Proxy類用於代表本節點外的其它nodes,有LocalThread,RMI,Socket三種實現,相應的是不同的通信協議。
連接完后返回一個遠程node,然后調用遠程node的findSuccessor方法找到本節點的后繼。
然后調用遠程node的notifyAndCopyEntries把它的前繼、后繼列表和entries引用都取過來,利用這個ref集合,首先建立本節點的前驅,即假設ref大小是1,那么說明ring里就兩個node,那么后繼也是本節點的前繼;假設ref大於1,那么取出第一個節點。即后繼的原前繼,比較本節點是否在他倆之間,假設是的話。本節點的前繼就是后繼的原前繼,否則后繼節點是錯誤的,把ref里的第一個節點作為后繼,調用notifyAndCopyEntries。循環上述步驟找出本節點的正確前繼。這個過程中。會不斷往references里加入節點,也解釋了為什么reference里存的是一個后繼list。
確定完前繼和后繼后,把entries的引用加入為本節點的entries,最后運行本節點的acceptEntries和createTasks方法(同create的最后兩步)。
leave
leave操作,關閉本節點線程池,並通知前繼和后繼,本節點要離開網絡。然后讓localNode斷開連接,賦null。
數據存取操作
insert
Insert(key,data),依照key的hash值在環上找到相應node,讓node把這個序列化后的data和key以Entry的形式存起來。調用的是node的insertEntry方法。找node過程調用的是本節點的findSuccessor(key)方法。
retrieve
Retrieve(key),依照key的hash值在環上找到相應node,調用node的retrieveEntries方法得到一個entry的Set返回。找node過程調用的是本節點的findSuccessor(key)方法。
remove
Remove(key,data)。依照key的hash值在環上找到相應node,調用的是node的removeEntry方法。找node過程調用的是本節點的findSuccessor(key)方法。
以上三種操作還配有異步方法。詳細不展開。
命令行操作
把Chord放到集群環境里之后,除了對節點進行join來增加已有網絡外。更多操作都會涉及類似的若干次通信,包含移除節點,展示節點Finger表等操作。
這些操作的實現,通過上述描寫敘述的新增節點。實現上應該非常好理解。以下列舉了open chord的命令行提供的對local或remote chord網絡進行的操作列表:
- CreateNode,創建多個節點
- Insert,插入數據到local網絡里
- InsertNetwork,插入數據到remote網絡里
- CrashNodes,消滅local網絡里的若干個節點
- JoinNetwork,增加節點到remote網絡里
- LeaveNetwork。離開remote網絡
- Remove。從local網絡里刪除數據
- RemoveNetwork,從remote網絡里刪除數據
- Retrieve,從local網絡里獲取數據
- RetrieveNetwork,從remote網絡里獲取數據
- ShowEntries。展示local網絡里每一個node的entry
- ShowEntriesNetwork,展示remote網絡里每一個node的entry
- ShowFingerTable,展示local網絡里指定node的finger table
- ShowFingerTableNetwork,展示remote網絡里指定node的finger table
- ShowNodes。展示local網絡里全部nodes
- ShowSuccessorList,展示local網絡里指定node的后繼列表
- ShutdownNodes。關閉一系列nodes
- Wait,堵塞console一段時間
local/單JVM下創建環樣例:
oc > create -names n1 Creating new chord network. oc > show Node list in the order as nodes are located on chord ring: Node n1 with id 65 52 9C 8C oc > help For help with any command, type name of command plus '-h' or '-help'. Parameters of commands are always passed to them in the format '-parametername parametervalue'. Some parameters require no value, so only the parameter name has to be provided to the command. Commands available from this console: ----- insert, retrieveN, insertN, refs, cprotocol, executeMacro, removeN, refsN, entriesN, create, entries, retrieve, successors, wait, shutdown, crash, exit, help, joinN, displaySystemOut, show, remove, leaveN ----- Note: Commands and parameters are case sensitive. oc > create -names n2_n3 -bootstraps n1 Starting node with name 'n2' with bootstrap node 'n1' Starting node with name 'n3' with bootstrap node 'n1' oc > show Node list in the order as nodes are located on chord ring: Node n1 with id 65 52 9C 8C Node n2 with id 9B 1A 3A B3 Node n3 with id 9C 47 20 AB oc > refs -node n1 Retrieving node n1 Node: 65 52 9C 8C , oclocal://n1/ Finger table: 9B 1A 3A B3 , oclocal://n2/ (0-157) Successor List: 9B 1A 3A B3 , oclocal://n2/ 9C 47 20 AB , oclocal://n3/ Predecessor: 9C 47 20 AB , oclocal://n3/ oc > create -names n22_b1_o1p3_o2_ooi1_onf8_jiow_09ni_90j0_jfn_n23_j902n_j9_noi32_n9_j92 -bootstraps n2_n3 Starting node with name 'n22' with bootstrap node 'n2' Starting node with name 'b1' with bootstrap node 'n3' Starting node with name 'o1p3' with bootstrap node 'n3' Starting node with name 'o2' with bootstrap node 'n3' Starting node with name 'ooi1' with bootstrap node 'n3' Starting node with name 'onf8' with bootstrap node 'n3' Starting node with name 'jiow' with bootstrap node 'n3' Starting node with name '09ni' with bootstrap node 'n3' Starting node with name '90j0' with bootstrap node 'n3' Starting node with name 'jfn' with bootstrap node 'n3' Starting node with name 'n23' with bootstrap node 'n3' Starting node with name 'j902n' with bootstrap node 'n3' Starting node with name 'j9' with bootstrap node 'n3' Starting node with name 'noi32' with bootstrap node 'n3' Starting node with name 'n9' with bootstrap node 'n3' Starting node with name 'j92' with bootstrap node 'n3' oc > refs -node n1 Retrieving node n1 Node: 65 52 9C 8C , oclocal://n1/ Finger table: 9B 1A 3A B3 , oclocal://n2/ (0-157) BB 5B 1A 3D , oclocal://j902n/ (158) E7 9D 67 6A , oclocal://n23/ (159) Successor List: 9B 1A 3A B3 , oclocal://n2/ 9C 47 20 AB , oclocal://n3/ Predecessor: 5D FC 4C 35 , oclocal://o1p3/ oc > refs -node n23 Retrieving node n23 Node: E7 9D 67 6A , oclocal://n23/ Finger table: F8 FE D0 C3 , oclocal://n22/ (0-156) 0E AF 2E B7 , oclocal://09ni/ (157) 2C F2 8A 7F , oclocal://ooi1/ (158) 9B 1A 3A B3 , oclocal://n2/ (159) Successor List: F8 FE D0 C3 , oclocal://n22/ F9 19 F3 7F , oclocal://b1/ Predecessor: CE 4B 77 E3 , oclocal://jfn/ oc > joinN Creating new chord overlay network! URL of created chord node ocsocket://10.65.17.185/.
總結
以下簡單總結我對Chord的理解。
Chord這樣的DHT的實現。本質上是在一致性哈希的基礎上,添加了Finger表這樣的高速路由信息,通過在節點上保存整個網絡的部分信息,讓節點的查找/路由以O(logN)的代價實現,適合大型p2p網絡。
所以,我理解的Chord基本就等於一致性哈希+Finger表。