[10][lecture] Lecture 6: Raft

Posted WhateverYoung

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[10][lecture] Lecture 6: Raft相关的知识,希望对你有一定的参考价值。

6.824 2018 Lecture 6: Raft (2)

KVservice

key/value service as the example, as in Lab 3
  goal: 集群服务对客户端表现和单机一致
  goal: 少数节点失效依旧可用
  watch out for network partition and split brain!
  系统层级 clients - k/v layer - k/v table - raft layer - raft log]
  raft接口:client RPC -> Start() -> majority commit protocol -> applyCh

提示:
  leader commits/executes 当集群中多数节点AppendEntries成功返回
  leader 提交后通知集群提交和执行 (== send on applyCh)
  为何不等所有节点回复?因为少数节点失效,仍要具备可用性
  多数票方案为何有用?任何两个多数票选方案都一定存在重叠节点,保障了下一个leader可以看到所有commit过的log

lab 2B raft log

  • 主节点存在,从节点就会和主节点交互,不会和其他从节点交互
  • 如何保证切主过程前后client不会出现异常?比如读到老数据,重复的op,丢失的op,乱序的op等等
  • 我们需要保证的是,当一个节点都按照log entry来执行了某个命令,其他节点必须达成共识也在这个log index执行这条命令;方法是从节点会按照主节点来同步log,因此可能会删除其log,选举和同步共同保证了commit的log不会被覆盖(raft保证了新主节点,一定包括所有commit的log),因此选主的时候要限制去选有最新log的节点(要求是,先比较最新的log term高优先主;term一致时长的优先主),候选人只会给满足 up to date log的节点投票
  • 如何快速回退追随者的log,文中给出的默认方式每次回退一条;供参考的实现方式:在拒绝的请求中返回,互斥entry的term,第一次出现这个term的log index,如果主节点有这个term,nextIndex配置为回退到主节点该term的最新entry;如果没有nextIndex配置为追随者第一次出现这个term的log index

lab 2C persistence

节点crash后如何恢复?
raft允许少数节点crash后依旧可用,但是crash的节点要修复后加入集群,保证复制因子
两种策略:
第一,使用一个新的server加入,需要将全量log或者快照发送到该节点
第二,修复crash节点,重新加入,从持久化log中读取到log,然后慢慢从主节点catch up到最新的log

讨论哪些状态需要做持久化?
论文中给出,log[],votefor,currentTerm
因此上述状态的每次变更都需要持久化落盘操作
log[],需要记住log entry,来保证多数复制的log entry在选举中可以获胜,从而达到新leader必须有所有commit log的要求
voteFor,防止选举阶段发生两次投票事件造成双主
currentTerm,保障term单调递增,检测rpc消息中term是否是旧的

哪些状态不需要落盘?
commitIndex,lastApplied,next/matchIndex[],可以在重启后动态重建上述变量

持久化性能有影响?
SSD 0.1ms HD 10ms,因此持久化限制我们吞吐在 100-10000ops
RPC耗时,<<1ms
可以使用batch方式优化磁盘性能

service如何从crash中恢复?
最简单的方案,从初始态开始,重放持久化的raft日志,但是lastApplied没有持久化,因此需要额外的工作,同时回放所有日志对于长期运行的系统会很慢
使用snapshot技术,只回放近期的日志

log compaction and Snapshots (Lab 3B)

  • raft log会慢慢变大,远大于state machine的状态本身,每次节点重启回放的时间非常用,影响了可用性
  • 幸运的是,节点不需要又存储raft log又存储state machine,两者是冗余的,执行过的log已经在state machine中体现,client只看到state,看不到log,state通常不大,所以思路就是仅存储state,通过raft log entry来存储全量的state,作为snapshot,提交后就可以安全的删除snapshot覆盖的log entry
  • 考虑哪些log不能删除?第一,un-commit log,可能已经是复制集中的多数,还没收到主的提交消息;第二,un-excuted log,还没反应到state;还有虽然已经执行过的其他节点还需要同步的log
  • service周期性持久化快照:service with state, snapshot on disk, raft log, raft persistent,将全量的state快照作为一条特殊的command,因此也就是一条特殊的log entry。首先service将快照持久化到磁盘,然后通过一条特殊command即特殊的log entry通知raft快照,raft就这一条log enty达成共识,随后raft可以安全的清理快照覆盖的log,server可以随时做快照清理raft log
  • 因此持久化部分,包括service’s snapshot,raft logs,两者联合等同于全量的log
  • 节点crash恢复,首先读snapshot,然后从raft获得提交但是还没快照的log entry
  • 如果追随者需要同步的日志主节点已经快照了清理了怎么办,使用InstallSnapshot rpc,(TODO 为何不保证只清理所有节点都同步的log)
  • 主节点InstallSnapshot rpc参数,term,lastIncludedIndex,lastIncludedTerm,snapshot data
  • 从节点反应,term低于自己拒绝;如果lastIncludedIndex自己已经有了拒绝;清理log,set lastApplied to lastIncludedIndex,对状态机执行snapshot替代持久化状态
  • log和快照是统一的,主节点可以决定如何同步状态,对于新的节点,使用快照快一些;对于只落后一些的节点,使用log同步即可

configuration change not in lab

  • 所谓config就是指集群中的节点配置
  • 场景:需要增加节点,移除节点,或者增加复制集因子来提高可用性等等
  • 触发依赖人为操作,raft需要自动处理不中断服务
  • 注意每个节点都有整个集群的配置信息,如果在raft之外通知每个节点新的配置,问题就是每个节点改变配置的时间是不一致的,就有可能造成集群双主,例子如下
 we get as far as telling S1 and S4 that the new config is 1,2,4
    S1: 1,2,3  1,2,4
    S2: 1,2,3  1,2,3
    S3: 1,2,3  1,2,3
    S4:        1,2,4
  OOPS! now *two* leaders could be elected!
    S2 and S3 could elect S2
    S1 and S4 could elect S1
  • 因此集群改变配置要依赖raft达成共识,采用一个过渡阶段来解决这个问题,集群一开始配置是cold,然后人为通知主节点切换配置到cnew,此时raft会记下一条log entry,每个节点默认使用其log中最后的config,leader首先commit cold-new,此时无论是old还是new都会有多数节点完成commit,然后主节点在commit cnew
  • 主节点crash在上述不同阶段?是否会有双主产生?不会,一般来说,主节点需要满足以下条件之一:A:old 节点,还没复制到cold new;B,old-new节点,已经复制到cold-new;C,new节点,已经复制到cnew;A+A和C+C不可能,因为互斥原则;A+B,B同时需要old的多数票和new的多数票,和A互斥;A+C,除非coldnew已经提交,不会到C阶段,如果coldnew已经提交,就不会出现A;B+B,互斥;B+C,B需要两者的多数票,和C互斥

performance

  • 一般来说,不是所有服务都需要性能特别高,比如gfs或mapreduce可以不那么高;kv service需要高性能
  • 大多数一致性算法,复杂度基本一致,每次共识达成都需要一次rpc通信和一次落盘
  • raft为了简单性损失了部分性能:第一,追随者会拒绝乱序的log,直到找到合适的log才接收(优化可以是空出一些hole然后写入指定位置,当网络乱序很多的场景);第二,appendEntry只能逐条进行,可以做batch或者流水线去优化;第三,快照技术对于很大的全量状态可用性很差;第四,leader如果很慢,整个系统就会很慢
  • 对性能影响很大的部分:磁盘写入性能;网络消息性能;所有log command是串行的,无法利用多核;read-only操作可以快速执行
  • 更多的优化参见:zookeeper,etcd3,paxos made live;harp

raft FAQ

Q: 何时主节点需要在读请求中commit no-op?

A: 两种方案保护client不会读到老的数据,leader刚刚选举完成时,不能确定commit位置,需要额外的操作,一种是commit no-op保障;另一种是租约机制。文中说读不会涉及任何write,只有一种场景除外,就是leader刚刚选举后的第一次read操作。

Q: commit no op保护的是什么?

A: 借助figure 8来阐述,依旧是c中的S1无法确定2是否已经commit,因此就无法知道返回给客户端的读是否需要包括2,所以它需要做一次commit,空的commit刚好适合这种场景,一旦commit一次后,2就会保证commit,后续的leader都会有2,否则会出现figure 8中的s5情况,2其实没有commit

Q: 租约机制如何实现,为何需要依赖bounded clock skew?

A: 有一种实现可以是,收到leader的心跳消息,保证接下来的100ms内不会产生选举,所以就不会出现figure 8中的情况,主节点可以安全的服务读请求100ms,然后每次心跳会续租。这需要每个节点对100ms的定义是一致的

Q: Cold和Cnew表示什么?是leader吗?

A: 表示的是集群的配置,主要是包含哪些节点,paper中没有提供更多的细节。可能就是ip地址加端口号,或者一个hostname

Q: 如何理解cold,cnew以及过度状态?

A: Cold_new的含义是,这个期间,多数票选需要保证节点both old and new。 Suppose C_old=S1,S2,S3 and C_new=S4,S5,S6, S1为leader,最后阶段S1 commit cnew,但是它仍是leader,所以需要step down,让位给cnew中的节点做主节点

Q: 为什么cold需要关闭自己,否则就会导致选举?

A: 主要问题是cold节点没有commit cnew配置,所以这些节点没有获得集群变更给cnew的信息

Q: 切换配置时选主需要old new同时获得多数票选,会影响性能多少?

A: 基本不会,基于修改配置并不频繁,old new同时多数票选带来的负荷也不大,当集群中节点相对稳定的时候

Q: 新成员加入有一个过程没有投票权,这个过程如何提高了集群可用性?

A: 该改进主要是为了新的节点可以快速得到全量的log,防止其加入后无法commit log,因为一直在做catch up操作,导致集群不可用。如果节点没有cathup,那么coldnew就无法commit,直到它catch up并且提交了cold new才会继续执行client command,因为log是按顺序提交的,新的log就要等coldnew提交完成,因此该阶段可以减少集群停留在coldnew的时间,使得配置变更更迅速

Q: 没有投票权的server状态是否仅存在于配置变更中?还是说无论何时,只有catch up的server才能投票?如何定义catch up?

A: 论文中相关细节不多,可以猜测一下,情况应该是leader开启一次集群配置变更操作,应该是在所有新加入的节点都已经catchup后,当leader开始coldnew时,这些server就都已经catch up了,因为只有多数节点完成coldnew log的复制,才会commit coldnew,当cold newcommit时,集群中节点就具备投票权。

Q: 论文中采用一种方式抑制新一轮的选举,为了防止old集群节点timeout触发新的选举,为什么不通过消息的来源来判断发送消息的节点是否在集群中,这样就可以判断是否可以接收这条消息了?

A: 这种方法也可行,不过考虑一下,有一种情况是,在old new状态下,某个new的节点成为leader,此时少数old节点还没开始变更,它们仅知道old配置节点的存在,我们不希望这些节点忽略来自新leader的选举(不过忽略也没问题,因为多数节点已经coldnew了,否则不可能new节点成为主)

Q: 变更的起始点和终止点?

A: 当前leader察觉到coldnew log entry时开始,如果在cold new没有commit时切主,另一个主没有cold new,那么本次变更就提前结束了,变更失败但是一致性仍在是对的;继续会commit coldnew,commit cnew后,变更结束

Q: 如果state本身和log一样都很大,比如都是唯一的kv插入,快照还有用吗?

A: 取决于具体情况,对于kv而言,快照可以具备和log不同的结构,比如更快速的load到内存,而不是一条一条回放日志,因此依旧具备实用性,一般来说,log远比state大。

Q: 快照rpc会很消耗带宽资源吗?

A: 会的,优化起来并不容易,因为如果要增量复制,对state要求比较高,对哪些log可以安全删除要求也很高。

Q: 写快照耗时太长,会不会导致选举发生?

A: 有可能发生,快照Tb级别,会耗时s级别甚至更长,此时解决方案是后台写快照,可以采用copy-on-write方案,子进程写快照,或者保证快照每次写小于超时时间,然后分多次写也可以。

Q: figure 13是否少了一些条件判断,比如If lastIncludedIndex < commitIndex, return
immediately. or If there is already a snapshot and lastIncludedIndex < currentSnapshot.lastIncludedIndex, return immediately.

A: 是的,要防止接收陈旧的snapshot,由于网络原因后者乱序问题,可能发生这种情况

Q: 如果接收快照时,我的快照优先于请求来的如何处理?

A: 忽略请求即可,是安全的。

Q: 实际应用中如何处理快照问题?

A: TODO 参考etcd-raft实现

Q: RPCinstallSnapshot原子性?执行一半失败了怎么办?

A: 和其他rpc一样,务必保证原子性,要么成功要么失败,不能有中间状态。同时该rpc实现还需要保证幂等性,leader可能会重复发送请求。

Q: 压缩技术可否应用在快照上?

A: 可以的,取决于存储的数据类型。如果每一次快照和前一次都有很多冗余,可以使用tree结构,就可以在不同快照版本之间共享node。

Q: 执行op和将op写入log之间有什么关系?

A: 只有leader commit的log才能执行,leader首先执行,然后追随者根据请求中的commitindex,执行本地的log,执行的含义是将log entry给上层应用,对于kv服务来说,交给kv层来执行get或者put请求

Q: 如何实现确定当前主节点时存在的,从而忽略选主的消息?

A: 每个节点随机有一个选举的超时时间,当超时时间到达还没收到主的心跳或者其他的选举请求时,发起选举。举例来说,hb 10ms一次,leader 发送心跳,10,20,30,假设s1节点在30时没收到心跳,35时timeout到达,开始出发选举,s2在30收到了心跳,因此s2会至少保证到40之前,不会发生超时选举事件,这里的假设是30-40之间,主是活着的,当然也可以更长,允许到50,也就是允许断链一次心跳,此时s2收到s1的选举,可以安全的忽略

reference

以上是关于[10][lecture] Lecture 6: Raft的主要内容,如果未能解决你的问题,请参考以下文章

lecture 10.30

lecture 10.22

CS61A 学习笔记 lecture 6 recursion

Lecture6 逻辑斯蒂回归(Logistic Regression)

[4][lecture] Lecture 3: GFS

[9][lecture] Lecture 5: Raft