P2P网络-kademlia协议

简介

提及去中心化区块链,P2P网络作为区块链系统的核心组件必然是每个区块链开发者应当了解的。本文旨在通过重点介绍以太坊P2P网络的Kademlia协议,抛砖引玉,希望有不同见解的读者在评论区做更深入的交流。

从中心化到去中心化的网络

传统的客户端(Client)-服务器(Server)的模型中,可扩展性的短板与单点故障的问题,是与以服务器为中心的架构分不开的。去中心化的区块链世界中,换个角度又是“人人都是中心”。

对等网络,即对等计算机网络,是一种在对等者(Peer)之间分配任务和工作负载的分布式应用架构,是对等计算模型在应用层形成的一种组网或网络形式。“Peer”在英语里有“对等者、伙伴、对端”的意义。因此,从字面上,P2P可以理解为对等计算或对等网络。其可以定义为:网络的参与者共享他们所拥有的一部分硬件资源(处理能力、存储能力、网络连接能力、打印机等),这些共享资源通过网络提供服务和内容,能被其它对等节点(Peer)直接访问而无需经过中间实体。在此网络中的参与者既是资源、服务和内容的提供者(Server),又是资源、服务和内容的获取者(Client)。[^1]

P2P网络完美的服务于区块链去中心化,自组织的平等思想。

从数学的角度上来讲,P2P网络可以看做一个有向图。理想情况下,所有的对等节点间都应该有一条路径相连。随着节点的增多,如何在个体有限资源下保证整个图的连通性就成为我们亟待解决的问题,因此只能保证每个节点对网络拓扑和其他对等节点只有一个不完整的视图,对每个对等节点来说,图的连通性通过与其他对等节点的邻接关系来反映。所以网络覆盖层需要中间节点将消息转发至正确目的地。图的结构为每对节点提供了多条中间路径,因此如何寻找最优的路径查找特定网络节点,即节点路由技术。区块链系统对于每个节点如何处理节点与存储对象之间的映射关系对等节点的改变(加入与离开网络),邻接的对等节点可能会持有不正确的邻接信息。如何使用网络覆盖层维护机制(Overlay maintenance mechanisms)保存更新的邻接信息,使得所有节点间保持连通性。

Kademlia协议

背景

Kademlia是美国纽约大学的 P. Maymounkov 和 D. Mazieres 在2002年发布的一项研究结果。Kademlia是一种分布式哈希表(DHT),是第三代对等网络的节点动态管理和路由协议。

与前两代协议如 Chord、CAN、Pastry 等相比,Kad以全局唯一id标记对等网络节点,以节点ID异或(XOR)值度量节点之间距离,并通过距离分割子树构建路由表,建立了一种全新的网络拓扑结构。相比于其他算法,更简单,更高效。

网络拓扑的构建

对于每一个节点使用唯一的ID去标识,以太坊使用了256位哈希空间(原始Kad算法160位)作为NodeId,那么理论上就可容纳2^256个节点。每个节点的位置利用其Id值的最短前缀可以唯一确定,那么使用一颗以最短前缀为叶子节点的二叉树来构建整个网络拓扑是比较合适的,其中每个叶子节点代表一个网络节点。至此一个完整而无冗余的网络拓扑就被构建好了。

构造路由表

正如前文所诉,保证每个节点对网络拓扑和其他对等节点只有一个不完整的视图,对每个对等节点来说,图的连通性通过与其他对等节点的邻接关系来反映。我们需要对整个网络拓扑进行分割,分割出属于每个节点自己的部分。即对于以每个节点自己的视角完成子树拆分:根据公共前缀长度将这颗二叉树分解为一系列不包含自己的子树。顶层的子树,由整棵不包含自己的树的另一半组成,即与节点之间的公共前缀长度为0;下一层子树由剩下部分不包含自己的一半组成,即公共前缀长度为1;依此类推,直到分割完整棵树。101节点视角的子树拆分

扫描二维码关注公众号,回复: 14605946 查看本文章
当前节点101 层级 XOR距离 子树节点
0 0 101(当前节点)
1 [2^0, 2^1] 100
2 [2^1, 2^2] 110,111
3 [2^2, 2^3) 000,001,010,011

每个节点在完成子树拆分后,仅需保证每个子树中至少一个节点可达,经过迭代的查找是能够遍历整个网络的每一个节点的,但因为分布式下节点是动态更新的,所以要记录每个子树里面的 K 个节点。这里所说的 K 值是一个系统级的常量,通常是偶数,以太坊的K值设定为16。协议中定义这样的每一个子树为K-Bucket,K-桶;那么构造的路由表Routing-Table就是一个K-Bucket List。

路由查找

对于特定网络节点的查找,Kad算法的核心思想是逐步迭代,递进查找。首先是计算XOR距离,这是一个逻辑上的距离。
由之前子树拆分的图及其划分可以得出根据目标节点与本地节点XOR距离能够找到对应层的K-Bucket,从层中获取α个节点,不足则从附近的桶中获取(幸运的是此情况大多数是目标节点已经非常接近本地节点了)。

Kademlia Search

每次迭代查找的距离至少减半,可以看出整个查询是收敛的,且时间复杂度为O(log N)。

节点通信

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) 信息。
  4. FIND_VALUE : 返回一个节点的 (IP address, UDP port, Node ID) 信息。如果本操作的接受者收到同一个 key 的 STORE 操作,则会直接返回存储的 value 值。

GETH中Kad的UDP通信

  1. PING: 校验节点是否存活
  2. PONG: 对PING事件的回复
  3. FIND_NODE: 向节点查询某个与目标节点ID距离接近的节点
  4. NEIGHBORS: 对FIND_NODE命令响应,发送与目标节点ID距离接近的K桶中的节点

K-Bucket更新

K-Bucket即K-桶,以前文所述的子树的划分来定义每个K-桶。每个K-桶内维护的节点数存在上限,节点的离开与加入则使我们需要一定的算法来进行更新K-桶。
当节点 x 收到一个 PRC 消息时,发送者 y 的 IP 地址就被用来更新对应的 K 桶

以ID值而非IP地址做异或运算
Z有响应
Z没有响应
计算距离
找到对应K桶进行更新
判断y的IP地址是否已经存在于此K桶
把对应项移入K桶尾部
判断K桶记录是否小于K个
把y的IP address,UDP port,Node ID信息插入队列尾部
选择头部记录项Z进行PING操作
从K桶移除Z的信息
把Y的信息插入队列尾部
把Z的信息移至队列尾部
忽略Y的信息

这样就高效的实现了一种把最近看到的节点更新的策略,这种更新策略由节点的在线时长与继续在线的概率关系决定。

Gnutella showed that the longer a node is up,the more likely it is to remain up for one more hour.

除非在线节点一直未从 K 桶中移出过。也就是说在线时间长的节点具有较高的可能性继续保留在 K 桶列表中。对 Kad 网络的稳定性和减少网络维护成本(不需要频繁构建节点的路由表)带来很大好处。

这种机制的另一个好处是能在一定程度上防御 DOS 攻击,因为只有当老节点失效后,Kad 才会更新 K 桶的信息,这就避免了通过新节点的加入来泛洪路由信息。

为了防止 K 桶老化,所有在一定时间之内无更新操作的 K 桶,都会分别从自己的 K 桶中随机选择一些节点执行 RPC_PING 操作。

上述这些 K 桶机制使 Kad 缓和了流量瓶颈(所有节点不会同时进行大量的更新操作),同时也能对节点的失效进行迅速响应。

对象与节点的映射(数据存储)

使用Kademlia网络构建大规模分布式存储系统,需要解决以下两个核心问题:

  1. 建立对象与网络节点之间的映射
  2. 节点动态变化时保证对象的可访问

建立对象与节点的映射,一般有两种方法:

  1. 查表:维护全局<对象,节点>映射表
  2. 计算:直接根据对象特征,通过数学运算得到目标节点

很明显我们只能选择第二种方法,但是又导致了下列问题

  • 对象<key, value>被存储在与key距离最接近的​K个节点,如果​ K个节点全部离线,那么对象便不可达。
  • 对象<key, value>被存储在与key距离最接近的K个节点,如果网络新加入节点N且​N距离key更接近,对象也需要进行一次迁移,因为下次去查找的时候,会直接定位到N,如果数据不迁移至​N,那对象虽然数据存在,但也是不可达。

解决上述问题有两种解决方案:

  • PULL:如果新增一个节点​N,且​距离某对象​O更为接近,此时某节点访问对象​内容,根据算法很可能被定位至节点N​上,​此时再去​当前所在的节点上Pull数据返回给对象访问者;
  • PUSH: 如果新增一个节点N,且​距离某对象​O的更为接近,一旦对象​所在的节点探测到N的存在,会主动地将​数据推至N,这也可以保证下次访问​时无需中转而直接获取到数据。

对比两种方案:

  • PULL:新增节点​需要了解对象​此时所在节点位置,这是做不到的,​可以根据自身路由表计算出当前距离​最接近的节点,但是没法知道前一个时刻距离​最接近的节点;而且该方案没法处理节点批量离线导致的对象不可访问问题。
  • PUSH: 探测到新节点​加入,然后计算本地存储的对象中有哪些距离​更为接近,将这些接近的对象push至此​。当然,这种做法也无法时刻都在做,因为在大量节点的P2P网络中,节点变更是常态,一种比较合理的方式是定期做检查。但是该方案依赖于​能够感知到新增​的存在,好在,​需要迁移对象的一定是距离​很接近的节点。在我们前面新增节点流程描述中可以知道一个新增节点上线时,算法会偏向于将该新增节点通知到距离其更近的节点中。因此,一旦一个新节点上线,那么距离其接近的节点就会更快地了解到该节点信息,从而将其本地存储的数据可以push至新增节点。

通过对比可确认PUSH才是切实可行的,而且符合P2P网络的松耦合的设计。但是该方案也并非完美:一旦有新增节点可能就会带来大量的数据拷贝,消耗大量资源。万事万物总是这样,在收获好处的同时总得付出代价。在论文中,作者提出了按照小时为单位执行Re-Publishing:每个小时,每个节点​对本地存储的每一个对象​进行Re-Publishing。每一次Re-Publishing包括下面两个步骤:

  1. 首先查询​当前最近的K个节点信息
  2. 节点​向1中获得的节点发送数据存储消息,从而完成数据更新

针对每个节点的每个对象都执行类似操作,会导致P2P网络出现突激的网络流量。仔细分析上面的行为,我们可以发现,其实很多的Re-Publishing是无用的:

  • 如果在一个小时的周期内,网络拓扑没有发生变化,那根本不需要进行Re-Publishing;

  • 对于不同的节点、​,如果对象​在节点、​上均已存储,那其实在一个Re-Publishing周期内,只需要一个节点(​或者​)来更新​即可,没必要大家同时一起上,浪费资源;

  • 步骤1中的查询当前最近是否有必要:因为根据上面的表述,对于新增节点,其实是可以很快速地反映到最近节点路由表,所以一般情况下,查询本地节点路由表即可。

最终存放<key,value>对数据的解决方案为:

  1. 发起者首先定位 K个ID值最接近key的节点
  2. 发起者对这K个节点发起STORE操作
  3. 执行 STORE 操作的K个节点每小时重发布自己所有的 <key,value>对数据
  4. 为了限制失效信息,所有<key,value>对数据在初始发布24小时后过期
  5. 另外,为了保证数据发布、搜寻的一致性,规定在任何时候,当节点 O发现新节点N比O上的某些<key,value>对象数据更接近,则O把这些<key,value>对象数据复制到N上,但是并不会从O上删除。
  6. 最后,一旦有节点 FIND_VALUE 操作成功执行,则 <key,value>对象数据会缓存在没有返回value值的最接近的节点上。这样下一次查询相同的key时就会更加快速的得到结果。通过这样的方式,热门 <key,value> 对数据的缓存范围就逐步扩大,使系统具有极佳的响应速度(cache为存活24小时,但是目标节点上的内容时每1小时向其他最近节点重新发布<key, value>使得数据的超时时间得以刷新,而远离目标节点的节点的数据存活时间当然就可能不会被重新发布到,所以也就是数据缓存的超时时间和节点的距离成反比)。

参考

《Peer-To-Peer综述》—中科院计算技术研究所
https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf
https://zhuanlan.zhihu.com/p/38425656
https://www.cnblogs.com/1314xf/p/14019453.html

猜你喜欢

转载自blog.csdn.net/qq_34623248/article/details/122044553
P2P