论文阅读-Chord: A Scalable Peer-to-peer Lookup Service for Internet Applications

Posted by keys961 on May 9, 2019

1. 介绍

P2P System(对等系统):节点关系平等,可用于存储冗余、数据持久、临近节点选取、匿名、搜索、认证、层次名称管理,核心操作是寻找特定数据的节点位置

Chord:基于对等应用的分布式查找协议

  • 仅支持的操作:给定一个key,将它映射到节点
  • 基于一致性散列,在节点加入/离开时,改动很小
  • 集群有$N$个节点,每个节点只需维护$O(logN)$的路由消息,节点加入/离开时的信息交互不超过$O(log^2N)$
  • 协议简单(路由只需经过$O(logN)$个节点),正确性和性能都可证明

2. 系统模型

Chord解决以下问题:

  • 负载均衡:Chord的一致性散列可直接实现
  • 去中心化:Chord是完全分布式的,提高了健壮性,并让P2P应用更松散
  • 可扩展性:Chord查询的时间根据节点数量增长,不需调整参数
  • 可用性:当节点加入/离开时,Chord内部表会自动调整,每个key总能找到对应的节点
  • 灵活命名:Chord的key是flat的,提供了灵活性

使用Chord:通过库链接到客户端和服务端,提供lookup(key)以返回节点地址,并通知上层应用key位置的变化

Chord可提供的基础功能:

  • 合作镜像:类似SVN,可用于版本控制
  • 时间共享存储:将数据存在别的服务器,以提高可用性
  • 分布式索引
  • 大型组合搜索:加密密钥,进行key到节点的映射

一个基于Chord的分布式存储架构如下:FS提供灵活命名,Block store提供存储,Chord提供路由查询

arch_chord_store

3. 基本的Chord协议

3.1. 概述

协议基于一致性散列,实现负载均衡,节点加入/离开时,只需移动$O(1/N)$的数据。

为提高扩展性,节点不需知道所有节点的路由信息,只需要$O(logN)$节点信息,查询也只需要经过$O(logN)$的节点。而路由信息更新只需要传输$O(log^2N)$条信息。

而Chord算法协议的核心就是一致性散列

3.2. 一致性散列

使用基本的散列函数(如SHA-1),将节点和key散列成一个$m$位长的标识符,$m$必须足够大(冲突概率足够小),整个散列空间也为$m$位,组成一个$0 \sim 2^{m}-1$的环。

具体散列和路由规则略。(因为很熟悉了)

论文中略去了虚拟节点。一般情况下,对于每个IPv4地址一个节点,32个虚拟节点是适合的。

定理1:$N$个节点和$K$个key,高概率下:

  • 每个节点接管最多$(1+\epsilon)K/N$个key
  • 当节点加入/离开,$O(K/N)$个键需要被其它节点接管

3.3. 可扩展的key位置

协议中,每个节点只需要知道它在环上的后继节点。通过不停地路由到后继节点,即可找到正确的节点位置。此外Chord维护了额外信息加速路由。

假定:

  • $m$为散列空间的位数
  • 每个节点$n$维护$m$个后继节点(简称finger table)

则定义有finger table,记录后继节点:

  • 表中的第$i$项($n.finger[i]$)为:
    • $start$:即$(n+2^{i-1}) \% 2^m$
    • $interval$:即区间$[n.finger[i].start, n.finger[i+1].start)$
    • $node$:比节点$n$大$2^{i-1}$的第一个后继节点,记为$s$。即$s=successor(n.finger[i].start)$ ($1\le i \le m$)

根据某个标识符$id$(散列值)查找后继者的伪码如下所示,基本思路是:

  • 先找$id$的最近前驱节点$n’$
  • 然后找到该节点$n’$的最近后继节点

对于远程调用和远程引用,使用RPC;对于本地调用和本地引用,直接本地执行

successor

定理2:大概率下,在$N$节点的集群,找到某个$id$的后继节点,需要和$O(logN)$个节点进行通信。

3.4. 节点加入

Chord需要保证每个key都能被定位,因此需要维护2个不变量:

  • 每个节点维护的后继者都是正确的
  • 对于每个key $k$,节点$successor(k)$负责$k$

最好,finger table也保证正确,以加快查找

定理3:大概率下,在$N$节点的集群,单节点的加入或离开需要传输$O(log^2 N)$条消息,以重建路由不变量和finger table

为维护不变量,每个节点维护一个前驱指针(可反向遍历节点),当节点$n$加入时,并执行3个任务:

  • 初始化节点$n$的前驱指针和finger table
  • 更新已存在节点的finger table和前驱指针,反映节点$n$的加入
  • 通知上层应用将数据迁移到新节点

node_add

3.4.1. 初始前驱和fingers

在$n.init _ finger _ table(n’)$中实现。

最初实现可通过$m$个RPC填充所有的fingers,复杂度为$O(mlogN)$。

不过可以检查第$i$项是否也是第$i+1$项(可检查$finger[i].interval$是否不含有节点,若不含,则成立)以减少RPC次数,从而降低到$O(log^2 N)$。

不过,实际优化中,新节点可直接查询最近的节点,获取整个finger table和前驱节点信息,从而降低时间复杂度为$O(logN)$

3.4.2. 更新已存在节点的fingers

在$n.update _ others()$实现,将$n$插入到其它节点的fingers中。

$n$成为节点$p$的finger第$i$项,需要满足:

  • $p$需要在$n$之前,且差距至少为$2^{i-1}$
  • $p$的原有finger第$i$项在$n$之后

伪码中,可以看出,它是一个沿时钟反向的递归流程

实现可以达到:

  • 需要修改$O(logN)$个节点的finger table
  • 整个更新时间复杂度为$O(log^2 N)$(可优化到$O(logN)$)

3.4.3. 迁移key

即通知上层应用迁移数据,具体实现需要看上层。

4. 并发操作和失效

4.1. 稳定协议

稳定协议:保证节点的后继节点指针(即$node.successor$)是最新的,用于验证和修正finger table。

当并发节点加入/离开时,可能出现下面3种情形:

  • 当前的finger table依旧正确(查找依旧正常,为$O(logN)$)
  • 当前后继者指针是正确的,finger table不准确(查询可以正确,但较慢)
  • 两者都不正确(查询会失败,可通知上层等待重试)

下述伪码可以替代第3节的伪码,用于支持并发的操作:

  • 节点加入时,调用$n.join(n’)$,只会获取后继指针
  • 周期性调用$n.stablelize()$,验证后继者是否正确,并告知新的后继者,调用$n.notify(succ)$以更新前驱指针
  • 周期性调用$n.fix _ finger()$以更新自己的finger table,每次更新一项(随机指定),最终能完全更新完毕

实质上,只要有正确的后继指针,则就可以查找成功(前驱和后继都可以),finger table只是加速查询罢了

stablization

定理4:一旦节点能处理某个查询,未来,对于该查询则都能处理。

定理5:最后的节点加入后,一段时间内,所有节点的后继指针都会是正确的。

定理6:已有一个稳定的$N$节点的集群,当另有$N$个节点加入集群且fingers为空(但有正确的后继指针),那么大概率下,查询依旧能保证在$O(logN)$下完成。

推论:只要调整fingers的时间小于网络扩大一倍的时间,也能保证$O(logN)$的查询

4.2. 错误和复制

当节点$n$失效,则其它包含$n$的finger table对应的节点需要找到$n$的后继者;此外,失效不能扰乱处理中的查询,尽管系统在重新稳定化。

恢复的关键:保持正确的后继指针

实现

  • 维护“后继者列表”(successor-list),包含自己在环上最接近的$r$个继承者(在$n.stablize()$中维护)
  • 若节点发现后继者指针不正确(即节点可能失效了),则选择列表中的第一项作为后继者
  • 稳定化后一段时间,finger table会被修正

节点失效,并在稳定化之前,其它节点依旧可以向失效节点发送请求,作为查找后继节点($find _ successor()$)的一部分。这种情况下,需要在节点失效后,选取另一条路由路径,这就要一个备用节点列表

  • 可在finger table中查找备用节点
  • 若失效节点在finger table中处于较小索引位置,也可用”后继者列表”选出备用节点

定理7:若后继者列表长度$r=O(logN)$,集群有$N$个节点且初始稳定,每个节点失效的可能性为$O(1/2)$,则大概率下,$find_ successor()$会返回最接近的活跃后继者。

定理8:若后继者列表长度$r=O(logN)$,集群有$N$个节点且初始稳定,每个节点失效的可能性为$O(1/2)$,则大概率下,$find _ successor()$在失效发生下的时间复杂度为$O(log N)$