1. 程式人生 > >從資料的角度帶你深入瞭解IPFS

從資料的角度帶你深入瞭解IPFS

IPFS 和區塊鏈有著非常緊密的聯絡, 隨著區塊鏈的不斷髮展,對資料的儲存需求也越來越高。本文從IPFS 的底層設計出發, 結合原始碼, 分析了IPFS 的一些技術細節。

一、概述

IPFS 和區塊鏈有著非常緊密的聯絡, 隨著區塊鏈的不斷髮展,對資料的儲存需求也越來越高, 由於效能和成本的限制,現有的區塊鏈設計方案大部分都選擇了把較大的資料儲存在鏈外,通過對資料進行加密, 雜湊運算等手段來防止資料被篡改, 在區塊鏈上只引用所存資料的hash 值, 從而滿足業務對資料的儲存需求。 本文從IPFS 的底層設計出發, 結合原始碼, 分析了IPFS 的一些技術細節。 由於IPFS還在不斷更新中, 文中引用的部分可能和最新程式碼有所出入。

閱讀本文需要讀者

  • 瞭解網路程式設計
  • 瞭解分散式儲存
  • 瞭解基本的區塊鏈知識

二、什麼是IPFS?

維基百科上是這樣解釋的:是一個旨在建立持久且分散式儲存和共享檔案的網路傳輸協議。

上面的解釋稍顯晦澀, 我的理解是:

1. 首先它是一個FS(檔案系統)

2. 其次它支援點對點傳輸

既然是檔案系統, 那它和普通的檔案系統有什麼區別呢? 有以下幾點區別:

  • 儲存方式: 它是分散式儲存的, 為了方便傳輸,檔案被切分成多個block, 每個block 通過hash運算得到唯一的ID, 方便在網路中進行識別和去重。 考慮到傳輸效率, 同一個block 可能有多個copy, 分別儲存在不同的網路節點上。
  • 內容定址方式: 每個block都有唯一的ID,我們只需要根據節點的ID 就可以獲取到它所對應的block。

那麼問題來了, 既然檔案被切分成了多個block,如何組織這些block 資料,組成邏輯上的檔案呢? 在IFPS中採用的merkledag, 下面是 merkledag的一個示意圖:

簡單來說, 就是2種資料結構merkle 和DAG(有向無環圖)的結合, 通過這種邏輯結構, 可以滿足:

  • 內容定址: 使用hash ID來唯一識別一個數據塊的內容
  • 防篡改: 可以方便的檢查雜湊值來確認資料是否被篡改
  • 去重: 由於內容相同的資料塊雜湊是相同的,可以很容去掉重複的資料,節省儲存空間

確定了資料模型後, 接下來要做的事: 如何把資料分發到不同的網路節點上, 達到分散式儲存和共享的目的? 我們先思考一下, 通過網路,比如HTTP, 訪問某個檔案的步驟,首先我們要知道儲存這個檔案的伺服器地址, 然後我們需要知道這個檔案對應的ID, 比如檔名。前者我們可以抽象成網路節點定址, 後者我們抽象成檔案物件定址; 在IPFS中, 這兩種定址方式使用了相同的演算法, KAD, 介紹KAD演算法的文章很多,這裡不贅述, 只簡單說明一下核心思想:

KAD 最精妙之處就是使用XOR 來計算ID 之間的距離,並且統一了節點ID 和 物件ID的定址方式。 採用 XOR(按位元異或操作)演算法計算 key 之間的“距離”。

這種做法使得它具備了類似於“幾何距離”的某些特性(下面用 ⊕ 表示 XOR)

  • (A ⊕ B) == (B ⊕ A) XOR 符合“交換律”,具備對稱性。
  • (A ⊕ A) == 0 反身性,自身距離為零
  • (A ⊕ B) > 0 【不同】的兩個 key 之間的距離必大於零
  • (A ⊕ B) + (B ⊕ C) >= (A ⊕ C) 三角不等式

通過KAD演算法,IPFS 把不同ID的資料塊分發到與之距離較近的網路節點中,達到分散式儲存的目的。

通過IPFS獲取檔案時,只需要根據merkledag, 按圖索驥,根據每個block的ID, 通過KAD演算法從相應網路節點中下載block資料, 最後驗證是否資料完整, 完成拼接即可。

下面我們再從技術實現的角度做更深入的介紹。

三、IPFS的系統架構

我們先看一下IPFS的系統架構圖, 分為5層:

  • 一層為naming, 基於PKI的一個名稱空間;
  • 第二層為merkledag, IPFS 內部的邏輯資料結構;
  • 第三層為exchange, 節點之間block data的交換協議;
  • 第四層為routing, 主要實現節點定址和物件定址;
  • 第五層為network, 封裝了P2P通訊的連線和傳輸部分。

站在資料的角度來看, 又可以分為2個大的模組:

  • IPLD( InterPlanetary Linked Data) 主要用來定義資料, 給資料建模;
  • libp2p解決的是資料如何傳輸的問題。

下面分別介紹IFPS 中的2個主要部分IPLD 和 libP2P。

1.IPLD

通過hash 值來實現內容定址的方式在分散式計算領域得到了廣泛的應用, 比如區塊鏈, 再比如git repo。 雖然使用hash 連線資料的方式有相似之處, 但是底層資料結構並不能通用, IPFS 是個極具野心的專案, 為了讓這些不同領域之間的資料可互操作, 它定義了統一的資料模型IPLD, 通過它, 可以方便地訪問來自不同領域的資料。

前面已經介紹資料的邏輯結構是用merkledag表示的, 那麼它是如何實現的呢? 圍繞merkledag作為核心, 它定義了以下幾個概念:

  • merkle link 代表dag 中的邊
  • merkel-dag 有向無環圖
  • merkle-path 訪問dag節點的類似unix path的路徑
  • IPLD data model 基於json 的資料模型
  • IPLD serialized format 序列化格式
  • canonical 格式: 為了保證同樣的logic object 總是序列化為一個同樣的輸出, 而制定的確定性規則

圍繞這些定義它實現了下面幾個components

  • CID 內容ID
  • data model 資料模型
  • serialization format 序列化格式
  • tools & libraries 工具和庫
  • IPLD selector 類似CSS 選擇器, 方便選取dag中的節點
  • IPLD transformation 對dag 進行轉換計算

我們知道,資料是多樣性的,為了給不同的資料建模, 我們需要一種通用的資料格式, 通過它可以最大程度地相容不同的資料, IPFS 中定義了一個抽象的集合, multiformat, 包含multihash、multiaddr、multibase、multicodec、multistream幾個部分。

(一)multihash

自識別hash, 由3個部分組成,分別是:hash函式編碼、hash值的長度和hash內容, 下面是個簡單的例子:

這種設計的最大好處是非常方便升級,一旦有一天我們使用的hash 函式不再安全了, 或者發現了更好的hash 函式,我們可以很方便的升級系統。

(二)multiaddr

自描述地址格式,可以描述各種不同的地址

(三)multibase

multibase 代表的是一種編碼格式, 方便把CID 編碼成不同的格式, 比如這裡定義了2進位制、8進位制、10進位制、16進位制、也有我們熟悉的base58btc 和 base64編碼。

(四)multicodec

mulcodec 代表的是自描述的編解碼, 其實是個table, 用1到2個位元組定了資料內容的格式, 比如用字母z表示base58btc編碼, 0x50表示protobuf 等等。

五)multistream

multistream 首先是個stream, 它利用multicodec,實現了自描述的功能, 下面是基於一個javascript 的例子; 先new 一個buffer 物件, 裡面是json物件, 然後給它加一個字首protobuf, 這樣這個multistream 就構造好了, 可以通過網路傳輸。在解析時可以先取codec 字首,然後移除字首, 得到具體的資料內容。

結合上面的部分, 我們重點介紹一下CID。
CID 是IPFS分散式檔案系統中標準的檔案定址格式,它集合了內容定址、加密雜湊演算法和自我描述的格式, 是IPLD 內部核心的識別符。目前有2個版本,CIDv0 和CIDv1。

CIDv0是一個向後相容的版本,其中:

  • multibase 一直為 base58btc
  • multicodec 一直為 protobuf-mdag
  • version 一直為 CIDv0
  • multihash 表示為cidv0 ::= <multihash-content-address>

為了更靈活的表述ID資料, 支援更多的格式, IPLD 定義了CIDv1,CIDv1由4個部分組成:

  • multibase
  • version
  • multicodec
  • multihash

IPLD 是IPFS 的資料描述格式, 解決了如何定義資料的問題, 下面這張圖是結合原始碼整理的一份邏輯圖,我們可以看到上面是一些高階的介面, 比如file, mfs, fuse 等。 下面是資料結構的持久化部分,節點之間交換的內容是以block 為基礎的, 最下面就是物理儲存了。比如block 儲存在blocks 目錄, 其他節點之間的資訊儲存在leveldb, 還有keystore, config 等。

2.資料如何傳輸呢?

接下來我們介紹libP2P, 看看資料是如何傳輸的。libP2P 是個模組化的網路協議棧。

做過socket程式設計的小夥伴應該都知道, 使用raw socket 程式設計傳輸資料的過程,無非就是以下幾個步驟:

  1. 獲取目標伺服器地址
  2. 和目標伺服器建立連線
  3. 握手協議
  4. 傳輸資料
  5. 關閉連線

libP2P 也是這樣,不過區別在於它把各個部分都模組化了, 定義了通用的介面, 可以很方便的進行擴充套件

(一)架構圖

由以下幾個部分組成,分別是:

  • Peer Routing
  • Swarm (傳輸和連線)
  • Distributed Record Store
  • Discovery

下面我們對它們做分別介紹, 我們先看關鍵的路由部分。

(二)Peer Routing

libP2P定義了routing 介面,目前有2個實現,分別是KAD routing 和 MDNS routing, 擴充套件很容易, 只要按照介面實現相應的方法即可。

ipfs 中的節點路由表是通過維護多個K-BUCKET來實現的, 每次新增節點, 會計算節點ID 和自身節點ID 之間的common prefix, 根據這個公共字首把節點加到對應的KBUCKET 中, KBUCKET 最大值為20, 當超出時,再進行拆分。

更新路由表的流程如下:

除了KAD routing 之外, IPFS 也實現了MDNS routing, 主要用來在區域網內發現節點, 這個功能相對比較獨立, 由於用到了多播地址, 在一些公有云部署環境中可能無法工作。

(三)Swarm(傳輸和連線)

swarm 定義了以下介面:

  • transport 網路傳輸層的介面
  • connection 處理網路連線的介面
  • stream multiplex 同一connection 複用多個stream的介面

下面我們重點看下是如何動態協商stream protocol 的,整個流程如下:

  1. 預設先通過multistream-select 完成握手
  2. 發起方嘗試使用某個協議, 接收方如果不接受, 再嘗試其他協議, 直到找到雙方都支援的協議或者協商失敗。

另外為了提高協商效率, 也提供了一個ls 訊息, 用來查詢目標節點支援的全部協議。

(四)Distributed Record Store

record 表示一個記錄, 可以用來儲存一個鍵值對,比如ipns name publish 就是釋出一個objectId 繫結指定 node id 的record 到ipfs 網路中, 這樣通過ipns 定址時就會查詢對應的record, 再解析到objectId, 實現定址的功能。

(五)Discovery

目前系統支援3種發現方式, 分別是:

  • bootstrap 通過配置的啟動節點發現其他的節點
  • random walk 通過查詢隨機生成的peerID, 從而發現新的節點
  • mdns 通過multicast 發現區域網內的節點

最後總結一下原始碼中的邏輯模組:

從下到上分為5個層次:

  • 最底層為傳輸層, 主要封裝各種協議, 比如TCP,SCTP, BLE, TOR 等網路協議
  • 傳輸層上面封裝了連線層,實現連線管理和通知等功能
  • 連線層上面是stream 層, 實現了stream的多路複用
  • stream層上面是路由層
  • 最上層是discovery, messaging以及record store 等

四、總結

本文從定義資料和傳輸資料的角度分別介紹了IPFS的2個主要模組IPLD 和 libP2P:

  • IPLD 主要用來定義資料, 給資料建模
  • libP2P 解決資料傳輸問題

這兩部分相輔相成, 雖然都源自於IPFS專案,但是也可以獨立使用在其他專案中。

IPFS的遠景目標就是替換現在瀏覽器使用的 HTTP 協議, 目前專案還在迭代開發中, 一些功能也在不斷完善。為了解決資料的持久化問題, 引入了filecoin 激勵機制, 通過token激勵,讓更多的節點加入到網路中來,從而提供更穩定的服務。