目前在项目的主线上已经实现了kad协议,而kBucket作为存储节点的一环,值得进行一次分析。

Kbucket简要介绍

在kad中,peer每获取到一个节点的信息,会将其存放到自己的KBucket中。每个peer_id由公钥经过sha2_256运算之后得到,长度为32个字节。每个节点都可以与另外的节点经过异或运算得到最长前缀,即从第一位开始的连续0的个数。0越多,代表两个节点越接近,最多可以有32*8个连续的0。所以对KBucket而言,桶的最大个数为256个。

KBucket结构

在rust-libp2p中,每个KBucket内部维护了一个Node类型的ArrayVector,大小为20,其中Node结构使用key存放peer_id,value存放地址信息;first_connected_pos作为KBucket的连接标记位,记录可被清理的节点下标;同时还提供了apply_pending的属性,存储预备插入的节点

与rust-libp2p不同的是,libp2p-rs使用了一个新的设计方法。因为peerstore的存在,我们不需要在KBucket里面存储peer所对应的地址信息,相对应的,peer相关的一些连接信息,如最后连接时间,就可以作为新的value被存放到node中。这样设计还有一个好处,就是也不需要apply_pending和connect_pos这些属性了,每个peer可以单独维护一个自己的连接状态信息,KBucket清理时,可以直接用filter的方式执行相关操作。综合以上情况,我们设计了一个结构体PeerInfo,用来作为新的Value类型

PeerInfo中记录了三个属性

/// The information of a peer in Kad routing table.
#[derive(Clone, Debug)]
pub struct PeerInfo {
    /// The time instant at which we talk to the remote peer.
    /// Sets to `Some` if it is deemed to be alive. Otherwise,
    /// it is set to `None`
    aliveness: Option<Instant>,

    /// The time this peer was added to the routing table.
    added_at: Instant,

    /// reserved for future use?
    replaceable: bool,
}

aliveness表示最后通信时刻,added_at记录该节点被添加到路由表的时刻,replaceable标记这条信息是否可以被替换(目前未启用)。通过这种方式,在对KBucket进行增删操作时,能够更加容易判断相关peer的状态。

代码分析

下面以KBucketTable的try_add_peer()进行分析:

  1. 首先判断是否为已存在节点。如果存在,且属于迭代查询过程中调用的方法,那就需要更新peer的最后通信时间
  2. 如果不是已存在节点,需要分情况讨论

    1. 如果调用insert方法能够成功添加,就需要在peerstore中将该节点的GC标记位设置为false,防止因为GC导致地址信息被清理,进而重复进行迭代查询。
    2. 如果添加失败,说明KBucket满了,需要进行清理。首先通过filter和min_by找出最久未通信的节点,将其从KBucket中驱逐,同时peerstore中修改为可被GC。之后再将新的节点插入到KBucket中,peerstore标记不进行GC

      fn try_add_peer(&mut self, peer: PeerId, queried: bool) {
      let timeout = self.check_kad_peer_interval;
      let now = Instant::now();
      let key = kbucket::Key::new(peer.clone());
      
      log::debug!(
          "trying to add a peer: {:?} bucket-index={:?}, query={}",
          peer,
          self.kbuckets.bucket_index(&key),
          queried
      );
      
      match self.kbuckets.entry(&key) {
          kbucket::Entry::Present(mut entry) => {
              // already in RT, update the node's aliveness if queried is true
              if queried {
                  entry.value().set_aliveness(Some(Instant::now()));
                  log::debug!("{:?} updated: {:?}", peer, entry.value());
              }
          }
          kbucket::Entry::Absent(mut entry) => {
              let info = PeerInfo::new(queried);
              if entry.insert(info.clone()) {
                  log::debug!("Peer added to routing table: {} {:?}", peer, info);
                  // pin this peer in PeerStore to prevent GC from recycling multiaddr
                  if let Some(s) = self.swarm.as_ref() {
                      s.pin(&peer)
                  }
              } else {
                  log::debug!("Bucket full, trying to replace an old node for {}", peer);
                  // try replacing an 'old' peer
                  let bucket = entry.bucket();
                  let candidate = bucket
                      .iter()
                      .filter(|n| n.value.get_aliveness().map_or(true, |a| now.duration_since(a) > timeout))
                      .min_by(|x, y| x.value.get_aliveness().cmp(&y.value.get_aliveness()));
      
                  if let Some(candidate) = candidate {
                      let key = candidate.key.clone();
                      let evicted = bucket.remove(&key);
                      log::debug!("Bucket full. Peer node added, {} replacing {:?}", peer, evicted);
                      // unpin the evicted peer
                      if let Some(s) = self.swarm.as_ref() {
                          s.unpin(key.preimage())
                      }
                      // now try to insert the value again
                      let _ = entry.insert(info);
                      // pin this peer in PeerStore to prevent GC from recycling multiaddr
                      if let Some(s) = self.swarm.as_ref() {
                          s.pin(&peer)
                      }
                  } else {
                      log::debug!("Bucket full, but can't find an replaced node, give up {}", peer);
                  }
              }
          }
          _ => {}
      }
      }

Netwarps 由国内资深的云计算和分布式技术开发团队组成,该团队在金融、电力、通信及互联网行业有非常丰富的落地经验。Netwarps 目前在深圳、北京均设立了研发中心,团队规模30+,其中大部分为具备十年以上开发经验的技术人员,分别来自互联网、金融、云计算、区块链以及科研机构等专业领域。
Netwarps 专注于安全存储技术产品的研发与应用,主要产品有去中心化文件系统(DFS)、去中心化计算平台(DCP),致力于提供基于去中心化网络技术实现的分布式存储和分布式计算平台,具有高可用、低功耗和低网络的技术特点,适用于物联网、工业互联网等场景。
公众号:Netwarps