6.824 分布式系统 lab随想QA 重点梳理

如果有三台服务器,请描述一个leader服务器断网后会发生什么,并说明为什么committed的日志不会丢失。

另外两台服务器会选出其中一台作为新leader。由于committed的log必然已被复制到2~3台服务器上,因此新leader必然会包含所有committed的log;又因为leader都是append-only的,就保证了committed的log永远不会消失且永远会在leader里面。

为什么新leader不能commit以前的term的日志?

此问题实际上等价于:term x的leader上任的时候,发现自己日志的最后一项的term是term x-2甚至更小(如果是x-1的话似乎也许没问题),并且在一次心跳后发现该日志已被复制到过半的服务器,能否将其标记为commited?

答案是不能,其实也是论文的Figure 8讨论的问题。即使term x的leader发现最后一项是term x-2且被复制过半,也无法保证该日志不被删除,因为如果leader没有commit任何日志就宕机,则下次投票的时候,某个带有term x-1的log的服务器会成为新leader,然后覆盖掉那些term x-2的日志(如图d)。

想要commit以前的term的日志,则必须如图e所示,新leader添加了个log并commit,就可以连带commit前面的日志。

Figure 8

为什么follower收到日志的时候,不能直接把本地的preIndex之后的日志全都删掉,然后直接append新日志;而是检测到日志冲突才删日志?

是为了防止网络不可靠导致的乱序、堆积到达。

假如Follower1的本地日志是[0,1,1],Leader本地的日志是[0,1,1,2,2,3] commitIndex=2

  1. Leader向Follower1发送AppendEntries [2,2,3] preIndex=2 commitIndex=2,但是网络太慢,此包暂未到达。
  2. 然后,leader更新commitIndex为5(在其他服务器上复制过半),并且收到了新的一个log,此时leader本地的log为[0,1,1,2,2,3,3],于是向Follower1发送AppendEntries [2,2,3,3] preIndex=2 commitIndex=5。Follower1成功接收到了这个包,于是本地更新为[0,1,1,2,2,3,3] commitIndex=5
  3. Leader更新commitIndex=6,并再次发送心跳,Follower1成功接收到了,变成[0,1,1,2,2,3,3] commitIndex=6
  4. 然后,Follower终于收到了第一个被延迟的网络包。如果直接删掉preIndex后面的所有日志并直接append的话,就会变成[0,1,1,2,2,3] commitIndex=6,使得index=6的committed log被删掉。如果此时leader宕机,进行选举之后,Follower1有可能被选为leader,于是index=6的committed log被永久地丢弃了,这是不合法的。

第2、3步可以改成:Leader利用其他服务器将commitIndex更新到6,然后Follower才接收到AppendEntries [2,2,3,3] preIndex=2 commitIndex=6,从而一次性地变成[0,1,1,2,2,3,3] commitIndex=6

说说persist了哪些变量,分别叙述持久化它们的原因。

votedFor:防止一个服务器在一个任期内宕机又重启导致投了两次票,触发脑裂。

log:防止committedIndex丢失。

currTerm:假设有三台服务器,因为某些原因进行了多轮选举,最终在term 3选出一个为leader,但leader瞬间就联系不到另外两台服务器了。然后往该leader上放非常多的log,都是term3,但都无法commit。剩下两台服务器还没接收到log就直接宕机,忘记了自己的term,同时log列表也是空的,于是只能认为自己在term 1,并选举出一个leader。由于有两台服务器,所以这个leader成功commit了一些term 1的log。突然,所有服务器恢复联系,由于term不一致,连在一起的两台服务器中的leader马上下台,孤单一人的leader用自己的日志完全覆盖剩下两台服务器的日志,这显然不合法了。

logOffset:记录Snapshot里面有多少个日志,以保证日志的逻辑索引值正确。本质上应该是Snapshot的一部分,但由于是raft层的数据所以只能存储在raft state中。

为什么要clientId+requestId去标识一个请求,而不能只用clientId?同样的,为什么不能直接用uuid标识?

只用clientId: 假设client发送了PUT x 1,收到了apply,然后发送PUT x 2。但是此时有些服务器尚未apply PUT x 1,以至于PUT x 2在等待回复时会看到对PUT x 1的回复,如果只用clientId的话,PUT x 2会误以为这是对自己的答复,误以为自己apply了,但事实上PUT x 2的日志被删除了。此时若client接着GET x,发现回复是1,就不满足线性一致性了。

用uuid:server需要记录哪些操作已经被执行了,以防止重复执行。如果使用uuid的话,要记录每个请求是否被完成的一张表,会非常大无法压缩。如果使用clientId+requestId且requestId单调递增的话,只要对每个client记录一个requestId就行了。

说说snapshot里面存储了哪些变量,分别叙述存储它们的原因。

Database:Snapshot的本质数据,此处的database加上raft层剩下的log就组成了完整的数据。

LastAppliedIndex:主要是为了防止应用Snapshot之后,剔除掉applyCh中传来的不合时宜的log和Snapshot,具体见如何保证snapshot的过程中不会有commit丢失

LastRequestDoneOfClients:Snapshot之后,log就不能自动更新LastRequestDoneOfClients了,需要Snapshot来说明被压缩的日志apply了哪些请求。

Config: 保证config不丢失,至少configNum不能丢;用于记录push的目标。

PreConfig: 这个疑似真不需要Snapshot,甚至该变量就是没必要的,尚未实践。

ShardState: 用于记录shard状态,Snapshot前未完成的push继续完成,未gc的继续gc,未pull的继续等待。

如何保证snapshot的过程中不会有commit丢失?

保证有序性,在server层维护LastAppliedIndex,保证applyCh传上来的log的index必须等于kv.LastAppliedIndex+1。当传来Snapshot的时候,Snapshot.LastAppliedIndex不能小于kv.LastAppliedIndex;如果Snapshot合法,则更新kv.LastAppliedIndex=Snapshot.LastAppliedIndex。

即:保证LastAppliedIndex要么由于一个一个按序的log来一个一个地增加,要么由更先进的Snapshot来增加一定数量;禁止减少,禁止被log增加超过1。

为什么config必须一个一个按序进行不能跳过?

config的很多想法都不得不面临一个问题:如果一个push无人接收怎么办?如果一个pull无人理会怎么办?这个问题直接提升了不按序的实现的复杂性。

另外,如果一个服务器重启,它可能会携带出旧config、旧shard,无人可以保证它们被乱迁移会不会出问题。

最佳的解法就是一个一个配置config,把配置看成group协作的过程,如果一个group push不到或pull不来就会等待,由于config不会被跳过,因此必然会有group 来接应。更多益处见叙述一下当一个服务器宕机重启后,它是如何进行config的。顺便说出所有gc的时机

叙述一下当一个服务器宕机重启后,它是如何进行config的。顺便说出所有gc的时机。

从Snapshot里面发现目前的config,并查看state,如果全为online或offline,则开始查找下一个config;如果不是:

  • push:如果push的目标group的configNum大于自己的configNum,则说明目标group已经在以前接收过自己的数据了,于是直接将此push标记为成功,转为gc。
  • pull:由于pull是被动的,如果它config落后了也没人告诉它。解决方法是,pull成功接收到数据之后,要将其存入到raft的log中,被apply后才回复“pull成功”。这可以:1.让所有Follower同步进行shard的添加等数据操作;2.服务器重启后,一直在等待pull,如果此时log中apply了一个添加shard的log,且configNum和shard编号都一致,则直接判定为pull成功;3.将具体数据存到raft层,保证不丢失,使得push方在push成功后可以放心地gc。
  • gc:则继续向日志中添加gc log。apply了gc日志,只要configNum和shard号对上,则不管是以前的log还是新加的log,都算gc成功,转为offline。

由于重启后,可能会去查找过期的config,而日志中可能已经有非常多的新config,因此无法保证log中configNum单调增,但可以进行筛选,每次只能取configNum比目前的config大1的config。当然,理论上不可能出现比当前config大2的config log。

由于config的完成是需要一定时间来检测的,且与apply协程完全异步,因此可能当前config还没被标记完成,就apply了下一个config了。此时必然可以保证,老的config是被配置成功了的(重启的leader会触发,而所有Follower会不断触发,所以从Follower角度出发更容易想出这段逻辑)。此时将pull标记为online是无害的,但push和gc标记为offline的同时都必须进行一次gc操作,但不需要Start gc log,因为所有服务器都会进行这个apply,只要暴力地删除本地的对应shard就算完成了。

gc时机

  1. gc态会Start gc log,当log apply的时候进行删除就行了;
  2. 提前接收到新log的时候,直接对本地进行删除即可。

描述在一次shard转移时双方的状态变化,并说明特殊情况下如何保证正常工作

若config 3完成后收到config 4,一个shard需要从源迁移到目的,则两者中该shard对应的状态:

阶段源/configNum: 状态目的/configNum: 状态可处理请求的group数
初始3: online3: offline1
启动4: push 或 3: online3: offline 或 4: pull0 或 1
开始4: push4: pull0
抵达4: push4: online1
确认4: gc4: online1
删除4: offline4: online1
  1. 初始时,源和目的都接收到新config后迁移才能开始。
  2. 源主动向目的发迁移请求,目的接受到后,告知raft层,对shard进行install,完毕后目的变成online,开始接收此shard的请求。然后,RPC返回。
  3. 源接收到返回的RPC,得知目的已经安装成功,说明迁移完成,则源进入gc状态。
  4. 源告知raft层进行shard删除,删除完毕后变成offline状态。

特殊情况:

  1. 从gid 0去pull和向gid 0去push时要特判,因为此时pull永远等不到,而push也无法找到真的gid 0的group。
  2. 若源端迟一点得知新config,则目的不会做任何事,保持pull等待接收;若目的端迟一点得知新config,则会拒绝新config的push。只有configNum完全对上的时候才能进行迁移,因此各种重启也不会有事。
  3. 若目的install成功,但源并没有收到RPC回复(或者源宕机),则目的可以继续正常运行;而源会重新进行push,此时目的会直接回复成功,使得源也正常工作。源未收到确认时一直保持不可服务状态。

lab4中,所有client请求的操作是如何保证总是可以完成、不重复、apply后不丢失的?

要确保在每个瞬间,对任意一个请求,只有零个或一个group能处理它(不丢失);client可以追踪到可处理的group(总是可完成);一个shard在转移数据时也转移相关的所有信息,包括防重复信息(不重复)。

总是可以完成

对一个group访问失败后,会重新查询config,因此总是保证config不长时间落后。

不重复

重复的情况:如果client请求发给了group 1,发生了超时,回头发现config变化,于是把请求发给group 2。group 1将此请求commit了,然后进行了一次数据过大造成的Snapshot,完成后发现了新config,于是把请求所在的shard给push到了group 2。group 2 pull完成后,发现了client的这个请求,由于这个请求之前的log已经被压缩到了Snapshot中,导致group 2并不知道这个请求被执行过了,于是又commit并且apply了这个log。于是,请求被重复执行。

解决方法:在push的时候带上LastRequestDoneOfClients(clientId->DoneRequestId),而接收方则把DoneRequestId往大了取。因为一个client的所有请求都是串行执行的,因此即使它的10个请求访问了10个group,对于所有group来说,requestId总是单调增的。于是,group 1说client1已经执行到了requestId 10,而group 2发现本地的client1才执行到requestId 9,就会将其更新为10,,并拒绝那个requestId为10的重复的请求。

apply后不丢失

丢失的情况:如果在config 4的时候,接收了一个请求并apply;config 5的时候要将其push,发现接收方的config为10,于是直接判定push成功,shard直接被丢掉,已经apply的请求也被丢掉了。

首先给出一个重要性质:对于每个configNum,任何一个shard只存在于一个group。 因此,每个请求也应当标明configNum,如果Start时和apply时(apply时的检测才重要,Start时的检测仅仅是为了加速)configNum对不上,则判定为请求失败(即禁止修改不同configNum的shard)。失败的话server不会更新LastRequestDoneOfClients,以让该请求不会被判重复,可以再次完成。

然后再考虑两种情况:

  1. 如果大部分group都很新,其中一个group更换了leader(或者全部重启),其config暂时较老;刚好client也没更新config,两者configNum相同,于是向该group发起请求。但是,即使更换了leader,那些config的更新、apply、push、gc都记录在了log当中新来的请求会排在这些log后面,因此轮到该请求的时候,config和状态已经很新了,则拒绝执行
  2. 如果大部分group都很新,其中一个group整个宕机了好久,重新启动的时候尚未获取到新的config,日志空空如也,某个shard为online;刚好client也比较卡,没更新config,两者configNum相同,于是向该group发起请求。但是,此时必然有其他group在等着从它那里pull group;或者说,请求对应的shard在所有更加新的config中应在的group都会被迫在某个config上停下,等待当前group恢复以将shard传球一样传出来。如果其他group已经接到过shard了,则当前group的状态必然是下一个configNum的push或gc或offline,因此此时对请求进行apply是无害的,或者说此group确实是唯一合法能接收此请求的group。apply后的shard会被安全地抛给新的config的group中去。

其他情况举例:group 1得知config 12后,将一个分片发给group 1,group 1 install成功但group 2宕机了。许久之后,group 2重启,一个很卡的client以config 12访问group 2,若put成功,就有可能造成操作丢失。解答:此时group 1的此分片还处于push状态,无法进行服务。

思考

真的需要server和client的configNum完全相同的时候才能接收请求吗? apply后不丢失中的两种情况以及描述在一次shard转移时双方的状态变化,并说明特殊情况下如何保证正常工作 似乎可以说明,在一个请求被commit时,只要当前server中的该请求对应的shard处于online状态,那么该server所在的group就是所有group里唯一一个可以合法接收请求的,因此此时执行该请求应该是没有任何问题的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值