go-libp2p-kbucket RoutingTable 源碼分析

  • 創建 RoutingTable 實例
//go-libp2p-kad-dht/dht.go
func New(ctx context.Context, h host.Host, options ...opts.Option) (*IpfsDHT, error) {
    ......
    // 從這裏開始看,這裏創建了今天的重點內容 RoutingTable
    dht := makeDHT(ctx, h, cfg.Datastore, cfg.Protocols)
    ......
    // 下文中提到的 RoutingTable.Update 方法會在這個 handler 中調用 
    h.SetStreamHandler(p, dht.handleNewStream)
    ......
}
  • 關於 RoutingTable

先看看它是如何初始化吧

// go-libpep-kbucket/table.go
// NewRoutingTable creates a new routing table with a given bucketsize, local ID, and latency tolerance.
func NewRoutingTable(bucketsize int, localID ID, latency time.Duration, m pstore.Metrics) *RoutingTable {
    rt := &RoutingTable{
        Buckets:     []*Bucket{newBucket()},
        bucketsize:  bucketsize,
        local:       localID,
        maxLatency:  latency,
        metrics:     m,
        PeerRemoved: func(peer.ID) {},
        PeerAdded:   func(peer.ID) {},
    }

    return rt
}

// go-libp2p-kad-dht/dht.go
//在 New DHT 的時候調用了 NewRoutingTable,我這句注視絕對是廢話哈哈
func makeDHT(ctx context.Context, h host.Host, dstore ds.Batching, protocols []protocol.ID) *IpfsDHT {
    // KValue == 20 , 
    rt := kb.NewRoutingTable(KValue, kb.ConvertPeerID(h.ID()), time.Minute, h.Peerstore())
    ......
}

RoutingTable 這個類還是很強硬的,它都不是一個接口只能用這一種實現,在 makeDHT 創建了一個 RoutingTable 的實例,接下來看看我最關心的k桶是如何更新的呢?

RoutingTable.Update 更新 k桶
  • 桶的數據結構相當於一個二維數組 Bucket[i][j], i 是桶號 j 對應桶中的內容
    具體如下:
//kBuckets define all the fingers to other nodes.
Buckets    []*Bucket   
......
//Bucket holds a list of peers.
type Bucket struct {
    lk   sync.RWMutex
    list *list.List
}
  • 更新 k 桶中的內容,請閱讀下文中的注視:
// Update adds or moves the given peer to the front of its respective bucket
// If a peer gets removed from a bucket, it is returned
// 添加或移除給定的 peer 到它對應的桶的前端
// 如果從桶中移除這個 peer ,則返回這個 peer
func (rt *RoutingTable) Update(p peer.ID) {
    peerID := ConvertPeerID(p)
    //計算桶號,通過 peerID 和 當前節點 ID 做異或,算出對應的桶號
    //後面會單獨講解這個實現
    cpl := commonPrefixLen(peerID, rt.local)

    rt.tabLock.Lock()
    defer rt.tabLock.Unlock()
    bucketID := cpl
    //如果計算出來的桶 ID 已經比現有的桶多了,則把它放到最後一個桶裏
    if bucketID >= len(rt.Buckets) {
        bucketID = len(rt.Buckets) - 1
    }
    //通過桶號得到一個具體的桶
    bucket := rt.Buckets[bucketID]
    // 如果這個 peer 已經在桶中了,將它移動到當前桶的最前面。
    if bucket.Has(p) {
        // If the peer is already in the table, move it to the front.
        // This signifies that it it "more active" and the less active nodes
        // Will as a result tend towards the back of the list
        bucket.MoveToFront(p)
        return
    }
    // 延遲太大的會丟棄
    if rt.metrics.LatencyEWMA(p) > rt.maxLatency {
        // Connection doesnt meet requirements, skip!
        return
    }
    // New peer, add to bucket
    bucket.PushFront(p)
    rt.PeerAdded(p)
    
    /* 
    ---------------------------
    這個地方的邏輯是:
    ---------------------------
    當前桶中的數據已經大於 最大限額時
        如果 當前桶號已經是最後一個桶了,那麼創建下一個桶,這個地方比較複雜
            下一個桶會將所有桶中 peer 的與 localID 距離大於 總桶數的 peer 移動到下一個桶中。
            因爲未必會找到距離大於總桶數的 peer,
            所以 bucket.Split 之後當前桶的總數還有可能會大於最大限額,所以要判斷並刪除最後一個元素
        如果當前桶不是最後一個桶,則直接刪除當前桶中最後一個元素
    */
    // Are we past the max bucket size?
    if bucket.Len() > rt.bucketsize {
        // If this bucket is the rightmost bucket, and its full
        // we need to split it and create a new bucket
        if bucketID == len(rt.Buckets)-1 {
            rt.nextBucket()
        } else {
            // If the bucket cant split kick out least active node
            rt.PeerRemoved(bucket.PopBack())
        }
    }
}
  • 仔細看看怎麼分桶

通過下面兩個方法可以看出 a、b 做了異或,
然後通過 ZeroPrefixLen 取前導 0 來求出桶號
假設 a = 1 ,b = 2 換成 比特位
a = 00000001 ,b = 00000010
a xor b = 00000011 一共 6 個前導 0
所以如果 b 是自己,那麼 a 應該放在第 6 個桶裏,
前面講過 6 這個桶可能還未創建,那麼則放到最新的桶中
在實際使用中 peerID 會通過 sha256 得到一個 32 位的 hash
每一位是 8 個bit,所以最多可以分 8*32 = 256 個桶
這樣算下來,第0個桶可以裝 2 的 256 次方個id
第256個桶就只能放 1 個 id ,k 桶變成了一個三角形
dht 中又約定了一個桶最多隻能放 20個 id ,
最後幾個桶是裝不滿 20 個的,懶得算了,
填滿所有桶,可以裝大約小於 256 * 20 = 5120 個 id

//go-libp2p-kbucket/util.go
func commonPrefixLen(a, b ID) int {
    return ks.ZeroPrefixLen(u.XOR(a, b))
}

//go-libp2p-kbucket/keyspace/xor.go
func ZeroPrefixLen(id []byte) int {
    for i, b := range id {
        if b != 0 {
            return i*8 + bits.LeadingZeros8(uint8(b))
        }
    }
    return len(id) * 8
}
  • 誰來調用這個 RoutingTable.Update 呢?

有兩個地方會去調用 RoutingTable.Update ,它被封裝在 IpfsDHT.Update 中。筆記一開頭就提到了 dht.handleNewStream ,順着這個方法可以找到調用 Update 的邏輯,還有 IpfsDHT.sendRequest 方法也會調用 Update

首先看 sendRequest ,dht 包中所有發給 peer 的請求都會調用這個方法

// sendRequest sends out a request, but also makes sure to
// measure the RTT for latency measurements.
func (dht *IpfsDHT) sendRequest(ctx context.Context, p peer.ID, pmes *pb.Message) (*pb.Message, error) {

    ms, err := dht.messageSenderForPeer(p)
    if err != nil {
        return nil, err
    }

    start := time.Now()

    rpmes, err := ms.SendRequest(ctx, pmes)
    if err != nil {
        return nil, err
    }
    //這裏會有條件的調用 RoutingTable.Update 方法
    // update the peer (on valid msgs only)
    dht.updateFromMessage(ctx, p, rpmes)

    dht.peerstore.RecordLatency(p, time.Since(start))
    log.Event(ctx, "dhtReceivedMessage", dht.self, p, rpmes)
    return rpmes, nil
}

再看看 handleNewMessage ,是通過 dht.handleNewStream 來調用的


func (dht *IpfsDHT) handleNewMessage(s inet.Stream) {
    ......
    //這裏會有條件的調用 RoutingTable.Update 方法
    // update the peer (on valid msgs only)
    dht.updateFromMessage(ctx, mPeer, pmes)
    ......
}   

以上代碼片段可以看出 sendRequest 成功時主動去調用 Update 而 handleNewMessage 成功時也會被動的調用一次 Update 去更新 RoutingTable

//TODO 下面這些有空再寫,其實並不重要

RoutingTable.Remove
RoutingTable.NearestPeer / NearestPeers
RoutingTable.Find
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章