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

你可能感兴趣的:(go-libp2p-kbucket RoutingTable 源码分析)