From d30e121f3b9bea0d8050392a6896a6f9bf44cb4c Mon Sep 17 00:00:00 2001 From: om14 Date: Tue, 31 Oct 2023 15:38:57 +0800 Subject: [PATCH] 2022 --- _posts/2022/01/2022-01-23-redis-connection.md | 71 +++++ .../2022/02/2022-02-11-paxos-made-simple.md | 172 ++++++++++++ _posts/2022/02/2022-02-12-zab.md | 144 ++++++++++ _posts/2022/02/2022-02-15-redlock.md | 245 ++++++++++++++++++ .../03/2022-03-12-time-clock-and-ordering.md | 170 ++++++++++++ _posts/2022/03/2022-03-27-immuDB.md | 113 ++++++++ _posts/2022/03/2022-03-28-Kademlia-DHT.md | 90 +++++++ 7 files changed, 1005 insertions(+) create mode 100644 _posts/2022/01/2022-01-23-redis-connection.md create mode 100644 _posts/2022/02/2022-02-11-paxos-made-simple.md create mode 100644 _posts/2022/02/2022-02-12-zab.md create mode 100644 _posts/2022/02/2022-02-15-redlock.md create mode 100644 _posts/2022/03/2022-03-12-time-clock-and-ordering.md create mode 100644 _posts/2022/03/2022-03-27-immuDB.md create mode 100644 _posts/2022/03/2022-03-28-Kademlia-DHT.md diff --git a/_posts/2022/01/2022-01-23-redis-connection.md b/_posts/2022/01/2022-01-23-redis-connection.md new file mode 100644 index 0000000..0344a07 --- /dev/null +++ b/_posts/2022/01/2022-01-23-redis-connection.md @@ -0,0 +1,71 @@ +--- +layout: post +title: "Redis v6.2.0 server接收client命令" +date: 2022-01-23 14:51:01 +0800 +categories: Redis +--- + +以standalone模式按默认配置启动redis-server,只考虑最基本的情况 + +## 1. 准备连接 + +```c +struct redisServer { + int ipfd[CONFIG_BINDADDR_MAX]; /* TCP socket file descriptors */ + int ipfd_count; /* Used slots in ipfd[] */ +} +``` + +默认ipfd_count为2,一个监听本地TCP4,另一个监听本地TCP6。 + +Redis启动时,通过 +```c +int listenToPort(int port, int *fds, int *count) +``` +先创建socket,绑定端口并监听客户端请求。即此时完成了bind()和listen(),但还没有开始accept() + +```c + for (j = 0; j < server.ipfd_count; j++) { + if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, + acceptTcpHandler,NULL) == AE_ERR) + { + serverPanic( + "Unrecoverable error creating server.ipfd file event."); + } + } +``` +之后的main函数将调用aeCreateFileEvent()为本地监听的socket创建一个可读的文件事件,并将socket加入到server的epoll对象中,并以acceptTcpHandler作为监听socket的回调函数。 + +```c +void aeMain(aeEventLoop *eventLoop) { + eventLoop->stop = 0; + while (!eventLoop->stop) { + aeProcessEvents(eventLoop, AE_ALL_EVENTS| + AE_CALL_BEFORE_SLEEP| + AE_CALL_AFTER_SLEEP); + } +} +``` +所有的启动准备工作(启动监听、加载RDB等)结束后,main函数调用aeMain()循环处理所有的文件事件和时间事件 + +```c +numevents = aeApiPoll(eventLoop, tvp); + +for (j = 0; j < numevents; j++) { + + /* Fire the readable event if the call sequence is not + * inverted. */ + if (!invert && fe->mask & mask & AE_READABLE) { + fe->rfileProc(eventLoop,fd,fe->clientData,mask); + fired++; + fe = &eventLoop->events[fd]; /* Refresh in case of resize. */ + } +} +``` +在aeProcessEvents()中,aeApiPoll()会调用epoll_wait()获取当前已经就绪的事件,然后for循环依次处理,文件可读事件就会调用rfileProc回调函数。 + +假设这时有客户端发起连接,则for循环中rfileProc实际上就调用了之前注册的acceptTcpHandler函数用于处理连接请求。 + +## 2. 准备读取 + +在acceptTcpHandler中相当于是执行了listen之后的一次accept(),此时已经和client建立了连接但还没有读到client准备执行的命令。这时的情况就类似之前执行了listen()但没有accept()。实际上处理方式也和之前类似,即再为当前的连接设置一个文件读事件,将连接的socket加入到server的epoll对象中,回调函数是readQueryFromClient(),当有客户端命令发来时,readQueryFromClient()就可以读到客户端命令并调用相关的执行函数了。 \ No newline at end of file diff --git a/_posts/2022/02/2022-02-11-paxos-made-simple.md b/_posts/2022/02/2022-02-11-paxos-made-simple.md new file mode 100644 index 0000000..a1d932d --- /dev/null +++ b/_posts/2022/02/2022-02-11-paxos-made-simple.md @@ -0,0 +1,172 @@ +--- +layout: post +title: "Paxos Made Simple" +date: 2022-02-11 10:34:01 +0800 +categories: "6.824 分布式" +--- + +The Paxos algorithm, when presented in plain English, is very simple. --Leslie Lamport + +## 1 Introduction + +## 2 The Consensus Algorithm +### 2.1 The Problem +假设有一系列进程能propose values,一致性算法需要保证只有一个值被选中;如果没有值被提出,就没有值被选中;如果一个值已经被选中,那么这些进程应该能知道被选中的值。 + +一致性的安全要求是: + +* Only a value that has been proposed may be chosen +* Only a single value is chosen, and +* A process never learns that a value has been chosen unless it actually +has been. + +目标是保证某个被提出的值最终被选中,且进程能最终知道这个值。 + +一致性算法中有3种角色: +* proposers +* acceptors +* learners + +在实现中,一个进程可能会扮演多个角色。假设角色间可以互相通信,我们使用典型的异步、非拜占庭模型: + +* 所有角色按任意速度运行,可能停止或重启。由于所有角色可能都在某个值被选中后重启,因此,某个角色必须在宕机重启的过程中记住某些信息(持久化),否则无解 + +* 消息可以等任意时间后送达,可以重复或丢失,但不会被纂改 + +### 2.2 Choosing a Value + +*(注: 论文中有两个概念需要明确, Accept(接收)指Acceptor确认收到了某个提案;Chosen(选中)指某个提案被整个系统确认为最终的一致性提案。被Accept的提案不一定被Chosen,而被Chosen的提案一定是被Accept过的)* + +最简单的办法是只有一个acceptor,proposer向acceptor发送提案,acceptor选择接收最先收到的值。这种办法非常简单,但acceptor宕机后整个系统就不可用了 + +所以需要设置多个acceptor。一个proposer向多个acceptor发出提案的值。一个acceptor可能会接受这个值。只有当足够多的acceptor都接受了这个值,才能认为系统最终选中了这个值。为了保证只有一个值最终被选中,上文中的足够多实际要求是大于一半。 + +在没有宕机或消息丢失的情况下,即使只有一个proposer我们也希望能最终选中一个值,这要求以下条件 + +* P1. An acceptor must accept the first proposal that it receives. + +但这个条件引入新的问题,几个值可能几乎同时被提出,每个acceptor接受了不同的值,导致没有大多数产生。哪怕只有2个提案,当一个acceptor宕机后,也可能每个提案恰好被一半的acceptor接受,没有产生大多数。 + +P1和每个值必须被大多数acceptor接受的条件暗示每个acceptor必须接受不止一个提案。我们让acceptor给接受的多个提案分配一个数字(natural number),这样每个提案实际包含proposal number和value两个部分。为了防止迷惑,要求不同提案需要有不同的编号,编号如何产生依赖于实现,当前只需要假设有这样的编号机制。 + +*(注: 为什么上文说暗示每个acceptor必须接收不止一个提案? 假设一共有3台acceptor,其中一台宕机,还剩下两台运行,记为acceptorX和acceptorY,此时还没有任何提案发出。假设有一个proposer此时提出提案A发给X和Y,但发给Y的数据丢失,只有X收到A。此后另一个proposer发出提案B,同时发给X和Y且均正常到达。如果acceptor只能接收一个提案,那么结果是X收到A,Y收到B,没有提案能被选中。如果acceptor可以接收多个提案,X就能接收A和B,Y能接收B,最终选中提案B)* + +*(注: 编号的实际实现方式可以是 时间戳+proposerId 的组合,这样不同proposer的提案id一定不同,且按时间顺序单调递增)* + +可以允许多个提案被选中,但前提是这些提案的值都一样。再结合提案编号,有如下条件需要满足 + +* P2. If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v. + +*(注: 为什么更高编号的提案值只能和已经被选中的值保持一致?因为paxos是目的就是让多个进程对某一个单值达成一致,不同提案中的值并没有优劣或大小或新旧之分,后发起的提案并没有"更新"这个说法,既然已经有提案被选中,就完全没有必要再去修改被选中的值,因此只需要保持一致即可)* + +因为编号是完全有序的,P2就保证了只有一个value被选中的安全性 + +一个提案至少要被一个acceptor接受才可能被最终选中。因此,我们可以通过满足P2(a)来满足P2 + +* P2(a) If a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v. + +此时仍然保留P1的约束,用于保证一定有某个提案被选中。由于通信是异步的,一个提案可能被某个没有接收到任何提案的acceptor(记为c)所接收。假设有一个新的proposer“醒来”,发起一个更大编号且不同值的提案,那么条件P1要求c必须接受这个提案,违反了条件P2(a)。想要同时保证P1和P2(a)需要把P2(a)加强为P2(b) + +* P2(b) If a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v. + +由此可以从P2(b)推导出P2(a),P2(a)再推导出P2。 + +我们考虑如何满足P2(b),首先考虑证明如何保持P2(b)。假设有一个编号为m值为v的提案被选中了,我们将证明一种proposer发起提案的算法,使n > m的提案也具有值v。我们使用数学归纳法,即假设m..(n - 1)的提案值也全部是v。当提案m被提出且选中时,一定存在一个集合C,这个集合由大多数Acceptor组成,且C中的Acceptor全部都接收了提案m。那么C中所有Acceptor都会接收m到n-1的提案,且这些提案的值都是v。那么这时任意一个包含大多数Acceptor的集合S,其中某个Acceptor一定也属于集合C。通过维护P2(c)的性质,我们可以归纳出提案n的值是v + +* P2(c) For any v and n, if a proposal with value v and number n is issued, +then there is a set S consisting of a majority of acceptors such that +either (a) no acceptor in S has accepted any proposal numbered less +than n, or (b) v is the value of the highest-numbered proposal among +all proposals numbered less than n accepted by the acceptors in S. + +*(注: P2(c)是怎么推导出P2(b)的?当提案m被选中后,任意S集合一定有C集合中的acceptor,而这个acceptor接收的提案m就是编号最大的提案)* + +为了维护P2(c)的性质,当一个proposer想要发起提案n时,必须知道小于n的最大编号提案是否已经或将要被大多数acceptor所接收。查询是否已经被接收很容易,而预测很难。那么干脆不预测,而是控制其一定不会被接收。也就是说,proposer要求acceptor不要再接受编号小于n的提案,于是有如下proposer的算法: + +1. A proposer chooses a new proposal number n and sends a request to +each member of some set of acceptors, asking it to respond with: + + (a) A promise never again to accept a proposal numbered less than n, and + + (b) The proposal with the highest number less than n that it has accepted, if any. + +这样的请求被称为prepare request with number n + +2. If the proposer receives the requested responses from a majority of +the acceptors, then it can issue a proposal with number n and value +v, where v is the value of the highest-numbered proposal among the +responses, or is any value selected by the proposer if the responders +reported no proposals. + +第2阶段请求称为accept request + +目前描述了proposer的算法,那么acceptor呢?acceptor可以接收prepare request和accept request。acceptor可以忽略任何违反了一致性安全的请求。acceptor总是可以回复prepare request,但只有在没有违反承诺的情况下接受accept request。也就是可以说成P1(a) + +* P1(a) An acceptor can accept a proposal numbered n iff it has not responded +to a prepare request having a number greater than n. + +P1(a)可以保证P1 + +目前已经有了完整的算法,最后引入一些小优化。acceptor如果已经回复了一个大于n的prepare request,之后再收到n的prepare request,就没有必要再处理后来的这个n prepare request。同样,如果已经接收了提案n, 也可以忽略id为n的prepare request。 + +有了这种优化后,acceptor只需要记住已经接收的最大提案编号和已经回复的最大prepare request编号。因为P2(c)需要即使宕机也能保证性质,这些信息需要持久化。而proposer却不需要记住这些信息。 + +结合proposer和acceptor,我们可以得到如下两阶段算法 + +* Phase 1. (a) A proposer selects a proposal number n and sends a prepare +request with number n to a majority of acceptors. + + (b) If an acceptor receives a prepare request with number n greater + than that of any prepare request to which it has already responded, + then it responds to the request with a promise not to accept any more + proposals numbered less than n and with the highest-numbered proposal (if any) that it has accepted. + +* Phase 2. (a) If the proposer receives a response to its prepare requests +(numbered n) from a majority of acceptors, then it sends an accept +request to each of those acceptors for a proposal numbered n with a +value v, where v is the value of the highest-numbered proposal among +the responses, or is any value if the responses reported no proposals. + + (b) If an acceptor receives an accept request for a proposal numbered + n, it accepts the proposal unless it has already responded to a prepare + request having a number greater than n. + +一个proposer可以提出多个提案,只要每一个提案遵守算法规则即可。而且proposer也可以在任意时间丢弃提案。acceptor在收到为n的prepare request时候,如果已经提前收到大于n的prepare request,可以通知proposer丢弃这个提案,这算是一个不影响正确性的性能提升。 + +### 2.3 Learning a Chosen Value +为了知道哪个值最终被选中了,learner必须找出一个被大多数acceptor所接受的提案。最显然的算法是每次acceptor接收一个提案后,将这个消息发给每一个learner。这样做却需要大量的通信,一个简单的做法是在learner中选出一个leader,acceptor只发消息给leader,由leader再分发消息。但这样leader宕机后整个系统不可用,进一步优化则是同时维护一组leader。*(注: 这里体现出系统设计中的trade off)* + +### 2.4 Progress + +很容易构造出一种没有进展的场景,假设有两个proposer分别为p和q,p先发起prepare request 1,在p发起accept request 1之前,q又发起了prepare request 2,这时p的accept request 1不会被接收,之后在q的accept request 2发起前p又重新发起prepare request 3...这样下去永远不会有进展。 + +为了保证有进度,必须在proposer中选出一个leader,只有leader能发起提案。通信正常情况下,总能最终选中一个值。 + +### 2.5 The Implementation + +Paxos算法中,每个进程都扮演proposer,acceptor和learner。算法选举一个proposer leader和learner leader。Paxos中,request和response都作为普通消息传递。需要有稳定存储保留acceptor需要记住的信息。Acceptor在发出回复前先将回复记录在存储介质中。 + +为了保证两个提案的编号一定不同,不同的proposer从不同的集合中选择数字作为编号,且每个proposer需要持久化它所发起的最大提案编号。 + +## 3 Implementing a State Machine + +实现分布式系统的一种简单方式是将其作为一系列客户端向中心服务器发送命令。服务器可以被看作是确定状态机,按某种顺序执行客户端命令。使用单个中心服务器有单点故障的风险,因此准备一套服务器,每个服务器只要按相同顺序执行命令,就总会产生同样的结果 *(注: 因为是确定状态机)* + +为了让所有状态机按同样顺序执行命令,我们实现一整套独立的Paxos实例,第i个Paxos实例选中的值就是第i个将要执行的命令。现在,假设服务器是固定的,所有Paxos实例使用同一套agents + +在普通情况下,一台server被选举为所有Paxos实例的leader。客户端向leader发送命令,由leader决定命令的顺序。如果leader决定一条命令应当在135th的位置被执行,那么leader会尝试在第135个实例中选中该条命令。这种尝试大部分情况会成功,但也可能因为宕机或其他server任务自己是leader而失败,但一致性算法保证了最多只有1条命令在第135的位置被执行。 + +这种办法的关键在于,在Paxos一致性算法中,将要提出的提案值只有到了phase2才能确定。回顾一下,在phase1结束后,这个值要么已经决定了,要么可以是任何值。 + +接下来描述leader宕机后,新leader被选举后的场景。 + +新leader之前是learner,应当知道大多数已经被选中的值。假设新leader已经知道1-134,138和139的选中命令(下文将描述为何有间隔)。那么它接下来会为135-137和大于139的实例执行phase1。假设执行后只得到了135和140的结果,但不能确定136和137应当是哪条命令 *(注: 不是很理解为什么136和137不能获得结果,猜测是136和137的Phase1回复中,acceptor还没有接收任何提案,所以新leader不知道应该是什么命令)* + +目前为止,新leader和其他server一样,知道了1-135的命令,却不能执行138-140的命令,因为136和137是未知的。新leader可以把客户端接下来的两个命令填充到136和137,但实际上我们让136和137填充一个特殊的命令"no-op",这样使状态不变,然后再执行138-140,再之后,新leader就可以正常从client收到命令后,按自己想法填充之后的所有命令。 + +然后解释为什么会有间隔存在,因为leader可以在第n个命名确定被选中之前就发起第n + 1轮提案。leader所有关乎第n轮提案的消息可能全部丢失,而这时第n + 1轮已经确定,正常情况下leader会为第n轮提案重发消息,但如果这时leader宕机,就没有server知道第n轮提案中到底是什么命令。 + +当一个新leader被选举后,它可以执行任意轮phase 1。在上面的场景中,执行了135-127和所有大于139的轮次。使用相同的提案id,新leader可以向其他server发送一条简短的消息,这时的acceptor如果在之前已经收到过某个proposer的phase 2消息,那么acceptor不会仅仅回复一个OK,而是带上之前的提案,比如上文中的135和140. + +目前只讨论了一切正常,只有一个leader存在的情况(除了leader切换过程中有一小段时间没有leader)。在不正常情况下,有可能选举失败,或者有多个server认为自己是leader,那么这两种情况都有可能让系统不能选中值而停止进度,但并不会破坏一致性。单一leader只是保证了make progress + diff --git a/_posts/2022/02/2022-02-12-zab.md b/_posts/2022/02/2022-02-12-zab.md new file mode 100644 index 0000000..b02d709 --- /dev/null +++ b/_posts/2022/02/2022-02-12-zab.md @@ -0,0 +1,144 @@ +--- +layout: post +title: "ZooKeeper’s atomic broadcast protocol: Theory and practice" +date: 2022-02-12 22:08:01 +0800 +categories: "6.824 分布式" +typora-root-url: "../../../" +--- + +## 1 Introduction + +​ ZooKeeper是一个fault-tolerant distributed coordination service for cloud computing applications。它为其他云计算应用提供分布式协调算法并且维护一个简单的数据库 + +​ *(注: highly-available 主要指系统的performance,highly-reliable 指系统的容错性)* + +​ ZooKeeper目标highly-available and highly-reliable,所以多个客户端依靠它实现bootstrapping*(注: 自举? 感觉是指由ZooKeeper完成一些基础工作)*,存储配置信息,存储运行进程的状态,分组管理,实现同步原语和管理错误恢复。ZooKeeper通过replication来实现availability和reliability ,并且设计目标是在以读为主的场景中有优良表现。 + +​ ZooKeeper数据库由几个host server组成,一般是3或5个,其中一个是leader。只要leader可用,则整个系统可用。Zab是the ZooKeeper Atomic Broadcast algorithm,用于原子更新副本。Zab可以负责集群的leader选择,副本间的同步,管理更新信息以及从宕机中回复到可靠状态。本文将介绍Zab细节。 + +## 2 Background + +​ 广播算法(broadcast algorithm)把消息从一个primary进程传递到网络或广播域中的所有其他进程,包括primary进程本身。原子广播协议是一种分布式算法,保证要么正确广播,要么取消广播且没有任何副作用。这是一种在分布式计算的group communication中广泛使用的算法。原子广播也可以被定义为reliable broadcast that satisfies total order。比如,其满足以下性质 + +* **Validity**: If a correct process broadcasts a message, then all correct processes will eventually deliver it. +* **Uniform Agreement**: If a process delivers a message, then all correct processes eventually deliver that message +* **Uniform Integrity**: For any message m, every process delivers m at most once, and only if m was previously broadcast by the sender of m. +* **Uniform Total Order**: If processes p and q both deliver messages m and m', then p delivers m before m' if and only if q delivers m before m'. + +有很多种原子广播协议,ZooKeeper采用了Paxos思想,当然,也不是照搬Paxos。 + +### 2.1 Paxos and design decisions for Zab + +​ Zab有两个重要的要求,要能处理多个客户端操作(outstanding client operations)和从崩溃中快速恢复 。An outstanding transaction is one that has been proposed but not yet delivered. 为了高性能,ZooKeeper需要能处理多个客户端发来的变更状态请求和并发提交的FIFO请求。此外,ZooKeeper需要能在leader崩溃后高效率地恢复。 + +​ *(注: outstanding client operations中的outstanding应该是指已发出但还未commit的状态)* + +​ 原版的Paxos协议并不支持multiple outstanding transactions,Paxos不能保证通信内容是FIFO,所以能处理消息丢失和乱序。如果两次变更(transaction)间有顺序依赖,那么Paxos不能保证这种依赖关系。这个问题的解决办法是批处理,将多个变更整合为一个提案(proposal)并且一次只允许一个提案,但这种方案会有性能损耗。 + +​ The manipulation of the sequence of transactions to use during recovery from primary crashes is claimed to not be efficient enough in Paxos. 为了提高效率,Zab采用 transaction identification 机制保证所有transaction有序。在这种机制下,为了更新新primary process的状态,只需要查询所有进程最高的trans id(transaction identification),然后从已经接受了该trans的进程中直接复制trans。在Paxos中却不能这么做,所以需要为新primary进程之前还没有"learned a value"(ZooKeeper中术语是"committed a transaction")的序号重新执行Paxos的Phase 1阶段。 + +​ 此外,ZooKeeper还需要满足以下性能要求 + +* low latency +* good throughput under bursty conditions, handling situations when write workloads increase rapidly +* smooth failure handling, so that the service can stay up when some non-leader server crashes. + +### 2.2 Crash-recovery system model + +​ ZooKeeper把crash-recovery模型建模为系统模型。系统中有p(1), p(2), ... , p(n)共n个进程,本文中也称为peers,进程间可以互相通信,每个进程都有稳定的存储设备,且各个进程都可能无限崩溃和恢复。一个quorum指所有进程中某个超半数的集合。进程有up和down两个状态,进程从崩溃到开始恢复都是down状态,进程从恢复到下一次崩溃时都是up状态。 + +​ 每个进程对之间都有一个双向通道,通道满足以下性质 + +* **integrity** asserting that process p(j) receives a message m from p(i) only if p(i) has sent m +* **prefix** stating that if process p(j) receives a message m and there is a message m' that precedes m in the sequence of messages p(i) sent to p(j), then p(j) receives m' before m. + +​ 为了满足这些特性,ZooKeeper采用TCP,因此有FIFO通道来通信。 + +### 2.3 Expected properties + +​ 为了保证进程间一致性,有一些safety性质需要满足,首先给出一些定义。 + +​ 在崩溃-恢复模型中,如果primary进程*(注: 后文直接写作leader了)*崩溃,则需要选举一个新的leader。由于广播消息是全体有序(total ordered)的,因此要求任意时刻最多只有一个leader,每个leader在位的周期称为epoch。Transaction是指leader广播给其他进程的状态改变消息,以表示,v表示新的状态,z代表一个id称为zxid。Transaction首先被leader发起,然后提交(delivered/committed)给消息提交的方法。 + +​ 为了满足一致性,需要保证以下性质 + +* **Integrity**: If some process delivers , then some process has broadcast . +* **Total order**: If some process delivers before , then any process that delivers **must** also deliver before +* **Agreement**: If some process p(i) delivers and some process p(j) delivers , then either p(i) delivers or p(j) delivers . + +​ 然后是primary order性质 + +* **Local primary order**: leader不变时,进程必须按leader广播消息的顺序提交消息 +* **Global primary order**: leader有变更时,必须先提交旧leader的消息,再提交新leader的消息 +* **Primary integrity**: 如果当前leader广播了消息,而一些进程提交了更早leader的消息,那么当前leader在广播前也需要提交 + +## 3 Atomic broadcast protocol + +​ Zab中每个peer有3种状态: + +* following +* leading +* election + +​ follower和leader会顺序执行Zab的三个阶段 + +1. discovery + +2. synchronization + +3. broadcast + +​ 在Phase 1前,节点处于选举状态,其执行选举算法,让其他节点为自己投票。在Phase 1阶段开始,节点检查自己到底是follower还是leader。因此,选举阶段也被称为Phase 0. + +​ Leader节点和其他follower一起协调,并且在Phase 3阶段最多只能有1个leader,leader负责广播消息。Phase 1和Phase 2重要作用是让各个节点达成一致状态,特别是从崩溃到恢复状态。它们组成了协议中关于恢复的部分,并且保证了trans的顺序。如果没有崩溃发生,所有节点将始终保持Phase 3。在Phase 1,2,3,节点可以决定回到选举阶段,只要有任何错误或超时产生。 + +​ ZooKeeper客户端是应用程序,通过和至少一个节点通信来使用ZooKeeper服务。客户端提交操作给连接到的节点,如果该操作需要更新状态,Zab层就会广播。如果该操作提交给了follower,那么会转发给leader。如果leader收到了操作请求,它会执行并广播状态变更给follower。读请求可以直接由follower处理。通过发起sync请求,client可以确保连接到的server是最新状态。 + +​ 在Zab中,zxid是实现全体有序的重要组成。transaction中的z是,其中e是leader发起trans时候的epoch,c是计数用的counter。counter在每一次leader发起新的trans时增长。在新的epoch产生时,e会增大而c会归零。这样每个trans可以根据zxid排序。 + +​ 有四个变量组成每个节点的持久化状态,属于协议中恢复的一部分: + +* **history**: a log of transaction proposals accepted +* **acceptedEpoch**: the epoch number of the last NEWEPOCH message accepted +* **currentEpoch**: the epoch number of the last NEWLEADER message accepted +* **lastZxid**: zxid of the last proposal in the history + +### 3.1 Phases of the protocol + +​ **Phase 0: Leader election** 节点在这个阶段被初始化,进入选举状态。这里没有说必须要用某种特定的选举算法,只要保证绝大概率能选举成功即可。在选举结束后,节点存储它的投票到本地易失性存储中。如果节点p投票给了节点p',那么p'就是p的prospective leader。只有到phase 3后,如果p'真的收到过半数投票,才会成为established leader。在投票阶段,如果节点投了自己,就进入leading状态,否则是following状态。 + +​ **Phase 1: Discovery** 这个阶段中,follower和自己的prospective leader通信,这样潜在的leader可以从follower中收集它们已经accept的最新tx。目的是在一个quorum中**discover**出最新被接收tx的序号,然后开启一个新的epoch,让旧leader无法再commit新的提案,完整算法如下 + + + +![image-20220213175459619](/assets/2022/02/zab/phase1.png) + +​ 在阶段开始时,follower会和自己的prospective leader开始leader-follower连接。如果节点p不处于leading状态,而其他节点把p选作prospective leader且尝试leader-follower连接时,p会拒绝连接。被拒绝连接或遇到错误的情况下,会让follower重新回到Phase 0。 + +​ *(注: Phase 1主要是leader收集信息,根据其follower的状态决定epoch的数值,同时选择具有最新消息的follower的history作为自己的history)* + +​ **Phase 2: Synchronization** 同步阶段会包含协议中有关恢复的内容,在集群中同步副本会用到上一阶段leader更新后的history。Leader会与follower通信,从自己的history中propose tx。如果follower自己的tx落后leader,就会确认提案。Leader收到quorum确认后,向其发起一个commit消息。这时,leader就established且不再是prospective,算法2给出完整描述。 + +![image-20220213185229428](/assets/2022/02/zab/phase2.png) + +​ *(注: 算法2大概就是leader将自己收集到的最新tx分发给follower)* + +​ **Phase 3: Broadcast** 如果没有崩溃发生,节点们会永远保持在这个阶段,每当ZooKeeper客户端发起写请求后就广播tx。在一开始,a quorum of peers是一致的,且此时不会有两个leader同时存在。Leader会允许新的follower加入到epoch中,因为a quorum of follower已经足够发起phase 3了。为了追上其他的节点,新来的follower会接受tx广播,并且被包含在leader的已知follower列表中。 + + + +​ Phase 3是处理状态改变的唯一阶段,Zab层需要通知ZooKeeper应用它已经准备好状态改变。为此,leader在Phase 3开始时调用ready(e),使应用可以广播tx。算法3描述如下 + +![image-20220213190838762](/assets/2022/02/zab/phase3.png) + +​ *(注: 算法3中,有新的状态变更时,Leader先广播tx,follower收到后回复ACK,Leader收到足够多ACK后再向这些follower发送commit,follower收到commit后执行真正的commit。但如果当前tx前还有已收到未commit的tx,就会等待)* + +​ 算法1,2,3显然是异步且不考虑可能的节点崩溃。为了检测崩溃,Zab周期性地在follower和leader间检测心跳。如果leader在一段时间后仍没有收到a quorum of followers的心跳,它会放弃自己的领导权并回到Phase 0. Follower如果一段时间后仍没有收到leader心跳的话,也会重新进入选举状态。 + +### 3.2 Analytical results + +​ 这里简单提一些Zab满足的性质,更深入的证明要看其他论文。下面的invariants可以从上面的3组算法中简单推导,但claims需要使用invariants小心论证 *(注: 具体证明在另外两篇论文中)* + + + +*TODO 大致了解了Zab协议的内容,需要先消化一下和Paxos,Raft做比较,后面的证明和实现之后再继续* diff --git a/_posts/2022/02/2022-02-15-redlock.md b/_posts/2022/02/2022-02-15-redlock.md new file mode 100644 index 0000000..5f38227 --- /dev/null +++ b/_posts/2022/02/2022-02-15-redlock.md @@ -0,0 +1,245 @@ +--- +layout: post +title: "Redlock discussion" +date: 2022-02-15 21:25:01 +0800 +categories: "分布式 Redis" +typora-root-url: "../../../" +--- + +关于Redis实现分布式锁的讨论 + +# 1. Distributed locks with Redis + +*(注: 原文链接[在此](https://redis.io/topics/distlock))* + +本文将提出一种新的算法 **Redlock** + +#### Safety and Liveness guarantees + +在我们看来,高效实现分布式锁至少需要保证以下3个性质 + +* **Safety property**: Mutual exclusion. At any given moment, only one client can hold a lock. +* **Liveness property A**: Deadlock free. Eventually it is always possible to acquire a lock, even if the client that locked a resource crashes or gets partitioned. +* **Liveness property B**: Fault tolerance. As long as the majority of Redis nodes are up, clients are able to acquire and release locks. + +#### Why failover-based implementations are not enough + +​ 为了理解本文提出的改进,先分析下当前大多数Redis分布式锁的情况。 + +​ 最简单方法是获取锁时创建一个key,且给key设置过期时间,释放锁时删除key。 + +​ 表面上看不错,但有一个问题: 无法应对单点故障。如果使用主从模型,master挂掉后使用slave呢?这仍然不可靠,因为Redis复本更新是异步的 + +​ 在这个模型中有明显的竞争条件: + +1. Client A acquires the lock in the master. +2. The master crashes before the write to the key is transmitted to the replica. +3. The replica gets promoted to master. +4. Client B acquires the lock to the same resource A already holds a lock for. **SAFETY VIOLATION!** + +​ 在某些特殊情况,这种多个客户端能同时拿到锁也许是OK的,那么可以使用上述算法,否则推荐使用下面的算法。 + +#### Correct implementation with a single instance + +​ 先看简单的单机情况,加锁时设置 + +``` + SET resource_name my_random_value NX PX 30000 +``` + +​ NX使只有key不存在时才能成功,PX使key30000毫秒后失效。设置的value必须保证全局唯一(across all clients and all lock requests) + +​ 一般使用随机的值保证释放锁的安全性,使用以下Lua脚本 + +```lua +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + +​ 主要是防止这样一种情况: Client A先加锁,执行了过长的计算过程,导致A设置的key已经过期且B获得锁,当A结束计算准备释放锁时,不能误把B的锁删了*(注: 类似CAS的思想)*。 + +​ 随机值的设定有很多种方法,比如当前时间戳带上client id。目前的算法在单机具有always available的保证下已经安全了,然后会在没有保证的情况下讨论分布式锁。设置的过期时间长度也叫lock validity time或auto release time。 + +#### The Redlock algorithm + +​ 在分布式环境下,假设我们有N台Redis master,这些节点完全独立,可能宕机。我们不使用副本机制或其它隐式的协调系统。本文中N = 5,客户端获取锁的算法如下 + +1. It gets the current time in milliseconds. +2. It tries to acquire the lock in all the N instances sequentially, using the same key name and random value in all the instances. During step 2, when setting the lock in each instance, the client uses a timeout which is small compared to the total lock auto-release time in order to acquire it. For example if the auto-release time is 10 seconds, the timeout could be in the ~ 5-50 milliseconds range. This prevents the client from remaining blocked for a long time trying to talk with a Redis node which is down: if an instance is not available, we should try to talk with the next instance ASAP. +3. The client computes how much time elapsed in order to acquire the lock, by subtracting from the current time the timestamp obtained in step 1. If and only if the client was able to acquire the lock in the majority of the instances (at least 3), and the total time elapsed to acquire the lock is less than lock validity time, the lock is considered to be acquired. +4. If the lock was acquired, its validity time is considered to be the initial validity time minus the time elapsed, as computed in step 3. +5. If the client failed to acquire the lock for some reason (either it was not able to lock N/2+1 instances or the validity time is negative), it will try to unlock all the instances (even the instances it believed it was not able to lock). + +*(注: 在单机的基础上,要求获得过半数节点的支持才获取锁成功。且在获取锁的时候,考虑到了访问每个Redis实例共花费的时间,一个优化是在访问每个Redis实例时设置timeout)* + +#### Is the algorithm asynchronous? + +​ 这个算法需要满足一个前提: 当进程间没有同步时钟时,每个进程的本地时间是差不多的,至少时间误差和锁的auto-release time相比很小。这个假设在现实世界中也是基本成立的。此时,我们需要重新更准确定义client持有锁的时间,应当是lock validity time减去为了获得锁花费的时间 + +#### Retry on failure + +​ 当客户端无法拿到锁时,其应该在随机延迟后重试,随机是为了防止产生脑裂而没有winner产生。同时,客户端拿到超半数成功的时间越短,产生脑裂的可能就越小,因此理想情况下,客户端应当使用multiplexing同时向Redis实例发消息。 + +​ 同时要强调,当client无法拿到超半数锁时,应当尽快把已经拿到的锁释放。 + +#### Releasing the lock + +​ 释放锁就是在所有实例上释放获得的锁 + +#### Safety arguments + +​ 首先讨论一个客户端能获取到超半数锁的情况。假设此时所有实例都设置成功,有相同的TTL,但不同实例设置成功的时间并不相同,因此各个实例实际key过期的时间有差异。假设第一个key最晚在T1时间设置成功,最后一个key最晚在T2时间设置成功,则所有实例key都存在的时长是 `MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT` + +​ 当超半数key都设置成功后,如果再来一个client尝试获取分布式锁,一定会获取失败。 + +​ 但我们还想要保证多个client同时获取锁时,不能同时成功。如果一个client获取到了超半数锁,但消耗的时间邻近或超过了maximum validity time,那么client会认为获取锁失败然后释放锁。因此我们只需要考虑耗时小于validity time的情况。根据上文可知,MIN_VALIDITY时间段内,是不可能获取超半数锁的。所以多个客户端同时获取锁的情况只能是一个client获取到锁后发现已经过了TTL,这个client会再释放锁。不过这里的安全性还需要形式化证明。 + +#### Liveness arguments + +​ 系统的liveness基于以下3个特点 + +1. The auto release of the lock (since keys expire): eventually keys are available again to be locked. +2. The fact that clients, usually, will cooperate removing the locks when the lock was not acquired, or when the lock was acquired and the work terminated, making it likely that we don’t have to wait for keys to expire to re-acquire the lock. +3. The fact that when a client needs to retry a lock, it waits a time which is comparably greater than the time needed to acquire the majority of locks, in order to probabilistically make split brain conditions during resource contention unlikely. + +#### Performance, crash-recovery and fsync + +​ 为了保证高性能,可以使用multiplexing同时发请求。 + +​ 同时考虑容错能力,假设我们配置的Redis实例没有任何持久化机制,当前client获取到了5个锁中的3个。然后这3台实例中有一台出错后重启,重启后就丢失了之前的key,此时另一个client可以再获得3个锁,造成不一致。 + +​ 一种办法是开启AOF持久化,但持久化一般是异步写数据到磁盘的,比如每秒一次,那么仍然可能key在被持久化前实例就重启了。如果开启同步持久化,则Redis性能会大打折扣。 + +​ 另一种解决办法是,几乎不用考虑持久化,而是让重启后的实例等待一段时间后再加入分配锁的群体中。等待时间需要超过最大的TTL。不过当超半数机器重启时,会对整个系统带来TTL的延迟。 + +#### Making the algorithm more reliable: Extending the lock + +​ 如果client的任务由几个小步骤组成,可以默认使用较短的 lock validity time,当快到过期时间时,client向所有实例发送一个Lua脚本尝试延长锁的时间。延长锁仍然需要得到超半数支持和在 validity time内完成。延长锁的机制并没有改变算法,因此需要限制重新获取锁的尝试次数,否则会破坏liveness性质 + + + + + +# 2. How to do distributed locking + +*(注: 原文链接[在此](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html))* + +​ Redis本身很适合transient, approximate, fast-changing data between servers,而且能容忍偶尔数据丢失的场景。但Redis逐渐进入对数据一致性和持久性有要求的领域,这本身并不是Redis的设计目标之一。 + +## What are you using that lock for? + +​ 从高层视角看,在分布式应用中需要锁的原因有两个: efficiency或者correctness。 + +* **Efficiency**: 使用锁可以防止同样的任务被多次执行,尤其是一些运算量大的任务。如果锁失败,两个节点做了完全一样的工作,那么整个资源的消耗就加倍。 +* **Correctness**: 保证运算结果正确 + +​ 两种情况都可能需要锁,我们需要明确自己到底需要哪一种。 + +​ 如果使用锁只是为了efficiency,那么不需要引入Redlock运行5个server和检查超半数实例的开销和复杂度。最好只使用一个Redis实例,或者再开启异步同步到其他节点以防master崩溃。 + +​ 如果只使用一个Redis实例,不可避免地会有Redis实例出现故障的情况,但因为此时锁的用途只是为了Efficiency,所以偶尔的故障不是大问题,这本身也是Redis自己的特点。因此这种锁的使用方法可以用在不是非常关键的场景。 + +​ 另一方面,Redlock算法,靠5个副本和大多数投票,乍一看很适合追求correctness的场景,但实际可能并不适合。下文将详细讨论 + +## Protecting a resource with a lock + +​ 先不管Redlock本身,首先讨论分布式锁通常是如何使用的。需要明确的是分布式锁和多线程中的mutex不同,要更复杂,因为不同节点可能会因为各种原因故障,网络也可能各种原因故障。 + +​ 例如,假设有一个应用,client需要更新存储。client获取锁 -> 读文件 -> 修改文件 -> 写回修改后的文件 -> 释放锁。在这个场景中,锁阻止两个client同时修改文件,可能造成更新丢失(lost updates)。代码可能如下: + +```java +// THIS CODE IS BROKEN +function writeData(filename, data) { + var lock = lockService.acquireLock(filename); + if (!lock) { + throw 'Failed to acquire lock'; + } + + try { + var file = storage.readFile(filename); + var updated = updateContents(file, data); + storage.writeFile(filename, updated); + } finally { + lock.release(); + } +} +``` + +​ 不幸的是,即使lock service完全正确,代码也是错误的,下面的图片示例 + + + +![image-20220305203638145](/assets/2022/02/redlock/image-1.png) + +​ Client1拿到锁后遇到GC,可能会暂停几分钟,导致运算超时,在自己没有写回数据前将锁释放,最终造成数据不一致。HBase曾经遇到过这个真实问题。 + +​ 也许会想到在准备写回的时候再次检查,但GC是可能发生在任意时间的,因此引入检查是不可行的。 + +​ 也许会想到自己用的编程语言没有那么长的GC,但可能有其他事件导致进程中断,比如page fault,比如等待网络数据,比如CPU有很多进程需要调度,或者某人给当前进程发送了SIGSTOP信号。总之,进程很可能被暂停。 + +​ 这就是上面代码出问题的原因,lock service再好也没有用。 + +## Making the lock safe with fencing + +​ 一个简单的解决办法是在每次写回文件时引入 fencing token。在当前场景中,fencing token可以是递增的数字(由 lock服务提供),每次获取锁时,同时获得一个数字作为fencing token + +![image-20220305211111643](/assets/2022/02/redlock/image-2.png) + +​ fencing token由lock服务提供,比如假设ZooKeeper 是lock服务,那么可以返回zxid或者znode version number作为fencing token。 + +​ 到此,引入了一个Redlock的问题: 没有办法生成fencing token。Redlock算法没有在每次获取锁时产生任何编号。这意味着即使Redlock没有问题,也是不安全的,因为可能存在一个client进程被暂停,即上一节讲的问题。 + +​ 如果要修改Redlock算法来产生生成fencing token也是不容易的。Redlock使用的随机值是不保证单调性的。如果简单地在Redis实例中维护一个counter也是不可靠的,因为实例可能会出故障,如果再引入多个实例来容错则要求节点间信息的同步和保证数据一致性, 则需要再引入共识算法,整个系统非常复杂。 + +## Using time to solve consensus + +​ Redlock很难生成fencing token这一点已经足够说明其不适用于correctness的场景。但还有其他一些问题值得讨论。 + +​ 在学术领域,这类问题最现实的模型是[asynchronous model with unreliable failure detectors](http://courses.csail.mit.edu/6.852/08/papers/CT96-JACM.pdf). 简单说就是该算法不对任何timing作假设: 进程可能暂停任意长时间,数据包可能在网络中延迟任意时间,并且时钟也可能任意错误,算法也不预期做正确的事。 + +​ 算法中唯一需要使用时钟的是生成timeout,是为了避免无限等待。但timeout不需要完全准确,因为一个请求timeout可能是数据包在网络中有延迟,也可能是本地时钟出错。总之,timeout作为failure detector,只是猜测某个环节出现问题。 + +​ 注意Redis使用的是gettimeofday来决定key是否过期,这样时钟完全可能突然向前或向后(由管理员手动调整或NTP服务器调整),那么key在Redis中实际的存在时间可能和预期非常不同。 + +​ 对于异步模型中的算法,这些不是大问题: 这些算法不需要任何timing假设就可以保证safety性质永远满足。只有liveness性质需要依赖timeout或其他错误检测。简单说,系统timing出现任何问题(进程暂停、网络延迟、时钟跳转)等都只影响算法的效率,而不会让算法做出错误的决定。 + +​ 然而,Redlock并不是这种模型,其safety性质需要timing假设: 假设了Redis节点几乎正确地维护了key的存在时间、网络延迟相比失效时间更小且进程暂停时间远比失效时间短。 + +## Breaking Redlock with bad timings + +​ 看一些具体的例子,假设有5个Redis实例A B C D E,两个client 1和2 + +​ 当有一台Redis实例的时钟跳转时: + +1. Client 1 acquires lock on nodes A, B, C. Due to a network issue, D and E cannot be reached. +2. The clock on node C jumps forward, causing the lock to expire. +3. Client 2 acquires lock on nodes C, D, E. Due to a network issue, A and B cannot be reached. +4. Clients 1 and 2 now both believe they hold the lock. + +​ 还有一种类似场景是C在持久化锁到磁盘前故障,然后立即重启。Redlock中已经推荐了延迟重启,但如果时钟突然快进,实例还是会立即重启 + +​ 如果认为时钟突变实际很难发生,也可以看下面进程暂停的例子 + +1. Client 1 requests lock on nodes A, B, C, D, E. +2. While the responses to client 1 are in flight, client 1 goes into stop-the-world GC. +3. Locks expire on all Redis nodes. +4. Client 2 acquires lock on nodes A, B, C, D, E. +5. Client 1 finishes GC, and receives the responses from Redis nodes indicating that it successfully acquired the lock (they were held in client 1’s kernel network buffers while the process was paused). +6. Clients 1 and 2 now both believe they hold the lock. + +​ 虽然Redis是用C写的,没有GC,但是客户端是可能遇到GC的,这种情况回到了最初的问题,需要引入fencing token等机制 + +​ 长的网络延迟同样可能造成和进程暂停一样的效果,取决于TCP设置的timeout。如果设置TCP timeout远小于Redis TTL,那么情况可以忽略,但这种情况又需要保证时钟的准确性。 + +## The synchrony assumptions of Redlock + +​ 这些例子表面Redlock只适用于假设的同步系统模型(synchronous system model),即需要满足以下性质 + +* **bounded network delay**: you can guarantee that packets always arrive within some guaranteed maximum delay +* **bounded process pauses**: in other words, hard real-time constraints, which you typically only find in car airbag systems and suchlike +* **bounded clock error**: cross your fingers that you don’t get your time from a [bad NTP server](http://xenia.media.mit.edu/~nelson/research/ntp-survey99/) + +​ synchronous model并不需要同步时钟完全正确,只是需要误差不能超过某一上界。Redlock假设这些延迟、暂停、漂移和锁的TTL相比都很小,如果误差较大,算法就失效了。 diff --git a/_posts/2022/03/2022-03-12-time-clock-and-ordering.md b/_posts/2022/03/2022-03-12-time-clock-and-ordering.md new file mode 100644 index 0000000..5b81baf --- /dev/null +++ b/_posts/2022/03/2022-03-12-time-clock-and-ordering.md @@ -0,0 +1,170 @@ +--- +layout: post +title: "Time, Clocks, and the Ordering of Events in a Distributed System" +date: 2022-03-12 11:43:01 +0800 +categories: "分布式" +typora-root-url: ../../../ +--- + + + +## Introduction + +​ 现实世界中,我们习惯有时间的概念,而且事件发生有先后顺序,before或者after。分布式系统由多个时空分隔的进程组成,以交换信息的方式通信。一台计算机也可以被看做是分布式系统,因为计算单元,存储单元,IO单元等由不同进程组成。如果消息传递延迟和单个进程中事件间隔时间比不可忽略时,系统就是分布式的。 + +​ 本文主要关注时空分隔的不同计算机组成的分布式系统。分布式系统中有时不可能知道两件事件哪一个先发生。"happened before"关系在系统中只是一种偏序(partial ordering)关系。本文将讨论由happened before原则定义的偏序关系,然后给出分布式算法将其拓展为所有事件的一致性全序(consistent total ordering) + + + +#### *注: 什么是偏序和全序?* + +​ 偏序和全序并不是论文中的内容,这里只是简单作为前置知识补充说明 + +如果集合中的某种关系满足以下3个性质,那么这个关系是偏序([Partial Order](https://mathworld.wolfram.com/PartialOrder.html)) + +* **Reflexivity:** 集合中所有元素a,都有aRa +* **Antisymmetry:** 如果aRb且bRa,那么a=b(或者说a不是b的话,如果aRb,那么bRa不成立) +* **Transitivity:** 如果aRb且bRc,那么aRc + +全序是偏序的一种特殊情况,全序可以对集合中任意两个元素进行比较,但偏序不能,偏序只能在部分元素间进行比较 + + + +## The Partial Ordering + +​ 如果a在比b之前的某个时刻先发生,大多数人会说事件a比事件b先发生(happened before),但这种断言可能只有在物理学时间理论中成立。实际上,如果系统正确满足某种规范(specification),那么这种规范必须给出系统中可观测事件的依据。如果规范以物理学中的时间理论为依据,那么系统就必须有物理时钟。但即使有了时钟,时钟本身也不是完全准确而导致会出现问题。因此我们不使用物理时钟而定义了"happened before"关系 + +​ 我们首先更准确地描述我们的系统。我们假设系统由一系列进程组成,每个进程包含一系列顺序事件。取决于应用程序自身,可以将一台计算机上运行的一段子程序看作一个事件,也可以将一条机器指令的执行看作一个事件。我们假设一个进程有事件序列,如果事件a比事件b先发生,那么事件a在序列中的位置比b靠前。换句话说,单个进程由一系列具有priori total ordering性质的事件组成 + +​ 我们假设在一个进程中发送或接收消息是一个事件。我们可以定义happened before关系,用→表示 + +​ 定义: 一个系统中的事件集合上的关系→满足以下3个条件 + +* 如果a和b是同一个进程中的事件,且a比b先发生,那么a→b +* 如果a是一个进程发消息,而b是在另一个进程接收这一条消息,那么a→b +* 如果a→b且b→c,那么a→c。如果a→b和b→a**都不成立**,那么两个不同的事件a和b是并发的*(注: 这一条就说明→只能是偏序关系了,因为两个并发事件不能比较先后顺序)* + +​ 我们设定a→a**不成立**,因此在系统中→是irreflexive partial ordering*(注: 这里就和普通的偏序略有差别了,普通偏序是有自反性的)* + +​ 接下来看下space-time diagram中关于上述定义的解释。 + + + +![image-20220312151300420](/assets/2022/03/time-clock-ordering/time-clock-ordering-1.png) + +​ 水平方向代表空间,竖直方向代表时间,且时间向上增长,点代表事件,竖直线代表进程,波浪线代表消息。很容易发现a→b意味着可以从点a沿着进程线或消息线向上移动到b点。比如在图一中p1→r4 + +​ 另一种解读视角是a→b代表事件a可能影响到事件b。如果两个事件不能相互影响,那么它们是并发的。比如图一中事件p3和q3,虽然从图中能看出q3在物理世界中先发生,但进程P并不知道进程Q在q3的结果,直到p4接收了进程Q的消息。(在p4之前,P最多知道进程Q**打算**在q3做什么) + +​ + +## Logical Clocks + +​ 现在为系统引入时钟,一开始的抽象时钟只是为事件分配一个编号。更准确的说,为每个进程$P_i$ 定义一个时钟$C_i$,时钟将为进程中的每个事件$a$分配编号$C_i\langle a\rangle$ 。目前认为这是一个逻辑时钟,可以通过计数器实现而不借助任何真正的计时工具。 + +​ 然后考虑这样的时钟系统正确性意味着什么,我们不能基于物理时间的定义,我们只能基于事件发生的先后顺序。最严格的条件是如果事件a发生在事件b之前,那么事件a应该在事件b发生前的某个时间发生,更正式的,有: + +* **Clock Condition**: for any events a, b: if a → b,那么$C\langle a\rangle$ < $C\langle b\rangle$ + +​ 注意我们不能从$a\nrightarrow b$推导出两者时钟上的关系。但可以从$\rightarrow$ 关系中推导出其满足以下两个条件 + +* **C1.** if a and b are events in process $P_i$, and a comes before b, then $C_i\langle a\rangle < C_i\langle b\rangle$ +* **C2.** if a is the sending of a message by process $P_i$ and b is the receipt of that message by process $P_j$, then $C_i\langle a\rangle < C_j\langle b\rangle$ + + + +![image-20220312160055075](/assets/2022/03/time-clock-ordering/time-clock-ordering-2.png) + +![image-20220312161200894](/assets/2022/03/time-clock-ordering/time-clock-ordering-3.png) + +​ 重新考虑space-time图中的时钟,我们假设进程时钟会经过每一个数字,比如在某个进程中,事件a发生在数字4,事件b发生在数字7,那么二者间隔中,时钟经历了5、6、7。我们在图一的基础上绘制点状的tick line得到图二。条件C1意味着一个进程中任意两个事件间都有一条tick line,条件C2意味着每条消息线都必须穿过一条tick line。 + +​ 我们可以把tick line看作是某个笛卡尔坐标系的时间坐标轴,然后在图二的基础上拉直这些tick line。在没有引入物理时间概念(需要引入物理时钟)时,不能区分图二和图三哪种表达方式更好。 + +​ 让我们假设这些进程是算法,事件代表进程执行过程中的某个动作。接下来将展示如何引入时钟到进程且满足时钟条件(Clock Condition)。进程$P_i$的时钟由一个寄存器$C_i$表示,$C_i$的值会在事件间改变,因此其值的改变并不代表有新的事件产生。 + +​ 为了保证系统满足Clock Condition,我们需要保证满足C1和C2。条件C1很简单,只用遵守以下实现规则: + +* **IR1.** Each process $P_i$ increments $C_i$ between any two successive events + +​ 为了满足条件C2,我们需要每条消息m包含一个时间戳$T_m$,$T_m$的值就是该消息发送出去的时间。当进程收到一个时间戳为$T_m$的消息时,其必须快进时钟到大于$T_m$的时刻,准确地说,有以下规则 + +* **IR2.** (a) If event a is the sending of a message m by process $P_i$, then the message m contains a timestamp $T_m = C_i\langle a\rangle$ + + ​ (b) Upon receiving a message m, process $P_j$ sets $C_j$ greater than or equal to its present value and greater than $T_m$ + +​ 在IR2(b)中,我们认为接收消息m的事件发生在设置$C_j$后。很明显,IR1和IR2保证满足了Clock Condition + +## Ordering the Events Totally + +​ 我们可以使用满足Clock Condition的时钟系统来为所有的事件排序。我们可以简单根据事件发生的时间进行排序。为了打破限制,我们使用进程的任意全排序(total ordering)$\prec$ *(注: 可以先简单理解为给每个进程分配了互不相同的优先级)*。更准确地,定义以下一种关系$\Rightarrow$ : + +​ 如果a是进程$P_i$的事件,b是进程$P_j$的事件,那么$a\Rightarrow b$有且只有当(1)$C_i\langle a\rangle < C_j\langle b\rangle$ 或者(2) $C_i\langle a\rangle = C_j\langle b\rangle$且$P_i \prec P_j$ + +​ 很容易看出这定义了全序关系,且Clock Condition暗示了如果$a \rightarrow b$ 则有$a \Rightarrow b$。也就是说,关系$\Rightarrow$是补充了happened before的偏序关系为全序关系 + +​ 关系$\Rightarrow$取决于系统的时钟$C_i$ ,并且不唯一。满足Clock Condition的不同时钟选择会产生不同的关系$\Rightarrow$.给定任何从关系$\rightarrow$扩展来的关系$\Rightarrow$,总有一种时钟系统能满足Clock Condition且生成关系$\Rightarrow$。只有关系$\rightarrow$是由系统的事件唯一确定的 + +​ 能对所有事件全排序对实现分布式系统是非常有帮助的。实际上,正确实现逻辑时钟的原因就是为了获得这样的全序关系。使用这种全序关系,可以解决以下问题。 + +​ 考虑有一个系统由几个固定进程组成,进程共享一个资源。资源一次只能被一个进程使用。我们希望找到一种算法,使以下三个条件成立 + +(1). 一个进程被授权使用资源后,在资源下一次被授权给另一个进程前,必须先释放资源 + +(2). 不同的资源请求必须按它们产生(发起请求的时间)的顺序依次满足 + +(3). 如果每个进程最终都释放了申请到的资源,那么每个请求最终都能被满足 + +​ 我们假设资源最开始被赋予了某一个进程。 这些条件是非常自然的,条件(2)说明了处理两个并发请求的顺序。 + +​ 需要认识到这不是一个小问题,使用中心化调度程序,按接收请求的顺序授权资源是错误的,除非有更多的场景假设。比如假设P0是中心调度程序,P1发送了资源请求消息给P0,然后P1又发消息给P2。可能P2先收到P1的消息,然后也发资源请求给P0,有可能P2的请求比P1请求先到达。如果P0按接收顺序给P2授权,就违反了上面的条件(2)。 + +​ 为了解决这个问题,实现一套满足IR1和IR2的时钟系统,并且使用它们定义所有事件的全序关系$\Rightarrow$ .这样就为所有请求操作和释放操作确定了顺序,有了这种顺序,再去寻找正确的算法就很容易,只需要让每个进程知道其他进程的操作。 + +​ 为了简化这个问题,做一些假设,这些假设不是必须的,但可以让我们避免陷入太多细节。首先假设对所有的任意两个进程$P_i$和$P_j$,消息从$P_i$到$P_j$的接收顺序和发送顺序一样。而且我们假设每条消息最终都能到达(这种假设可以避免引入消息编号和消息确认协议),另外假设每两个进程间可以直接通信。 + +​ 每个进程维护一个别人看不见的request queue。我们假设request queue最初包含单条消息$T_0:P_0$ request resource,$P_0$是最开始被授权资源的进程,$T_0$小于任何时钟的初始值。 + +​ 算法按以下5条规则定义(rule)。为了方便,认为每条规则的动作都是单个事件 + +1. 为了请求资源,进程$P_i$ 发送消息$T_m:P_i$ request resource给其他所有进程,并且把这条消息放入自己的request queue。$T_m$是消息的时间戳 + +2. 当进程$P_j$接收到消息$T_m:P_i$ request resource ,将这条消息放进自己的request queue并且发送一条带时间戳的确认消息给$P_i$ + +3. 为了释放资源,进程$P_i$从request queue中移除所有$T_m:P_i$ request resource消息并且发送一条带时间戳的$P_i$ release resource消息给其他所有进程 + +4. 当进程$P_j$收到$P_i$的release消息,就移除其request queue中所有的$T_m:P_i$ request消息 + +5. 进程$P_i$被授权资源时需要满足两个条件 + + (a) 有一条$T_m:P_i$ request消息在其request queue中且比队列中其他所有请求按关系$\Rightarrow$(消息按发送时间排序)都更早 + + (b) $P_i$在$T_m$时间后有收到其他所有进程的消息 + +​ 需要注意条件5是完全由$P_i$自己在本地检测的。 + +​ 容易证明上述算法满足上文的条件(1)~(3)。首先,观察rule5的条件(b),同时有消息按顺序接收的假设,保证了$P_i$已经知道了在自己请求前的所有请求。由于只有rule3和4可以删除request queue中的消息,可以证明条件(1)成立。 + +*(注: 证明: 如果条件(1)不成立,即资源在被下次授权前没有释放的话,那么request queue中就还有本次已获得授权的请求,破坏了rule5中条件(a) )* + +​ 条件(2)成立的原因是定义了全序关系,而且rule2保证了在$P_i$发起请求后,rule5 (b)最终会成立。 + +*(注: 这里不是很懂为什么这样证明了条件2成立?一种粗浅的理解: 我猜这个request queue存消息应该是按全序关系排序的?那么这样的话两个几乎同时发起的请求之间有确定的先后顺序。假设现在两个进程一前一后非常近时间地发起了请求,进程A的请求先于进程B的请求,在A和B收到对方的请求之前(消息延迟),各自请求在自己的request queue中都是排名第一的,这时如果没有rule5 (b),A和B会同时获取资源。而造成这个问题的原因是A和B各自发起的请求都还没有完全通知到其他进程,因此rule5 (b)实际确保了进程发起的请求已经通知到其他所有进程。这里还有一点比较tricky的是,这个问题首先就假设好了消息从进程$P_i$到进程$P_j$的接收顺序和发送顺序一样,即消息是FIFO的,那么进程A在收到自己的确认前一定收到了B的请求,进程B在收到自己的确认前也一定收到了A的请求。再进一步,进程A在收到某个确认X时,一定收到了进程X发出确认前的所有请求(我们并不关心发出确认后的请求,因为之后的请求一定比A当前的请求晚)。所以进程A收到所有确认时,实际是和所有其他进程沟通好了当前请求和其他请求的相对位置)* + +​ Rule3和4意味着如果每个被授权资源的进程最终都能释放资源,那么rule5 (a)最终会成立,这证明了条件3. + +​ 这是分布式算法,每个进程独立遵守运行规则,不依赖中心调度或中心存储。可以把这种同步机制特定为状态机(State Machine),状态机的command是request resource或release resource,state是a queue of waiting request commands,队列头是当前被授权的请求,有新的请求发来时,添加到队列尾。 + +​ 每个进程可以独立模拟状态机的执行过程。因为所有commands可以按全序关系排序,因此每个进程会看见相同的命令顺序。一个进程可以执行时间戳为T的命令,当且仅当已经了解到其他所有时间戳小于等于T的命令。这种方法可以在分布式系统中实现任意需要的多进程同步,但却需要所有进程都是active的,如果某一个进程挂了,则整个系统崩溃。 + +## Anomalous Behavior + +​ 这套调度算法利用全序关系为请求排序,这允许了以下一种anomalous behavior: 假设有一套国家范围内计算机组成的系统,一个人在计算机A上发起了请求,然后他打电话给在另一个城市的朋友,让朋友在另一台计算机B发出请求B。那么很有可能请求B携带更小的时间戳,并且排序在请求A之前。这种情况发生的原因是系统没有办法知道A实际比B更先,因为关于顺序的信息是存在于系统之外的。 + +​ 有两种办法解决这个问题,一是把需要的信息也纳入系统中,比如上文中的请求A从系统中获取时间戳A,请求B从系统中获取时间戳B,那么系统会保证时间戳B一定晚于时间戳A。另一种办法是构造更强的时钟条件: + +* Strong Clock Condition: 如果a比b先发生(包含外界信息),则C(a) < C(b) + +## Physical Clocks + +看不懂,也不想深究这一节了T_T diff --git a/_posts/2022/03/2022-03-27-immuDB.md b/_posts/2022/03/2022-03-27-immuDB.md new file mode 100644 index 0000000..42314b7 --- /dev/null +++ b/_posts/2022/03/2022-03-27-immuDB.md @@ -0,0 +1,113 @@ +--- +layout: post +title: "immudb: A Lightweight, Performant Immutable Database" +date: 2022-03-27 22:43:01 +0800 +categories: "database" +typora-root-url: ../../../ +--- + +​ *(注: 本文主要是对immudb的大概介绍,不算学术论文,了解大概意思即可)* + +## 1. Introduction and Motivation + +## 2. Background + +### 2.1 Merkle Hash Trees + +![image-20220327231121170](/assets/2022/03/immudb/immudb1.png) + +### 2.2 Inclusion Proofs + +​ 证明某个节点是在MHT中,需要提供一个路径,路径上的信息只是哈希值,保护了数据本身的隐私。 + +![image-20220327231210544](/assets/2022/03/immudb/immudb2.png) + +### 2.3 Mutable Merkle Hash Trees + +​ MHT本身是为了对数据签名而使用的,但有人设计出来可修改的MHT,这种MHT是append only,只能增加新的数据。假设现在要新增一个叶子节点的数据,一种办法是将该新数据放在原先的叶子层,并且设置空的sibling,然后从下而上计算中间节点的值,最后得到新的根节点。另外一种是不从底开始,而是尽量向根节点靠拢,不产生新的中间节点,比如直接和原来的根节点配对,然后生成新的根节点。 + +![image-20220327231300369](/assets/2022/03/immudb/immudb3.png) + +### 2.4 Consistency Proofs + +​ MHT被修改后,节点的证明路径必然也要变化,但核心思想不变,仍然是给定一些必要的中间结果值,重新计算出新根节点的值。 + +![image-20220327231339000](/assets/2022/03/immudb/immudb4.png) + +### 2.5 Root Signing + +​ 在MHT体系中,client可以向server提交数据以存储,然后抽查MHT。如果抽查不过,client可以指控server的错误行为,但问题是client不能证明到底是server错了还是自己算错了。为此,可以对MHT的根节点做签名并且广而告之。这样,client可以证明是server错了*(注: 这里不是很明白为什么对根节点做签名并广播签名就可以解决问题,可能要看下mutable merkle tree的原文)* + +### 2.6 Security of Mutable Merkle Trees + + 主要提供了3个安全特性 + +* 根节点代表了整棵树的数据,能防止篡改 +* 树是append only,尝试删除会导致树发生变化,一致性将被破坏 +* 不需要验证存储的真实数据,只要验证数据还在树中就好 + +### 2.7 Asymptotic Performance of Mutable Merkle Trees +* Space: N +* Depth: logN +* Insertion Time: logN +* Lookup Time: logN +* Proof Time: logN + +## 3. immudb + +![image-20220327234518174](/assets/2022/03/immudb/immudb5.png) + +immudb是mutable MHT的扩展 + +### 3.1 The immudb Server Process + +​ immudb server进程管理整个MHT和相关的数据。server进程有一个warden线程持续重算MHT的值以保持一致性,同时也会生成证明和做root signing。Server通过grpc暴露API给client和auditor。Server会连接存储层 + +### 3.2 The immudb Storage Layer + +​ immudb的存储层包含一个或多个application database和一个sysdb database,还有MHT存储server自己的元数据。每个database包含不同的数据$x_i$(这里的$x_i$实际就是一个(k, v)),以及一个MHT。 + +​ immudb存储层是按key-value存储设计,MHT中哈希的是kv-pair,而不仅仅是value,存储层维护一些索引辅助存取数据: + +* insertion order index $i \rarr x_i$ 维护了按插入顺序的随机访问,同时也被server生成证明和corruption warden使用,提升查询数据从O(logN)到O(1) +* key index $k \rarr \{v_x, v_y,...\}$ 维护了key k的当前值和历史值。immudb是append only,想修改value只能为key插入新的value。Clients可能获取最新值或者一系列历史值。 +* MHT node index $i, j \rarr I_{i,j}$ + +​ immudb目前存储层用的是Badger + +### 3.3 The immudb API + +* ```get(Key k)``` Returns the most recent key-value pair (k, v) associated with key k. +* ```safeGet(Key k, int i)``` As above, but also demands inclusion proof of (k, v) and consistency proof between the latest MHT known to the server and the tree as it appeared after the insertion of $x_i$ . +* ```getByIndex(int i)``` Returns the key-value pair (k, v) inserted $i$th +* ```set(Key k, Value v)``` +* ```safeSet(Key k, Value v, int i)``` +* ```consistencyProof(int i)``` Returns the consistency proof between the version of the MHT immediately following the insertion of $x_i$ and the current tree +* ```inclusionProof(int i)``` Returns the inclusion proof for the element $x_i$ + + + +![image-20220328135655025](/assets/2022/03/immudb/immudb6.png) + +​ 每个auditor和client需要自己维护最新的MHT状态的拷贝 + +### 3.4 immudb Defense in Depth + +​ 上文中主要讲了MHT防篡改,但其他安全实践也是需要的,比如访问控制、网络隔离等。强烈推荐将immudb部署在隔离的机器中,让client只能通过grcp API访问。 + +## 4 Applied immudb + +​ 朝鲜赞助的APT38 threat group通过SWIFT从不同银行盗取了11亿美元,大概办法是入侵后执行了转账SQL。如果部署了immudb,则可以检测出数据不一致。 + +## 5 Future Work + +* Drivers +* SQL-Like Querying +* Improved Storage Engine +* Caching +* High Availability and Sharding +* External Security Keys +* Encryption at Rest +* Gossip Protocol +* GPU Acceleration +* GDPR and CCPA Compliance diff --git a/_posts/2022/03/2022-03-28-Kademlia-DHT.md b/_posts/2022/03/2022-03-28-Kademlia-DHT.md new file mode 100644 index 0000000..9befd21 --- /dev/null +++ b/_posts/2022/03/2022-03-28-Kademlia-DHT.md @@ -0,0 +1,90 @@ +--- +layout: post +title: "Kademlia: A Peer-to-Peer Information System Basedonthe XOR Metric" +date: 2022-03-28 22:43:01 +0800 +tag: "distributed system" +typora-root-url:../../../ +--- + + 推荐读paper前先看[这个](https://codethechange.stanford.edu/guides/guide_kademlia.html),非常好地解释了XOR的来源 + + We describe a peer-to-peer distributed hash table with provable consistency and performance in a fault-prone environment. + +## 1 Introduction + +​ Kademlia是p2p分布式哈希表(DHT). Kademlia有一些之前其他DHT所不具有的特性。Kademlia减少了节点为了发现彼此而必须发送的配置信息,配置信息在查询key时自动作为side-effect发送。节点可以通过低延迟路径掌握足够的信息和灵活性(flexibility)去查询路由。Kademlia使用并行,异步查询避免宕机节点造成大的延迟。节点记录其他节点存在的算法可以抵御基础的denial of service attacks. + +​ Kademlia的key是opaque 160-bit quantities(比如SHA-1 哈希值)。参与系统的每个计算机有一个160-bit的节点ID。存储在节点里,这些节点的ID和key按某种概念来说是相近的。最终,一个 node-ID-based 路由算法能让任何人高效地定位到指定key的邻近服务器。 + +​ Kademlia的许多好处都来源于其对XOR的使用,用XOR度量key空间两个点的距离。 + +​ *(注: 然后和其他DHT做了一些比较,不太看得懂,略过)* + +## 2 System Description + +​ 我们的系统采取和其他DHT一样的方法,为每个节点分配160-bit ID并且提供一种算法使要查找的id越来越近,差不多达到logN的复杂度。 + +![image-20220328164628872](/assets/2022/03/kademlia/kademlia1.png) + +​ Kademlia将节点看作二叉树的叶子节点,每个节点的位置由ID的shortest unique prefix决定。然后将整颗二叉树分成若干个子树,这些子树都不包含给定的节点。比如图一中0011是给定节点,那么子树有1,01,000和0010。其实就是0011到根节点路径上所有中间节点的sibling组成的子树 + +![image-20220328165144619](/assets/2022/03/kademlia/kademlia2.png) + +​ Kademlia协议还保证每个节点知道各种子树的至少的一个节点,有了这个保证后,任何节点可以通过ID定位其他节点。图二显示了查找的过程,具体过程解释在下文 + +### 2.1 XOR Metric + +​ 每个Kademlia节点有160-bit 节点ID。节点ID目前只是随机的160-bit标识。节点发送的每条消息都包含节点ID,可以让接收节点知道发送节点的存在。 + +​ 中的key也是160-bit标识符。为了给节点赋值对,Kademlia依赖一种关于标识符距离的概念。给定两个160-bit标识符x和y,定义其距离就是x XOR y,也就是$d(x, y) = x \oplus y$。XOR作为距离的度量具有以下一些性质: + +* $d(x, x) = 0$ +* $d(x,y) = d(y,x)$ +* $d(x,y) + d(y,z) \ge d(x,z)$ + +​ *(注: XOR度量距离本质上就是找最长公共前缀)* + +### 2.2 Node State + +​ Kademlia节点存储和其他节点的沟通信息以便于路由查询信息。对每个$0 \le i < 160$,每个节点维护一个列表,列表中的节点距离自己$2^i$到$2^{i+1}$之间*(注: 其实就是上文说的子树)*。这些list被称为k-buckets。每个k-bucket按照节点最后被看见的时间排序,最近最少被看见的在列表头,最近被看到的在尾。对一些值较小的$i$,k-bucket往往是空的。对值较大的$i$,list可以增长到k值,k是system-wide replication parameter。k的选择是给定任意k个节点,几乎不可能在一小时内宕机,例如k = 20 + +![image-20220329104130348](/assets/2022/03/kademlia/kademlia3.png) + +​ 当Kademlia节点收到新的消息(无所谓是请求或者回复)时,会更新发送方节点ID所在的k-bucket。如果节点ID已经在列表里了,就把ID移动到列表尾; 如果节点ID不在列表中且列表中ID不到k个,就把新的节点ID插入到列表尾; 如果列表已经满了,则尝试和列表头的最少访问节点通信,如果通信不成功就将其踢出列表,然后把新ID放在列表尾,如果通信成功,就把其从列表头移到尾,然后丢弃新收到的节点ID。 + +​ k-buckets高效地实现了类似LRU的更新策略,但有个特点是alive的节点永远不会被踢出队列,首先是因为在线时间越长的节点继续在线一小时的概率更大(如图三),所以选择保留旧节点可以提高节点在线的概率。其次,可以抵御一定程度的DoS攻击。攻击者不能通过增加新节点的方式来更新节点中的正常路由列表。 + +### 2.3 Kademlia Protocol + +​ Kademlia协议包含4种RPC: + +* PING 检测节点是否在线 +* STORE 让一个节点存储 +* FIND_NODE 160-bit的Node ID作为参数,收到请求的节点需要返回列表,列表中是该节点知道的离目标节点最近的k的节 点。一般地,应答节点必须保证回复列表中有k个节点,除非应答节点自己的k-buckets中所有已知节点都还不到k个 +* FIND VALUE 和FIND_NODE差不多,返回列表,但如果应答节点之前收到同一个key的STORE请求,就直接返回value + +​ 在所有的RPC中,接收者必须echo一个160-bit随机RPC ID,可以作为一点防伪手段。PING也可以作为RPC回复中捎带的请求,这样应答节点可以获取关于发送节点更准确的网络地址等信息。 + +​ Kademlia参与者必须执行的程序中,最重要的是定位离给定节点ID最近的k个节点,这个过程也叫node lookup。Kademlia采用一种递归算法。Lookup发起者首先从自己的k-buckets中选取$\alpha$个最近的节点。然后发起者向被选中的节点发送并行,异步的FIND_NODE RPC。$\alpha$是system-wide 并发系数,比如3. + +​ 在递归步骤中,发起者会从最开始的$\alpha$个节点中收到它们各自的k个最近节点,也就最多有$\alpha \times k $个新的节点信息,这些新节点信息会和发起者最初自己的k个节点再一起排序,然后访问还没有访问的前k个节点。重复这个步骤,直到前k个节点全部被访问过,距离最小的节点就是目标节点。 + +​ 大多数操作都按上述lookup过程实现。要存储一个时,参与者首先找到离key最近的k个节点,然后向它们发送STORE请求。此外,每个节点需要re-publishes 的到期时间可以由应用自身特点而定。 + +​ 查找时,节点首先查找离key最近的k个节点。但是,查询value还是用FIND_VALUE而不是FIND_NODE,因为找到value后可以理解结束查找。此外,为了缓存结果,当一次查找value成功后,将保存在离key最近但没有返回value的节点中。 + +​ 因为整个系统拓扑是无方向的,未来对同一个key的请求很可能hit cache而不需要查询最近的节点。但系统也可能将key缓存的太多。为了避免over-caching,we make the expiration time of a pair in any node’s database exponentially inversely proportional to the number of nodes between the current node and the node whose ID is closest to the key ID。 + +​ buckets一般来说可以通过请求来更新,但有时候一个范围内的节点ID在过去一小时都没有任何查询,那么这时需要refresh操作,即随机选择一个该范围的ID,然后查询,相当于模拟真实请求。 + +​ 新节点a加入网络时,必须已知一个已经存在的节点b。a把b插入合适的k-bucket中,a然后执行node lookup,查找节点是它自己。这样a可以更新自己的k-buckets,也可以让其他节点知道自己的存在。 + +### 2.4 Routing Table + +​ routing table本身的结构是很直白的,但有一些subtlety处理很不平衡的树。routing table是一颗二叉树,叶子节点是k-buckets。每个k-bucket包含的节点都有一些公共前缀。这个前缀就是k-bucket在二叉树中的位置。因此每个k-bucket包含一些范围内的ID,所有k-bucket组成160-bit空间且互相没有重叠。 + +​ ![image-20220330151237014](/assets/2022/03/kademlia/kademlia4.png) + +​ routing tree中的节点都是动态申请的。如图四,节点最开始只有1个节点,也就只有1个k-bucket,cover了整个id空间。当节点遇到新的节点后,尝试将新节点插入到合适的k-bucket中。如果bucket还没满,就插入到bucket中;如果满了,且bucket的范围包含节点自己,那么就将bucket分为两个buckets,再重新尝试插入;如果满的bucket不包含自己,那么就直接丢弃新的节点信息。 + +![image-20220330152457594](/assets/2022/03/kademlia/kademlia5.png)