1. 程式人生 > >以太坊原始碼解讀(7)以太坊的P2P網路基礎

以太坊原始碼解讀(7)以太坊的P2P網路基礎

一、分散式網路的來歷

基於P2P技術的應用有很多,包括檔案分享,即時通訊,協同處理,流媒體通訊等等。其中檔案分享和下載是p2p技術最集中體現。其中,DHT技術是目前很多分散式系統所普遍採用的方案,也包括以太坊。所以這裡先要對DHT技術有所瞭解。

二、DHT(Distributed Hash Table)技術簡介

DHT全稱叫分散式雜湊表(Distributed Hash Table),是一種分散式儲存方法,一類可由鍵值來唯一標示的資訊按照某種約定/協議被分散地儲存在多個節點上,這樣可以有效地避免“中央集權式”的伺服器(比如:tracker)的單一故障而帶來的整個網路癱瘓。

1)首先我們需要弄懂一個概念——雜湊表。

雜湊表是一種資料結構,是根據關鍵碼值(Key value)而直接進行訪問的資料結構。也就是說,它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度。這個對映函式叫做雜湊函式,存放記錄的陣列叫做雜湊表

(陣列與連結串列的結合)

雜湊表的具體實現過程是:把Key通過一個固定的演算法函式既所謂的雜湊函式轉換成一個整型數字,然後就將該數字對陣列長度進行取餘,取餘結果就當作陣列的下標,將value儲存在以該數字為下標的陣列空間裡。這個陣列空間,我們可以稱之為一個“bucket”,也就是說bucket裡存的是多個鍵值對,鍵值對以連結串列的形式組織。

某個具體的雜湊表,bucket的數量通常是固定的(比如N個),桶的編號從0~N-1,分別就是上面取餘之後的結果。當使用雜湊表進行查詢的時候,就是再次使用雜湊函式將key轉換為對應的陣列下標,並定位到該空間獲取value,如此一來,就可以充分利用到陣列的定位效能進行資料定位。

特點:這種雜湊表的特點是建立雜湊表(HashMap)需要先指定大小,這導致如果雜湊表存滿了要擴容的時候就會有很大麻煩。

舉個例子:

假設雜湊函式為 hash(x)=x ,雜湊表的長度為5(有5個桶)
key=6時,hash(6)%5 = 1,即Key為6的元素儲存在第一個桶中
key=9時,hash(9)%5 = 4,即Key為9的元素儲存在第四個桶中
Key=17時,hash(17)%5=2,即Key為17的元素儲存在第二個桶中....

假設現在hash表長度擴容成8,那麼Key為6,7,8 的資料全都需要重新雜湊。因為,
key=6時,hash(6)%8 = 6,即Key為6的元素儲存在第六個桶中
key=9時,hash(9)%8 = 1,即Key為9的元素儲存在第一個桶中
Key=14時,hash(14)%8=6,即key為14的元素儲存在第六個桶中....

從上可以看出:擴容之後,元素的位置全變了。所以雜湊表在建立後就不要輕易的擴容。

2)什麼是分散式雜湊表?

DHT全稱叫分散式雜湊表(Distributed Hash Table),是一種分散式儲存方法。在不需要伺服器的情況下,每個客戶端負責一個小範圍的路由,並負責儲存一小部分資料,從而實現整個DHT網路的定址和儲存。DHT技術的應用來源於p2p網路發展的需要。第二代p2p檔案共享系統正是由於查詢節點十分困難且耗費網路資源而促進了第三代系統引入了DHT技術,用以快速的查詢節點以及資源。

2.1 異同    分散式雜湊表與雜湊表的共同之處在於能夠實現快速的查詢。它與上面雜湊表的不同在於:1)雜湊表通常是本地的,用於在本地快速的插入和查詢資料。而分散式雜湊表相當於將雜湊表中的bucket分散到不同的節點計算機中。2)雜湊表增添、刪除桶會導致所有的資料需要重新hash,但分散式雜湊表支援動態的節點的數目,節點可以隨意的進入或退出。

2.2 一致性雜湊    這種動態節點是如何實現的呢?不得不提的就是一致性雜湊。一致性雜湊的具體實現我們這裡不作介紹,我們只要知道它是一種經典的分散式雜湊表的實現方法。它是對機器(通常是其IP地址)和資料(通常是其KEY值)進行統一的運算,把他們全都對映到一個地址空間中。假設我們需要把值對映到32bit的地址空間,那麼我們的地址就可以分佈在從0到2^32 – 1的範圍內。然後我們將這些地址首尾相連構成一個環狀,即一致性雜湊的拓撲結構。

如上圖,A、B、C、D、E五個節點分別在整個地址空間的5個位置,分別負責由其上一個節點到本節點的地址空間,即A負責E~A的地址空間上的所有資料,B負責A~B的地址空間的所有資料,以此類推。如果某一資料(KEY值)對映到地址空間上的位置在AB之間,那麼這部分資料將儲存在B節點上。

因此,當有一個新的節點插入的時候,比如AB之間插入一個A',那麼整個網路上其他節點的儲存是不用變的,只要移動一部分B節點上的資料到A'上即可。

2.3 路由表:實現快速查詢   

使用分散式雜湊錶快速的查詢指定的資料在哪個節點,即資料定位,是通過特殊的路由方案解決的。最普通的路由方案就是當一個節點接收到某個“key”的查詢時,先看 key 是否在自己這裡。如果在自己這裡,就直接返回資訊;否則就把 key 轉發給自己的繼任者。以此類推。

這種玩法的時間複雜度是 O(N)。對於一個節點數很多的 DHT 網路,這種做法顯然非常低效。

成熟的DHT技術中使用的是更加高階的路由方案,比如在Chord中,用到一個叫做“Finger Table”的機制,或者在Kademlia中所使用的"K-桶",其實就是一種特殊的路由表。而路由表的目標就是:儘可能地將查詢key傳送到離儲存這個key最近的那臺機器上。

上面提到的Chord和Kademlia是兩種不同的DHT技術,他們所實現的DHT具有不同的拓撲結構,自然也就有不同的查詢方案(路由表)。Chord所採用的拓撲結構就是上面所說的環形結構,我們來簡單看看Chord的拓撲結構中如何進行路由以及如何插入新節點。

【“Finger Table”】

指標表(Finger Table)是每個節點都持有的一個路由表,這個表最多包含m項(m是雜湊值的位元數),從0~m-1算起,每一項都對應著某一個節點的ID,其中第i項與其對應的節點ID的關係是:(n+2^) mod 2^m。

【查詢】當某個節點收到某個key的查詢時,先在本地查詢,如果沒有則選擇最大的且不超過key的那一項,然後把 key 轉發給這一項對應的節點,遞迴執行上面查詢過程,知道找到對應的資料。

【節點插入】節點接入網路時,首先與任意一個已知節點建立連線,通過指標表查詢到自己節點ID的“前任”節點和“繼任”節點。然後將該節點插入到拓撲結構中,並與其相鄰的節點調整並獲取節點內容。

【節點正常退出】

如果某個節點想要主動離開這個 DHT 網路,按照約定需要作一些善後的處理工作,比如通知前任節點去更新其繼任節點。

【節點異常異常】

作為一個分散式系統,任何節點都有可能意外下線。為了保險起見,Chord 引入了一個“繼任者候選列表”的概念。每個節點都用這個列表來包含:距離自己最近的 N 個節點的資訊,順序是【由近到遠】。一旦自己的繼任者下線了,就在列表中找到一個【距離最近且線上】的節點,作為新的繼任者。然後 節點A 更新該列表,確保依然有 N 個候選。更新完“繼任者候選列表”後,節點A 也會通知自己的前任,那麼 A 的前任也就能更新自己的“繼任者候選列表”。

Chord 就介紹到這裡,我們的重點是Kademila,因為實際應用的 DHT 大部分都採用 Kad 及其變種,包括以太坊的分散式網路。想要進一步瞭解的話可以參考其原創論文:

Chord——A scalable peer-to-peer lookup service for internet applications

三、Kademlia協議

與Chord不同,Kademlia所用的拓撲結構是二叉樹。

二叉樹知識補充

在電腦科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。

特點:
1、樹的深度:樹中最大的結點層。深度為k的完全二叉樹,至多有2k-1個葉子節點,至多有2k-1個節點。
2、樹的結點(node):包含一個數據元素及若干指向子樹的分支;
3、孩子結點(child node):結點的子樹的根稱為該結點的孩子;
4、雙親結點:B 結點是A 結點的孩子,則A結點是B 結點的雙親;
5、兄弟結點:同一雙親的孩子結點; 堂兄結點:同一層上結點;
6、結點的度:結點子樹的個數;
7、葉子結點:也叫終端結點,是度為 0 的結點;

在Kad網路中,所有節點都被當作一顆二叉樹的葉子,並且每一個節點的位置都由其ID值的最短字首唯一確定。

Q1:如何把節點對映到二叉樹?

1)先把key以二進位制的形式表示,進行“最短唯一字首”來處理;
2)二進位制的第n位代表二叉樹的第n層,這樣一個子樹的每個節點連起來就是完整的id二進位制表示;
3)“1”代表進入左子樹,“0”代表進入右子樹(反過來也行)
4)按上面的步驟處理後得到到最後的葉子節點,就是該“key”對應的節點。

Q2:Kad網路的路由機制是怎樣的?

【二叉樹的拆分】對於任意一個節點,都可以把這個二叉樹分解為一系列連續的,不包含自己的子樹。最高層的子樹,由整棵樹不包含自己的樹的另一半組成。下一層子樹由剩下部分不包含自己的一半組成,以此類推,知道分割完整棵樹。

Kad 預設的雜湊值空間是 m=160(雜湊值有 160 位元),因此二叉樹最大深度為160,拆分出來的子樹最多有 160 個。如上圖中,節點的最小字首為0011,節點深度為4,剛好拆分出4個子樹。

每個節點都能夠以自己的視角拆分整個二叉樹。對於每一個節點而言,當它以自己的視角完成子樹拆分後,會得到 n 個子樹;對於每個子樹,如果能知道里面的一個節點,那麼它就可以利用這 n 個節點進行遞迴路由,從而遍歷整個子樹的所有節點。

【K-桶】上面說的知道子樹的一個節點就可以遍歷整個子樹,但實際上由於分散式系統的節點總在變化,所以Kad網路規定了每個節點要記錄每個拆分子樹中的K個節點(比如BT下載設定K=8)。這樣每個子樹的K個節點被記錄在一個路由表中,節點如果有n個拆分子樹就要控制n個路由表。這裡的路由表就是所謂的“K-桶”

針對上圖的id=0011節點,應該有四個k-桶:

i 距離 節點資訊
0 [2^0, 2^1) (ip, udp port, node ID)0-1
(ip, udp port, node ID)0-2
...
(ip, udp port, node ID)0-k
1 [2^1, 2^2) (ip, udp port, node ID)1-1
(ip, udp port, node ID)1-2
...
(ip, udp port, node ID)1-k
2 [2^2, 2^3) (ip, udp port, node ID)2-1
(ip, udp port, node ID)2-2
...
(ip, udp port, node ID)2-k
3 [2^3, 2^4) (ip, udp port, node ID)3-1
(ip, udp port, node ID)3-2
...
(ip, udp port, node ID)3-k

這裡節點的【距離】是通過異或的二進位制計算得來:

這兩點的距離就是2^5 + 2^2 = 36。因此上面的四個k-桶就分別儲存了距離從近到遠的四個範圍的k個節點。【可以把這種計算當成是二叉樹上兩個節點距離的計算方式】

需要注意的是,k-桶的列表不是一成不變的,而是不斷重新整理的。每個k-桶內部存放資訊的位置是根據上一次看到的時間順序排列的,最近(least-recently)看到的放在頭部,最後(most-recently)看到的放在尾部。PING不通的節點要進行刪除。

Q3、新節點新增的時候會有哪些動作?

新節點加入時,主要是先要構建自己的k-桶,需要進行如下步驟:
1)新節點首先向任意一個節點發起查詢請求(FIND_NODE),查詢自己的ID;
2)節點收到請求後,按照距離把新節點放到自己對應的K-桶中,然後按照協議找到K個與新節點最接近的節點返回;
3)新節點收到返回的節點資訊後,即可初始化自己的k-桶,並向這些節點發送查詢請求,如此往復(遞迴),建立起自己的路由表。

當舊節點收到一個新節點資訊的時候,要進行如下步驟:
1)計算自己與新節點的距離,根據這個距離,選擇對應的k-桶進行操作;
2)如果新節點的ip已經在這個k-桶,則將這一項移動到k-桶的尾部;
3)如果新節點的ip不在這個k-桶:
a)如果k-桶記錄小於k,則直接把傳送者新增進去;
b)如果k-桶記錄大於等於k,則選擇頭部的節點進行PING操作,如果沒有響應,則刪除這個節點,新增新節點到尾部;如果有響應,就忽略新節點;

Kad 就介紹到這裡。如果想了解更多,可以參考其論文:
Kademlia——A Peer-to-peer information system based on the XOR Metric

p2p網路的基本知識我們先介紹到這裡,下一節開始介紹以太坊基於kad網路的p2p原始碼解析。