死磕以太坊原始碼分析之Kademlia演算法

mindcarver發表於2020-11-22

死磕以太坊原始碼分析之Kademlia演算法

KAD 演算法概述

Kademlia是一種點對點分散式雜湊表(DHT),它在容易出錯的環境中也具有可證明的一致性和效能。使用一種基於異或指標的拓撲結構來路由查詢和定位節點,這簡化了演算法並有助於證明。該拓撲結構有一個特點:每次訊息交換都能夠傳遞或強化有效資訊。系統利用這些資訊進行併發的非同步查詢,可以容忍節點故障,並且故障不會導致使用者超時。

KAD演算法要處理的問題

  1. 如何分配儲存內容到各個節點,新增/刪除內容如何處理
  2. 如何找到儲存檔案的節點/地址/路徑

節點狀態

節點的基本屬性包括如下:

  • 節點ID,Node ID
  • 節點IP地址與埠號

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

對於任意一個節點,都可以把這顆二叉樹分解為一系列連續的,不包含自己的子樹。最高層的子樹,由整顆樹不包含自己的樹的另一半組成;下一層子樹由剩下部分不包含自己的一半組成;依此類推,直到分割完整顆樹。圖 1 就展示了節點0011如何進行子樹的劃分:

image-20201122115058412

虛線包含的部分就是各子樹,由上到下各層的字首分別為0,01,000,0010。

Kad 協議確保每個節點知道其各子樹的至少一個節點,只要這些子樹非空。在這個前提下,每個節點都可以通過ID值來找到任何一個節點。這個路由的過程是通過所謂的 XOR(異或)距離得到的。

圖 2 就演示了節點0011如何通過連續查詢來找到節點1110的。節點0011通過在逐步底層的子樹間不斷學習並查詢最佳節點,獲得了越來越接近的節點,最終收斂到目標節點上。

image-20201122115359426

需要說明的是:只有第一步查詢的節點101,是節點0011已經知道的,後面各步查詢的節點,都是由上一步查詢返回的更接近目標的節點,這是一個遞迴操作的過程


節點距離

Kad 網路中每個節點都有一個 160 bit 的 ID 值作為標誌符,Key 也是一個 160 bit 的標誌符,每一個加入 Kad 網路的計算機都會在 160 bit 的 key 空間被分配一個節點 ID(node ID)值(可以認為 ID 是隨機產生的), <key,value> 對的資料就存放在 ID 值“最”接近 key 值的節點上。

判斷兩個節點 x,y 的距離遠近是基於數學上的異或的二進位制運算, d(x,y)=x⊕y ,既對應位相同時結果為0,不同時結果為1。例如:

    010101
XOR 110001
----------
    100100

則這兩個節點的距離為 32+4=36 。

顯然,高位上數值的差異對結果的影響更大。

對於異或操作,有如下一些數學性質:

  • 兩個節點間的距離是隨機的
  • 節點與自身的距離是0
  • 對稱性。A 到 B 的距離和 B 到 A 的距離相等
  • 三角不等。distance(A,B)+distance(B,C) <= distance(A,C)

對於任意給定的節點 x 和距離 Δ≥0 ,總會存在一個精確的節點 y ,使得 d(x,y)=Δ 。另外,單向性也確保了對於同一個 key 值的所有查詢都會逐步收斂到同一個路徑上,而不管查詢的起始節點位置如何。這樣,只要沿著查詢路徑上的節點都快取這個 <key,value> 對,就可以減輕存放熱門 key 值節點的壓力,同時也能夠加快查詢響應速度。

K桶

K 桶的概念

Kad 的路由表是通過一些稱之為 K 桶的表格構造起來的。

對每一個 0≤i≤160 ,每個節點都儲存有一些和自己距離範圍在區間 [2i,2i+1) 內的一些節點資訊,這些資訊由一些 (IP address,UDP port,Node ID) 資料列表構成(Kad 網路是靠 UDP 協議交換資訊的)。每一個這樣的列表都稱之為一個 K 桶,並且每個 K 桶內部資訊存放位置是根據上次看到的時間順序排列,最近( least-recently)看到的放在頭部,最後(most-recently)看到的放在尾部。每個桶都有不超過 k 個的資料項。

一個節點的全部 K 桶列表如下圖 所示:

image-20201122143202816

當 i 值很小時,K 桶通常是空的(也就是說沒有足夠多的節點,比如當 i = 0 時,就最多可能只有1項);而當 i 值很大時,其對應 K 桶的項數又很可能會超過 k 個(當然,覆蓋距離範圍越廣,存在較多節點的可能性也就越大),這裡 k 是為平衡系統效能和網路負載而設定的一個常數,但必須是偶數,比如 k = 20。在 BitTorrent 的實現中,取值為 k = 8。

由於每個 K 桶覆蓋距離的範圍呈指數關係增長,這就形成了離自己近的節點的資訊多,離自己遠的節點的資訊少,從而可以保證路由查詢過程是收斂。因為是用指數方式劃分割槽間,經過證明,對於一個有 N 個節點的 Kad 網路,最多隻需要經過 logN 步查詢,就可以準確定位到目標節點。

K桶更新機制

當節點 x 收到一個 PRC 訊息時,傳送者 y 的 IP 地址就被用來更新對應的 K 桶,具體步驟如下:

  1. 計算自己和傳送者的距離: d(x,y)=x⊕y ,注意:x 和 y 是 ID 值,不是 IP 地址
  2. 通過距離 d 選擇對應的 K 桶進行更新操作
  3. 如果 y 的 IP 地址已經存在於這個 K 桶中,則把對應項移到該該 K 桶的尾部
  4. 如果 y 的 IP 地址沒有記錄在該 K 桶中
    1. 如果該 K 桶的記錄項小於 k 個,則直接把 y 的 (IP address, UDP port, Node ID) 資訊插入佇列尾部
    2. 如果該 K 桶的記錄項大於 k 個,則選擇頭部的記錄項(假如是節點 z)進行 RPC_PING 操作
      1. 如果 z 沒有響應,則從 K 桶中移除 z 的資訊,並把 y 的資訊插入佇列尾部
      2. 如果 z 有響應,則把 z 的資訊移到佇列尾部,同時忽略 y 的資訊

K 桶的更新機制非常高效的實現了一種把最近看到的節點更新的策略,除非線上節點一直未從 K 桶中移出過。也就是說線上時間長的節點具有較高的可能性繼續保留在 K 桶列表中。

所以,通過把線上時間長的節點留在 K 桶裡,Kad 就明顯增加 K 桶中的節點在下一時間段仍然線上的概率,這對應 Kad 網路的穩定性和減少網路維護成本(不需要頻繁構建節點的路由表)帶來很大好處。

這種機制的另一個好處是能在一定程度上防禦 DOS 攻擊,因為只有當老節點失效後,Kad 才會更新 K 桶的資訊,這就避免了通過新節點的加入來泛洪路由資訊。

為了防止 K 桶老化,所有在一定時間之內無更新操作的 K 桶,都會分別從自己的 K 桶中隨機選擇一些節點執行 RPC_PING 操作。

上述這些 K 桶機制使 Kad 緩和了流量瓶頸(所有節點不會同時進行大量的更新操作),同時也能對節點的失效進行迅速響應。


協議訊息

Kademlia 協議包括四種遠端 RPC 操作:PING、STORE、FIND_NODE、FIND_VALUE。

  1. PING 操作的作用是探測一個節點,用以判斷其是否仍然線上。

  2. STORE 操作的作用是通知一個節點儲存一個 <key,value> 對,以便以後查詢需要。

  3. FIND_NODE 操作使用一個 160 bit 的 ID 作為引數。本操作的接受者返回它所知道的更接近目標 ID 的 K 個節點的 (IP address, UDP port, Node ID) 資訊。

    這些節點的資訊可以是從一個單獨的 K 桶獲得,也可以從多個 K 桶獲得(如果最接近目標 ID 的 K 桶未滿)。不管是哪種情況,接受者都將返回 K 個節點的資訊給操作發起者。但如果接受者所有 K 桶的節點資訊加起來也沒有 K 個,則它會返回全部節點的資訊給發起者。

  4. FIND_VALUE 操作和 FIND_NODE 操作類似,不同的是它只需要返回一個節點的 (IP address, UDP port, Node ID) 資訊。如果本操作的接受者收到同一個 key 的 STORE 操作,則會直接返回儲存的 value 值。

    注:在 Kad 網路中,系統儲存的資料以 <key,value> 對形式存放。根據筆者的分析,在 BitSpirit 的 DHT 實現中,其 key 值為 torrent 檔案的 info_hash 串,其 value 值則和 torrent 檔案有密切關係。

為了防止偽造地址,在所有 RPC 操作中,接受者都需要響應一個隨機的 160 bit 的 ID 值。另外,為了確信傳送者的網路地址,PING 操作還可以附帶在接受者的 RPC 回覆資訊中(在上述 4種操作中 接受者回復 傳送者時,可以攜帶上 接受者對 傳送者的 PING, 以此校驗 傳送者是否還健在)。


路由查詢

Kad 技術的最大特點之一就是能夠提供快速的節點查詢機制,並且還可以通過引數進行查詢速度的調節。

假如節點 x 要查詢 ID 值為 t 的節點,Kad 按照如下遞迴操作步驟進行路由查詢:

  1. 計算到 t 的距離: d(x,y)=x⊕y
  2. 從 x 的第 [logd] 個 K 桶中取出 α 個節點的資訊(“[”“]”是取整符號),同時進行 FIND_NODE 操作。如果這個 K 桶中的資訊少於 α 個,則從附近多個桶中選擇距離最接近 d 的總共 α 個節點。
  3. 對接受到查詢操作的每個節點,如果發現自己就是 t,則回答自己是最接近 t 的;否則測量自己和 t 的距離,並從自己對應的 K 桶中選擇 α 個節點的資訊給 x。
  4. X 對新接受到的每個節點都再次執行 FIND_NODE 操作,此過程不斷重複執行,直到每一個分支都有節點響應自己是最接近 t 的。
  5. 通過上述查詢操作,x 得到了 k 個最接近 t 的節點資訊。

注意:這裡用“最接近”這個說法,是因為 ID 值為 t 的節點不一定存在網路中,也就是說 t 沒有分配給任何一臺電腦。

這裡 α 也是為系統優化而設立的一個引數,就像 K 一樣。在 BitTorrent 實現中,取值為 α=3 。

當 α=1 時,查詢過程就類似於 Chord 的逐跳查詢過程,如圖 4。

image-20201122133505567

整個路由查詢過程是遞迴操作的,其過程可用數學公式表示為:

N0=x (即查詢操作的發起者)

N1=find ⎯noden0(t)

N2=find ⎯noden1(t)

... ...

Nl=find ⎯nodenl−1(t)

這個遞迴過程一直持續到 Nl=t ,或者 Nl 的路由表中沒有任何關於 t 的資訊,即查詢失敗。

由於每次查詢都能從更接近 t 的 K 桶中獲取資訊,這樣的機制保證了每一次遞迴操作都能夠至少獲得距離減半(或距離減少 1 bit)的效果,從而保證整個查詢過程的收斂速度為 O(logN) ,這裡 N 為網路全部節點的數量。

當節點 x 要查詢 <key,value> 對時,和查詢節點的操作類似,x 選擇 k 個 ID 值最接近 key 值的節點,執行 FIND_VALUE 操作,並對每一個返回的新節點重複執行 FIND_VALUE 操作,直到某個節點返回 value 值。

一旦 FIND_VALUE 操作成功執行,則 <key,value> 對資料會快取在沒有返回 value 值的最接近的節點上。這樣下一次查詢相同的 key 時就會更加快速的得到結果。通過這樣的方式,熱門 <key,value> 對資料的快取範圍就逐步擴大,使系統具有極佳的響應速度( cache 為存活24小時,但是目標節點上的內容時每1小時向其他最近節點重新發布<key, value>使得資料的超時時間得以重新整理,而遠離目標節點的節點的資料存活時間當然就可能不會被重新發布到,所以也就是資料快取的超時時間和節點的距離成反比)


資料儲存

存放 <key,value> 對資料的過程為:

  1. 發起者首先定位 k 個 ID 值最接近 key 的節點
  2. 發起者對這 k 個節點發起 STORE 操作
  3. 執行 STORE 操作的 k 個節點每小時重發布自己所有的 <key,value> 對資料
  4. 為了限制失效資訊,所有 <key,value> 對資料在初始釋出24小時後過期

另外,為了保證資料釋出、搜尋的一致性,規定在任何時候,當節點 w 發現新節點 u 比 w 上的某些 <key,value> 對資料更接近,則 w 把這些 <key,value> 對資料複製到 u 上,但是並不會從 w 上刪除。


節點的加入和離開

如果節點 u 要想加入 Kad 網路,它必須要和一個已經在 Kad 網路的節點,比如 w,取得聯絡。

u 首先把 w 插入自己適當的 K 桶中,然後對自己的節點 ID 執行一次 FIND_NODE 操作 (向 w 釋出 查詢 u 的 FIND_NODE 請求),然後根據接收到的資訊更新自己的 K 桶內容。通過對自己鄰近節點由近及遠的逐步查詢,u 完成了仍然是空的 K 桶資訊的構建,同時也把自己的資訊釋出到其他節點的 K 桶中。

節點 u 為例,其路由表的生成過程為:

  1. 最初,u 的路由表為一個單個的 K 桶,覆蓋了整個 160 bit ID 空間,如圖 6 最上面的路由表;
  2. 當學習到新的節點資訊後,則 u 會嘗試把新節點的資訊,根據其字首值插入到對應的 K 桶中:
    1. 如果該 K 桶沒有滿,則新節點直接插入到這個 K 桶中;
    2. 如果該 K 桶已經滿了,
      1. 如果該 K 桶覆蓋範圍包含了節點 u 的 ID,則把該 K 桶分裂為兩個大小相同的新 K 桶,並對原 K 桶內的節點資訊按照新的 K 桶字首值進行重新分配
      2. 如果該 K 桶覆蓋範圍沒有包節點 u 的 ID,則直接丟棄該新節點資訊
  3. 上述過程不斷重複,最終會形成表 1 結構的路由表。達到距離近的節點的資訊多,距離遠的節點的資訊少的結果,保證了路由查詢過程能快速收斂。

image-20201122145547341

在圖 7 中,演示了當覆蓋範圍包含自己 ID 值的 K 桶是如何逐步分裂的。

image-20201122145609681

當 K 桶 010 滿了之後,由於其覆蓋範圍包含了節點 0100 的 ID,故該 K 桶分裂為兩個新的 K 桶:0101 和 0100,原 K 桶 010 的資訊會根據其其字首值重新分佈到這兩個新的 K 桶中。注意,這裡並沒有使用 160 bit 的 ID 值表示法,只是為了方便原理的演示,實際 Kad 網路中的 ID 值都是 160 bit 的。

節點離開 Kad 網路不需要釋出任何資訊,Kademlia 協議的目標之一就是能夠彈性工作在任意節點隨時失效的情況下。為此,Kad 要求每個節點必須週期性 【一般是: 每小時】 的釋出全部自己存放的 <key,value> 對資料,並把這些資料快取在自己的 k 個最近鄰居處,這樣存放在失效節點的資料會很快被更新到其他新節點上。所以有節點離開了,那麼就離開了,而且節點中的k-桶重新整理機制也能保證會把已經不線上的節點資訊從自己本地k-桶中移除


參考

https://github.com/blockchainGuide ☆☆☆☆☆

https://mindcarver.cn/ ☆☆☆☆☆

https://blog.csdn.net/qq_25870633/article/details/81939101

[http://www.ic.unicamp.br/bit/ensino/mo809_1s13/papers/P2P/Kademlia-%20A%20Peer-to-Peer%20Information%20System%20Based%20on%20the%20XOR%20Metric%20.pdf](http://www.ic.unicamp.br/bit/ensino/mo809_1s13/papers/P2P/Kademlia- A Peer-to-Peer Information System Based on the XOR Metric .pdf)

https://blog.csdn.net/hoping/article/details/5307320

https://www.jianshu.com/p/f2c31e632f1d

https://www.ic.unicamp.br/~bit/ensino/mo809_1s13/papers/P2P/Kademlia- A Peer-to-Peer Information System Based on the XOR Metric .pdf

相關文章