DHT 分布式哈希表 (Kademlia) 和一致性哈希

DHT

在这里插入图片描述

分布式哈希表(distributed hash table,缩写DHT)是分布式计算系统中的一类,用来将一个键(key)的集合分散到所有在分布式系统中的节点。这里的节点类似哈希表中的存储位置。分布式哈希表通常是为了拥有大量节点的系统,而且系统的节点常常会加入或离开。

研究分布式哈希表的主要动机是为了开发点对点系统,像是 Napster、Gnutella、BitTorrent 及 Freenet。这些系统使用分散在互联网上的各项资源以提供文件分享服务,特别在带宽及硬盘存储空间上受益良多。

最初的四项分布式哈希表技术——内容可定址网络(Content addressable network,CAN)、Chord(Chord project)、Pastry(Pastry (DHT)),以及Tapestry (DHT)(Tapestry (DHT))皆同时于2001年发表。从那时开始,相关的研究便一直十分活跃。在学术领域以外,分布式哈希表技术已经被应用在BitTorrent及CoralCDN(Coral Content Distribution Network)等。

分布式散列表本质上强调以下特性:

  • 离散性:构成系统的节点并没有任何中央式的协调机制。
  • 伸缩性:即使有成千上万个节点,系统仍然应该十分有效率。
  • 容错性:即使节点不断地加入、离开或是停止工作,系统仍然必须达到一定的可靠度。

要达到以上的目标,有一个关键的技术:任一个节点只需要与系统中的部分节点沟通,当成员改变的时候,只有一部分的工作(例如数据或键的发送,哈希表的改变等)必须要完成。

分布式散列表的结构可以分成几个主要的组件。其基础是一个抽象的键空间(keyspace),例如说所有160位长的字符串集合。键空间分区(keyspace partitioning)将键空间分区成数个,并指定到在此系统的节点中。而延展网络则连接这些节点,并让他们能够借由在键空间内的任一值找到拥有该值的节点。

假设键空间是一个160位长的字符串集合。为了在分布式散列表中存储一个文件,名称为filename且内容为data,我们计算出filename的SHA1散列值——一个160位的键k——并将消息put(k,data)送给分布式散列表中的任意参与节点。此消息在延展网络中被路由,直到抵达在键空间分区中被指定负责存储关键值k的节点。而(k,data)即存储在该节点。其他的节点只需要重新计算filename的散列值k,然后提交消息get(k)给分布式哈希表中的任意参与节点,以此来找与k相关的数据。此消息也会在延展网络中被路由到负责存储k的节点。而此节点则会负责传回存储的数据data

基本上,就是一种映射 key 和节点的算法以及路由的算法。

其一为保证任何的路由路径长度必须尽量短,因而请求能快速地被完成;
其二为任一节点的邻近节点数目(又称最大节点度(Degree (graph theory)))必须尽量少,因此维护的花费不会过多。

分布式哈希表( DHT: Distributed Hash Table)

我们将散列表放在一个机器的内存里,当散列表比较小时候,没有问题,但如果这张散列表超过了一台机器的内存时候,或者当存储在一台机器上时候,这台机器挂掉了,那所有的数据都会消失……, 那现在我们又该怎么做呢?

这便需要引入DHT来处理这种情况了,说白了就是将一张哈希表分割在不同的机器上。

首先,将上面所说的散列空间0,1…9想像成一个首尾相衔的环,9之后又重新回到零,假设,这就是十台机器。

这样我们根据散列算法就可以将不同的学生映射到不同的机器上,实现了数据的分布。

一致性哈希(consistent hashing)

好了,上面我们使用DHT实现了数据的分布式存储,但再考虑深一层,分布式架构中,节点的故障是不可避免的,当添加和删除某一节点了,会导致大量散列数据失效,需要重新散列。
影响非常大,那我们用什么哈希算法来实现DHT才能尽量的避免这种情况呢,这便说到了一致性哈希。

consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在 key 映射关系

刚刚我们把学生散列到了hash数值空间里,现在我们需要的是,同时将机器也散列在这个hash空间,让学生和机器的散列值同处在一个数值空间。当其中一台机器宕机下线时,我们将相关的内容复制到由距离算法计算出来的下一台最近距离的机器即可。

总结:

  • 分布式哈希: 将哈希表分散在不同的节点上,并且能提供相应的方法来查找, 比如DHT算法
  • 一致性哈希: 当节点宕机或者扩容的时候,需要重新哈希,一致性哈希实现的 DHT 避免对大量的数据重新哈希, 比如Chord DHT. 所以一致性哈希是 DHT 的一种实现,避免在节点变化的时候出现的全部重新哈希的现象.

Kademlia算法

Kademlia是一种通过 DHT 的协议算法,它是由Petar和David在2002年为P2P网络而设计的。Kademlia规定了网络的结构,也规定了通过节点查询进行信息交换的方式。
Kademlia网络节点之间使用UDP进行通讯。参与通讯的所有节点形成一张虚拟网(或者叫做覆盖网)。这些节点通过一组数字(或称为节点ID)来进行身份标识。节点ID不仅可以用来做身份标识,还可以用来进行值定位(值通常是文件的散列或者关键词)。

当我们在网络中搜索某些值(即通常搜索存储文件散列或关键词的节点)的时候,Kademlia算法需要知道与这些值相关的键,然后逐步在网络中开始搜索。每一步都会找到一些节点,这些节点的ID与键更为接近,如果有节点直接返回搜索的值或者再也无法找到与键更为接近的节点ID的时候搜索便会停止。

这种搜索值的方法是非常高效的:与其他的分布式哈希表的实现类似,在一个包含n个节点的系统的值的搜索中,Kademlia仅访问O(log(n))个节点。

Kademlia简称为Kad,它使用了一个精妙的算法,来计算节点之间的"距离" (这里的距离不是地理空间的距离,而是路由的跳数),这个算法就是XOR操作(异或),因为这个操作和距离的计算类似:

  • (A ⊕ B) == (B ⊕ A): XOR 符合“交换律”,具备对称性。A和B的距离从哪一个节点计算都是相同的。
  • (A ⊕ A) == 0: 反身性,自己和自己的距离为零。
  • (A ⊕ B) > 0: 两个不同的 key 之间的距离必大于零。
  • (A ⊕ B) + (B ⊕ C) >= (A ⊕ C): 三角不等式, A经过B到C的距离总是大于A直接到C的距离。

(精妙啊!是如何想起和距离计算联系在一起的?)

Kad使用160位的哈希算法(比如 SHA1),完整的 key 用二进制表示有160位,这样可以容纳2160个节点,可以说是不计其数了。

Kad把 key 映射到一个二叉树,每一个 key 都是这个二叉树的叶子

映射规则

  1. 先把 key 以二进制形式表示,然后从高位到低位依次处理。
  2. 二进制的第 n 个位就对应了二叉树的第 n 层
  3. 如果该位是1,进入左子树,是0则进入右子树(这只是人为约定,反过来处理也可以)
  4. 全部位都处理完后,这个 key 就对应了二叉树上的某个叶子

二叉树的拆分规则

在这里插入图片描述

对每一个节点,都可以按照自己的视角对整个二叉树进行拆分成最多160个子树。

拆分的规则是:先从根节点开始,把不包含自己的那个子树拆分出来;然后在剩下的子树再拆分不包含自己的第二层子树;以此类推,直到最后只剩下自己。

Kad 默认的散列值空间是 m=160(散列值有 160 bit),因此拆分出来的子树最多有 160 个(考虑到实际的节点数远远小于2160,子树的个数会明显小于 160)。

对于每一个节点而言,当它以自己的视角完成子树拆分后,会得到 n 个子树;对于每个子树,如果它都能知道里面的一个节点,那么它就可以利用这 n 个节点进行递归路由,从而到达整个二叉树的任何一个节点。

拆子树

每个节点在完成子树拆分后,只需要知道每个子树里面的一个节点,就足以实现全遍历。但是考虑到健壮性(节点可能宕机或者退出),光知道一个显然是不够的,需要知道多个才比较保险。

所以 Kad 论文中给出了一个K-桶(K-bucket)的概念。也就是说:每个节点在完成子树拆分后,要记录每个子树里面的 K 个节点。这里所说的 K 值是一个系统级的常量。由使用 Kad 的软件系统自己设定(比如 BT 下载使用的 Kad 网络,K 设定为 8)。
  
K 桶其实就是路由表。对于某个节点而言,如果以它自己为视角拆分了 n 个子树,那么它就需要维护 n 个路由表,并且每个路由表的上限是 K。
 
说 K 只是一个上限,是因为有两种情况使得 K 桶的尺寸会小于 K:

  1. 距离越近的子树就越小。如果整个子树可能存在的节点数小于 K,那么该子树的 K 桶尺寸永远也不可能达到 K。(这是由于K桶对应的距离越近,节点数越少)
  2. 有些子树虽然实际上线的节点数超过 K,但是因为种种原因,没有收集到该子树足够多的节点,这也会使得该子树的 K 桶尺寸小于 K。

如果选择这 K 个节点呢?
Kademlia选择把那些长时间在线的节点存入K桶,这一方法增长了未来某一时刻有效节点的数量,同时也提供了更为稳定的网络。当某个K桶已满,而又发现了相应于该桶的新节点的时候,那么,就首先检查K桶中最早访问的节点,假如该节点仍然存活,那么新节点就被安排到一个附属列表中(作为一个替代缓存).只有当K桶中的某个节点停止响应的时候,替代cache才被使用。换句话说,新发现的节点只有在老的节点消失后才被使用。

Kademlia协议

Kademlia协议共有四种消息。

  • PING消息: 用来测试节点是否仍然在线。
  • STORE消息: 在某个节点中存储一个键值对。
  • FIND_NODE消息: 消息请求的接收者将返回自己桶中离请求键值最近的K个节点。
  • FIND_VALUE消息: 与FIND_NODE一样,不过当请求的接收者存有请求者所请求的键的时候,它将返回相应键的值。

每一个RPC消息中都包含一个发起者加入的随机值,这一点确保响应消息在收到的时候能够与前面发送的请求消息匹配。

定位节点

节点查询可以异步进行,也可以同时进行,同时查询的数量由α表示,一般是3。

  1. 由查询发起者从自己的k-桶中筛选出若干距离目标ID最近的节点,并向这些节点同时发送异步查询请求;
  2. 被查询节点收到请求之后,将从自己的k-桶中找出自己所知道的距离查询目标ID最近的若干个节点,并返回给发起者;
  3. 发起者在收到这些返回信息之后,更新自己的结果列表,再次从自己所有已知的距离目标较近的节点中挑选出若干没有请求过的,并重复步骤1;
  4. 上述步骤不断重复,直至无法获得比查询者当前已知的k个节点更接近目标的活动节点为止。
  5. 在查询过程中,没有及时响应的节点将立即被排除;查询者必须保证最终获得的k个最节点都是活动的。

定位资源

通过把资源信息与键进行映射,资源即可进行定位,杂凑表是典型的用来映射的手段。由于以前的STORE消息,存储节点将会有对应STORE所存储的相关资源的信息。定位资源时,如果一个节点存有相应的资源的值的时候,它就返回该资源,搜索便结束了,除了该点以外,定位资源与定位离键最近的节点的过程相似。

考虑到节点未必都在线的情况,资源的值被存在多个节点上(节点中的K个),并且,为了提供冗余,还有可能在更多的节点上储存值。储存值的节点将定期搜索网络中与储存值所对应的键接近的K个节点并且把值复制到这些节点上,这些节点可作为那些下线的节点的补充。另外还有缓存技术。

加入网络

  1. 新节点A必须知道某个引导节点B,并把它加入到自己相应的K-桶中
  2. 生成一个随机的节点ID,直到离开网络,该节点会一直使用该ID号
  3. 向B(A目前知道的唯一节点)发起一个查询请求(FIND_NODE),请求的ID是自己(就是查询自己)
  4. B收到该请求之后,会先把A的ID加入自己的相应的 K-桶中。并且根据 FIND_NODE 请求的约定,B会找到K个最接近 A 的节点,并返回给 A
  5. A收到这K个节点的ID之后,把他们加入自己的 K-桶
  6. 然后A会继续向刚刚拿到的这批节点(还未发送过请求的节点)发送查询请求(协议类型 FIND_NODE),如此往复,直至A建立了足够详细的路由表。
  7. 这种“自我定位”将使得Kad的其他节点(收到请求的节点)能够使用A的ID填充他们的K-桶,同时也能够使用那些查询过程的中间节点来填充A的K-桶。这已过程既让A获得了详细的路由表,也让其它节点知道了A节点的加入

kad 在p2p网络中的应用

Kademlia 可在文件分享网络中使用,通过制作 Kademlia 关键字搜索,我们能够在文件分享网络中找到我们需要的文件以供我们下载。由于没有中央服务器存储文件的索引,这部分工作就被平均地分配到所有的客户端中去:

假如一个节点希望分享某个文件,它先根据文件的内容来处理该文件,通过运算,把文件的内容散列成一组数字,该数字在文件分享网络中可被用来标识文件。这组散列数字必须和节点ID有同样的长度,
然后,该节点便在网络中搜索ID值与文件的散列值相近的节点,并把它自己的IP地址存储在那些搜索到的节点上,也就是说,它把自己作为文件的源进行了发布。正在进行文件搜索的客户端将使用 Kademlia 协议来寻找网络上ID值与希望寻找的文件的散列值最近的那个节点,然后取得存储在那个节点上的文件源列表。
由于一个键可以对应很多值,即同一个文件可以有多个源,每一个存储源列表的节点可能有不同的文件的源的信息,这样的话,源列表可以从与键值相近的K个节点获得。

文件的散列值通常可以从其他的一些特别的 Internet 链接的地方获得,或者被包含在从其他某处获得的索引文件中。
文件名的搜索可以使用关键词来实现,文件名可以分割成连续的几个关键词,这些关键词都可以散列并且可以和相应的文件名和文件散列储存在网络中。搜索者可以使用其中的某个关键词,联系ID值与关键词散列最近的那个节点,取得包含该关键词的文件列表。由于在文件列表中的文件都有相关的散列值,通过该散列值就可利用上述通常取文件的方法获得要搜索的文件。

IPFS 改造后的 S/Kademlia 协议

S/Kademlia 是一种基于安全 Key 的路由协议,该协议通过在多条不相交的路径上使用并行查找来抵抗常见的攻击,并用加密技术来限制节点 ID 生成,然后引入可靠的兄弟广播来解决这个问题。

针对 Kademlia 网络的攻击

  • 日蚀攻击( Eclipse attack )

这种攻击的方式是尝试在网络中放置敌对节点,从而将一个或多个节点与其隔离,即所有消息都通过至少一个敌对节点进行路由。 这使攻击者可以控制覆盖网络的一部分。 因此,日蚀攻击可以从覆盖网络隐藏一些节点。 首先,如果一个节点不能自由选择其节点ID,其次,当它很难影响其他节点的路由表时,可以避免这种情况。 由于 Kademlia 采用 K-桶 中的活跃节点,并且只添加节点,如果一个桶未满,只要网络启动,这种攻击就很容易实现。

  • 女巫攻击(Sybil attack)

如果一个节点在网络能够自由选择它的 ID,攻击者在网络中放一些恶意节点,使得信息都必须经由恶意节点传递。恶意节点就能够在网络将一个或几个节点从网络中隐藏掉。要解决日蚀攻击,只要恶意节点不能自由选择 ID 或者很难通过策略修改其他节点的 K-Bucket,这一节点就避免了。但 KAD 会优先请求 K-Bucket 中的长时间在线的节点,一旦被攻击节点的 K-Bucket 是非满的,恶意节点就有机会加入攻击节点的 K-Bucket,那么攻击者只要足够长时间在线就能实现攻击了。

  • 客户流失攻击(Churn attack)

攻击者控制一堆节点,一下子把节点从网络中流失,从而导致网络稳定性降低。

  • 敌对路由(Adversarial routing)

恶意节点收到查询指令后,不按照KAD的要求返回距离Key最接近的网络节点,而是转移给同伙节点,最终导致查询实效。为了避免这种无聊的敌对路由攻击,设计算法在查询时进行并行查询,每一条查询路径不想交,一旦并行查询的路径中有一条不碰到而已节点,就能成功查询了。

3.4.2 S/Kademlia 解决方式

S/Kademlia 协议针对 KAD 容易被攻击做出了几个改进:

  1. 为了避免日蚀攻击和女巫攻击,S/Kademlia 需要节点不能自由选择节点 ID,不能大批量生成 ID,同时不能窃取和伪装其他节点的 ID。肯定就需要通过非对称加密确保节点身份不被窃取,设置一定的计算量障碍,强迫节点进行一定的哈希运算确保不能自由选择和批量生产。
  2. 为避免敌对路由攻击,设计并发不想交的查询路径。

安全的节点分配策略

S/Kademlia 节点 ID 的分配策略有三个:

  • 不能自由选择
  • 不能大量生成
  • 不能窃取和伪装。

S/Kademlia 要求每个节点在接入网络前必须解决两个密码学问题。

  • 静态问题是:产生一对公钥和私钥,公钥两次哈希运算后,具有 C1 个前导零。公钥的一次哈希值就是节点的 NodeID。
  • 动态问题是:不断生成一个随机数 X,将 X 与 NodeID 求XOR 再求哈希,哈希值要求 C2 个前导零。

在这里插入图片描述

用于nodeId生成的静态(左)和动态(右)加密难题

这样设计,第一个静态问题,保证节点不能再自由选择节点 ID,后一个动态问题,提高了大量生成ID的成本。女巫攻击和日蚀攻击将难以进行。

为确保节点身份不被窃取,节点对发出的消息进行签名。其他节点收到消息,验证签名的合法性,然后检查ID是否满足两个难题的要求。验证节点信息的合法性的时间复杂度是很低的,而生成这样一个合法的攻击信息的时间复杂度是很高的,这种不对称就能避免上面三种攻击了。

不相交的路径查询算法

Kademlia 协议中,访问α个 K-Bucket 中的节点,然后排序后,选择前 α 个继续迭代请求,缺点很明显,如果有恶意节点,查询很可能就会失败。

S/Kademlia 提出每次查询选择 k 个节点,放入 d 个不同的桶中。这 d 个桶进行查找,d 条查询路径做到不相交,单个桶有失效的可能,但是只要 d 个桶中有一条查询到了需要的信息,工作就完成了。通过不相交路径查询,解决了敌对路由攻击。

总结

S/Kademlia 提出以上切实可行的解决方案,使得原有的 Kademlia 更具弹性。 首先,通过结合公钥密码术使用加密谜题来限制 nodeId 生成。 此外,我们通过兄弟列表扩展 Kademlia 路由表。 这降低了桶分裂算法的复杂性,并允许 DHT 以安全复制的方式存储数据。 最后,提出了一种查找算法,该算法使用多条不相交路径来提高查找成功率。

猜你喜欢

转载自blog.csdn.net/smilejiasmile/article/details/129061991