MIT6.824 LAB3

167 阅读9分钟

线性一致性

是我们对于存储系统中强一致的一种标准定义。线性一致是特定的操作历史记录的特性。

如何证明线性一致性?

1.序列中的请求的顺序与实际时间匹配
2.每个读请求看到的都是序列中前一个写请求写入的值
如果将上面的规则应用之后生成了一个带环的图,那么证明请求历史记录不是线性一致的。

image.png 执行顺序: 写入0->写入2->读出2->写入1->读出1。所以可以证明这里是线性一致性的。

image.png 如果有两个客户端C1,C2同时执行两次读请求,C1先读出2再读出1,而C2先读出1再读出2.那么根据线性一致性的证明,我们执行顺序是:写入2->c1读出2->写入1->c2读出1->c1读出1->写入2,最终形成一个环,那么就不符合线性一致性的要求。

Zookeeper

通过增加更多的服务器可以使分布式系统的性能提高吗?

当我们加入更多的服务器,Leader节点是一个性能瓶颈,因为Leader需要处理每一个请求,它需要将每个请求的拷贝发送给每个其他服务器,所以,随着服务器数量的增加,性能反而会降低。

解决方法: 让所有的写请求通过Leader下发,而一般情况,读请求会远大于写请求,所以我们可以把读请求发给某一个副本。(类似于读写分离)。
问题: 我们不能保证副本中的数据是最新的。如果我们要构建一个线性一致性系统,我们并不能这么做。

Zookeeper如何使得服务器数量的增加而提高性能?

Zookeeper为追求性能放弃了线性一致性。不提供线性一致的读。

Zookeeper如何保证一致性

  1. 写请求是线性一致的
  2. 任何一个客户端的请求,都会按照客户端指定的顺序来执行,即FIFO客户端序列。即单个客户端的请求是线性一致的。
  3. sync操作,本质上是一个写请求,当我想读出Zookeeper中最新的数据,这个时候,我可以发送一个sync请求,它的效果相当于一个写请求。这个写请求会出现在所有副本的Log中,而客户端发送读请求时,需要在sync请求返回后才能返回读请求。这样,我们就可以读出最新的数据。但是这是一个代价很高的操作,因为我们现在将一个低成本的读操作转换成了一个耗费Leader时间的sync操作。

Ready file

Zookeeper中有一个Master节点维护了一个配置,描述了该系统的一些信息,如果Master要更新这个配置,而慈寿寺有大量的客户端需要读取相应的配置,配置被分割成了多个file,我们如果保证配置的原子性更新?
假设Master做了一系列写请求更新配置,Master节点会以这种顺序执行写请求
首先我们假设有一些Ready file,如果Ready file存在,那么允许读这个配置,如果Ready file不存在,那么说明配置正在更新过程中,不允许读取。
如果Master要更新配置,那么会首先删除Ready file。更新完成之后,Master会再次创建Ready file。更新过程中保证写请求期望的执行顺序。
接下来,所有的副本都会按照相同的顺序执行请求,即删除它们的Ready file,执行写请求,最后创建Ready file 所以,如果客户端看见了Ready file,那么副本接下来执行的读请求,会在Ready file重新创建的位置之后执行,即Zookepper可以保证这些读请求看到之前对于配置的全部更新。

问题:假设Master在配置更新了之后创建了Readyfile,之后又要重新配置,而这时在第二次配置更新之前,需要读取配置的客户端通过调用exist来判断Readyfile是否存在,这样客户端读取了组成配置的第一个file,但是在读取第二个file时,Master可能正在更新配置。所以客户端读取到了一个旧配置的f1和新配置的f2.

image.png
解决方法: 在客户端发送exists请求查询Ready file是否存在,实际上,客户端不仅会查询Ready file是否存在,还会建立一个针对这个Ready file 的watch。如果Ready file有任何变更,例如被删除或者被创建,副本会给客户端发送一个通知。
在这个问题中,客户端在读取f2之前,Master节点对配置有了新的更改,那么Master节点会通过watch机制通知客户端,放弃本次读取,重新读取。

znode

当我们通过RPC向Zookeeper请求数据时,我们可以直接指定/APP2/X。这里的文件和目录都被称为znodes。

znode的三种类型

  • Regular znodes,一旦创建,就永久存在,除非你删除了它
  • Ephemeral znodes,如果Zookeeper认为创建它的客户端挂了,它会删除这种类型的znodes。这种类型的znodes与客户端会话绑定在一起,所以客户端需要时不时的发送心跳给Zookeeper,防止客户端对应的ephemeral znodes被删除。
  • Sequential znodes,当你想要以特定的名字创建一个文件,Zookeeper实际上创建的文件名时你指定的文件名再加上一个数字。当有多个客户端同时创建Sequential文件时,Zookeeper会确保这里的数字不重合且递增。

Zookeeper API

  • CREATE(PATH,DATA,FLAG) PATH是文件的全路径,数据DATA,表明ZNODE类型的FLAG
  • DELETE(PATH,VERSION) PATH是文件的全路径,版本号Version。每一个znode都有一个表示当前版本的version,当znode有更新时,version也会随之增加。当且仅当znode的当前版本号与传入的version相同,才执行操作。
  • EXIST(PATH,WATCH)入参分别是文件的全路径名PATH,和一个额外参数WATCH。通过指定watch,你可以监听对应文件的变化,不论文件是否存在,你都可以设置watch为true,Zookeeper可以确保如果文件有任何变更,都会通知客户端。同时判断文件是否存在和watch文件的变化是原子操作。
  • GETDATA(PATH,WATCH)这里的watch监听的是文件的内容的变化
  • SETDATA(PATH,DATA,VERSION)如果传入了version,那么Zookeeper当且仅当文件的版本号与传入的version一致时,才会更新文件
  • LIST(PATH)返回路径下的所有文件

使用Zookeeper实现一个计数器

由于ZookeeperGET操作会读到旧数据,所以我们需要在SET操作时增加一个版本号,来确定我们读到的数据是否是最新数据。保证读操作和写操作的原子性(乐观锁思想),这里被称为mini-transaction,一个简单的原子事务。

WHILE TRUE:

X, V = GETDATA("F")

IF SETDATA("f", X + 1, V):

Time.Sleep(10ms) //防止大量重试浪费计算机资源
BREAK

使用Zookeeper实现非拓展锁

WHILE TRUEIF CREATE("f",data,ephemeral=TRUE) : RETURN
  IF EXIST("f",watch=TRUE):
     WAIT

流程: 如果创建锁文件成功,直接返回true,并且增加ephemeral防止节点宕机产生死锁问题。如果创建锁不存在则调用EXIST接口判断锁是否存在,存在则等待锁释放。
问题: 如果多个客户端并发的请求锁会发送什么?

  1. Zookeeper Leader会以某种顺序一次只执行一个请求,所以不会存在并发安全问题。
  2. 如果客户端调用CREATE返回了FALSE,在调用EXIST之前锁释放了,那么会重新开始循环,尝试获得锁
  3. 在调用EXIST的时候,锁释放了。因为EXIST请求是个只读请求,它必然会在两个写请求之间执行。即(释放锁和获得锁之间),那么这时候有两种可能:EXIST请求在DELETE之前处理,那么EXIST请求会返回TRUE,等待锁的释放。EXIST请求在DELETE之后处理,那么会重新尝试获得锁。
  4. 仍然存在大量重试的问题

使用Zookeeper实现拓展锁

通过创建Sequential文件实现来避免重试问题

CREATE("f",data,sequential=TRUE,ephemeral=TRUE)
WHILE TRUE:
    LIST("f*")
    IF NO LOWER #FILE: RETURN
    IF EXIST(NEXT LOWER #FILE, watch=TRUE):
    WAIT

过程:

  1. 创建一个Sequential文件,并返回一个递增序列号,序列号根据创建的顺序递增。
  2. 通过LIST列出所有以"f"开头的文件,即所有Sequential文件。目的是将我们的序列号与所有现存的序列号比对。
  3. 如果现存的Sequential文件的序列号都不小于我们得到的序列号,那么我们将获得锁,直接返回。
  4. 如果存在小于我们得到的序列号的文件,那么需要等待拥有更低序列号的客户端释放锁。

缺点: 无法保证原子性,即持有锁的客户端挂了,它会释放锁,另一个客户端可以立刻获得锁。

LAB3思考

image.png

如何保证服务端的线性一致性?

一个日志只能被apply一次,通过服务器连接clinetid+命令commandid来唯一标识一条命令。保证该条命令在一个服务器上只会被apply一次。

如何保证服务端的幂等性?

服务端保存一个map存放每个clientid的最后一次命令的结果。接收到请求如果commandid等于本次client的最后一次命令的id,则说明接收到了重复的请求,则直接根据map返回该命令的结果。不执行该请求。只需要对写请求进行去重,不需要对读请求进行去重。因为只有写请求的重复操作会改变服务端状态。
在服务端收到下层RAFT节点的apply请求时首先判断命令的index是否小于最后一个应用于状态机日志的index,如果小于,则说明是以前的日志被apply,显然不符合一条日志只在一个服务器apply一次。那么我们直接丢弃。

如果Raft状态机apply了日志,客户端还没有同步apply,Raft状态机发生leader变换,那么该条日志还要同步吗?

需要,只要Raft状态机apply了日志,客户端应该无条件执行该日志。

  • 快照是对于SERVER的概念