MIT 6.824学习笔记

introduction


为什么需要分布式计算?

并行容错,物理因素(地理位置),安全

分布式计算面临的挑战?

并发性,部分错误,性能问题

  • 基础设施

    存储,通信,计算————目标是抽象成从外部看上去就是一个中心化的系统一样

  • 实现

    RPC,线程,并发(锁)

  • 性能

    理想是得到可扩展性的提升(XX倍的计算资源可以带XX倍的性能),但是这个非常难

  • 容错

    机器出现故障在一台机器上是罕见的,但是在分布式计算机集群中是很常见的一个问题。

    可用性,可恢复性(利用非易失性存储,复制实现)

  • 一致性

    KV数据库(放入,取出),数据库取出的数据一定是最新的(强一致性),如果不能一定保证的是弱一致性,强一致性相比弱一致性的开销更大,因为可能需要花费更多的通信开销

MapReduce


Map:对输入进行分类处理

Reduce:对分类的结果进行归类

多线程


为什么需要多线程:

I/O并发,并行化,易用性(周期性的检查)

多线程面临的挑战:

线程竞争共享资源(互斥),线程协作(同步),死锁

GFS


  • 大型存储为什么这么难?

    性能问题 ->分片存储

    机器出错->容错->复制->不一致

    一致性->降低性能

    性能和一致性难以达到平衡

  • 强一致性

    同一时刻只处理同一条请求

  • GFS设计的目标

    容量大,速度快,全局化,自动恢复,单数据中心,内部使用(不会面向用户),针对大型文件的顺序访问(大的吞吐量)

  • master存储的数据

    filename到chunk handle(NV:备份至磁盘)的映射(从文件名到chunk标识的映射)

    handle到chunk server列表(V:不用备份至磁盘,因为master启动后会轮询所有的chunk服务器)的映射(chunk标识到具体存储位置的映射,因为一个chunk会存在多个server,所以返回的是列表)

    每个chunk的版本号(NV,便于master找到哪个是最新的chunk,版本号只在变更primary时修改),主要chunk的标号(V),担任primary的轮换时间(V)

    日志,检查点都需要写入磁盘进行容错。出错以后重启master,master只需要从最新的检查点开始重演日志然后恢复至出错前的最新状态。

  • 读数据

    1.client发送文件名和文件偏移量给master

    2.master发送chunk handle和chunk server列表给client,缓存这部分信息便于以后访问

    3.client向chunk server发送chunk handle和偏移量读取数据

  • 写数据

    • master没有primary chunk

      找到最新的chunk副本(通过master版本号和chunk版本号确认)

      挑选其中一个最新的chunk server作为primary chunk,并且设置任期,只有这个期限该节点是primary

      master递增版本号,通知primary和其余备份的secondary节点保存最新的版本号至各自的磁盘

      master将最新版本号写入磁盘进行备份

      client收到所有相关文件的chunk server地址后,发送需要修改的数据给primary和secondary节点,client可以通过发送给就近的chunk server,然后这些收到消息的chunk server再通知其它server

      primary首先按照client发来的请求进行写入操作,然后通知其它secondary节点按照primary同样的顺序执行

      secondary节点成功写入数据后,会向primary节点回复yes,如果primary没有收到或者收到了错误的回复,则会发送没有成功的消息告诉client,client可以重新执行请求。如果收到成功的回复,则说明所有备份节点都成功写入,如果失败,则有部分(可能也没有)节点成功写入。

      即使master不能联系上primary,也需要等待任期结束,因为可能是由于网络原因导致没有联系上,如果直接任期一个新的primary,可能会导致脑裂

      “脑裂” 如果同时出现两个primary导致的出现不同的副本的情况就是脑裂,可能由于网络分区造成的。

      创建新文件的过程与写入类似,master发现没有写入文件的chunk handle后会随机找到新的primary和secondary节点,并且生成全新的版本号

主从备份与复制


  • 复制实现容错,这个错误智能解决fail-stop问题

    fail-stop:由于单机出现物理故障导致的错误,不包括软件bug或硬件设计的缺陷

  • 状态转移(存储)

    primary将所有最新内容发送给backup进行备份(每次只发送修改的部分可提高效率)

  • 复制状态机(外部输入)

    primary将来自外部的输入或事件发送个backup,由于没有没有外界输入时primary和backup的操作一致,所以在保证有同样输入后backup和primary仍可以保证一致

  • 如何实现replicated state machine(复制状态机)

    状态是什么:主从同步,切换主机,匿名切换,新的备份

    一般都是只复制应用程序级别的数据(如GFS只复制应用程序可见的相同的chunk),这种方法虽然高效,但是必须要和应用程序绑定,内置在应用程序中实现,机器不知道复制的内容的具体含义

    还有一种就是基于系统底层进行的将数据进行全部复制(如VMware的容错机制),这种方法效率较低,但好处就是可以不用考虑在其上面的应用程序,具有很强的适配性

    primary和backup之间会通过日志(log entry)进行同步,这个是周期持续的,如果backup有段时间没有从log channel中收到primary发送的日志文件,就会根据容错机制,backup就可以开始自由执行而不需要再等待primary的输入,成为新的primary

    如果有些指令(例如随机数)在不同电脑上执行结果不一样,包含这种操作的网络数据包中的各种操作交给primary和backup后,只有primary会执行所有的操作,而backup不会作其中的指令,只会等待从log channel中获取最终结果。其余的可以一样的指令则是primary和back up共同执行

  • 非确定性事件

    外界输入(输入包和中断信号):可能不是同一时刻到达,到达时primary和backup的状态不一样

    特殊指令:指令在不同的电脑上可能结果不一样,如生成随机数,获取当前时间

    (多核处理):服务指令在多核上交错执行,执行顺序是不可预测的,导致结果可能会不一致,这个被VMware FT的论文屏蔽了

    上述问题通过backup利用log entry去同步执行结果,而自己不去执行的方式来保存同步

    这些问题需要通过日志事件(log entry)进行处理,log entry中可能包含有,指令的序号,指令类型,数据。

    backup中有一个缓存区存有和primary同步的操作,backup只有当缓存有操作才会执行指令,所以backup肯定会比primary慢一两个事件

  • 数据到达

    数据到达后,由于如果直接放入数据会导致primary(backup)不确定最终在哪个时间点看到数据包,所以数据会先存在VMware的私有内存中,然后先把primary(back up)挂起,然后在primary(backup)没有看到的情况下将数据复制到内存中,然后VMware在发送模拟NIC发送中断给primary(backup)

    数据包大部分都是传递的数据,只有极少一部分是非确定性的指令

  • 输出规则

    只有当所有的backup都收到了log record的后primary才能够向外发送输出,所以如果client能看到的最终输出一定是其他的backup也都拿到这个结果了

    这种方法会造成primary的停顿,因此会损失部分性能,为减少这部分损失,可以只针对高级操作(如写)进行停顿,而一些普通的操作(如读),就不需要按照这种方式同步

    如果由于网络原因,primary和backup互相无法通信,即双方都认为对方宕机了,那么如果直接上线新的主机代替主机就会导致两种新的运行方式,即“脑裂”。为了避免这种情况,需要引入外部帮助,通过设置test-and-set,primary和backup同时向其发起请求,只有一个能够单独申请成功,然后两个只有一个能够最好申请新的主机。

Go,Threads and Raft


  • Go

    使用闭包时注意调用外部参数是否需要为当前的值,如果需要则要在参数传入改值,因为可能闭包调用外部参数时这个值已经发生了变化。

    互斥变量mutex主要是用于保护互斥变量,或者有些中间会变化但总体不变的值,总之就是为了让一部分的代码能够具有原子性。

    条件变量condition可以让一个线程阻塞,然后等待另外一个线程唤醒

Fault Tolerance - Raft


mapreduce、GFS、VMware FT采用的容错机制都有一个特点,就是由单个节点进行控制(master,test-and-set),这样的好处是单点控制非常高效,但是存在单点故障的问题。如果单纯的增加控制节点的个数来增加容错,但是可能会遇到网络故障(如网络分区)导致的“脑裂”问题。

未解决上述问题,Raft提出的一种观点就是利用少数服从多数的思想

在服从大多数的思想中,服务器必须是奇数才能够保证一定能够得到一个大多数的投票结果,此外超过一半的限制条件所取的一半的值是所有服务器的一半的数量,而不是当前在线的所有服务器的一半的数量,因为一般来说,具有2f+1个服务器,可以容忍f台服务器出现故障,仍然可以得到大多数投票后的结果。

使用大多数的思想可以使得网络分区导致的两个分区都不能正常的工作。

大多数的思想确保了每次参与选举的服务器至少有一个是重叠的(如三台机器,需要两个服务器选出leader,那么肯定两次选举有一台服务器是重叠的),这也保证了每次选举新的leader知道前一个leader的任期号,这也是为什么Raft是正确的原因

  • 执行过程

    leader接收来自client的请求,然后将其记录为日志,然后通过RPC请求发送给其他的follower同步该操作日志,当leader收到超过一半(包括自己)成功接收的请求后,然后再把该日志变为提交状态,然后告诉其他follower该消息已经被提交。并且执行其中的操作。

  • 日志

    日志记录了需要执行操作,并且进行排序与执行操作的顺利一致。follower将待定的操作放在日志中,等待与leader同步后再执行(因为可能由于不同步而丢弃部分日志);leader需要将操作记录在日志中然后发送给follower进行同步,并提供以前的日志消息给一些由于故障丢失部分日志消息的follower进行恢复。此外,重启一台崩溃的服务器也需要日志来恢复至当前系统的最新状态。

  • leader选举

    每个term只有一个leade,每次都有一个选举计时器进行计时,超时后就会开始新的一轮的leader选举。然后服务器作为candidate强制开始选举,首先会将自己的term加一,因为自己也想成为leader,然后发送RequestVote进行投票。每个服务器都只能投一次票,所以如果发起RequestVote会给自己投票后就不会再给其它人投了,这也确保每次都只会有一个candidate能够获得大多数的票成为leader。

    成为leader的服务器会向其它服务器发送心跳包来告诉它们自己是leader了。这里的心跳包是以一种隐式的方式,通过发送包含空的日志以及新的term的AE代表心跳包,因为AE只有leader可以发送,从而告知follower自己是新的term的leader。并且每次follower收到一次AE,就会重置自己的选举计时器,从而防止其他follower成为leader。

  • 随机选举超时时间

    每个follower的选举时间计时的时间是随机的。这个随机时间必须大于心跳广播的间隔时间,防止每次心跳广播还没完成就开始新的一轮选举了。小于系统内发生一次故障的时间间隔。每个随机的选举超时时间之间的时间间隔也应当足够大,确保能够在这个间隔中收到超过一半的选票,防止出现分裂。

  • leader同步日志

    leader发送AE回带上自己最后一个日志的下标index和任期term,follower接收到后会检查AE中的index和term是不是和自己的相同,如果是则添加AE中附带的新的日志,返回true。如果不是,则会返回一个false,leader就会针对这个follower的nextindex减一,然后再发送一条新的AE,其中的lastindex和lastterm 为上次发送的前一个的日志index及其term,然后直到follower返回true,再发送从nextindex到最新的所有日志给这个follower进行同步

  • leader选举

    为什么不直接使用日志最长的服务器为leader?

    因为可能会出现虽然某服务器的日志长度最长,但是term却不是最大的,如果它成为leader可能会覆盖一些已经其他服务器已经提交的日志(更大的term)。

    选举限制

    1.发起投票的选举人最后的的日志的term必须高于收到投票的人,能够收到赞成票(因为更大的term更有可能是从上一个leader接收过日志的服务器)

    2.发起投票的人发送的最后的日志的term与收到投票的相同,那么last index要大于或等于收到投票的人,能够收到赞成票

  • 日志加速回滚

    follower出现冲突后发送产生冲突的term以及该term的第一个index,以及log的长度,然后leader进行对比。然后分情况处理:

    1.如果leader没有这个term,就直接设置该follower的nextindex为返回的这个index,然后重新发送日志进行同步。

    2.如果leader有这个term的一些index,那么设置该follower的next index为自己的这个term中的最后一个日志的后一个进行同步。

    3.如果follower日志长度短与leader,也就是发送的冲突的位置follower根本就没有,那么设置nextindex为follower的最后一个日志的index(通过log的长度算出)。

  • 持久化

    log日志:持久化存储日志是因为日志代表了整个应用的状态,可以用于重建整个系统

    currenTterm当前的任期:为了保证每个term只有一个leader,如果没有持久化,则可能需要从当前日志最后一个term去计算,然而这可能得到的不正确。

    votedfor所投票的候选人的下标:确保每个term中只给一个服务器投票,因为可能服务器在投过票后崩溃,重启后又给另外的服务器投票。

    为什么commitIndex(已提交的最新日志下标),lastApplied(已应用的最新日志下标),nextindex(每个服务器的下一个应该同步的日志编号)、matchindex(与每个服务器同步的日志下标),不用持久化存储呢?

    因为这些都可以通过检查日志,以及发送给follower的AE进行重构

    服务器重启后会重新执行已经提交的日志来重建应用状态,这种方式显然是非常低效的。

  • 日志压缩和快照

    将一部分的索引下的日志项创建一个快照,里面包含了这些日志的最后状态,以及最后一个日志项的term以及index.然后这部分日志就不需要存储了。从而大大减少存储空间。

    快照的另外一个好处在于可以帮助重启的服务器通过读取快照里的应用状态信息,快速恢复最新的应用状态。

  • 同步快照

    当follower的日志快照远远慢于leader,部分没有的日志在leader中已经放入快照中了,此时leader会发送包含当前它的日志快照以及当前的日志的installSnapshot RPC给该落后的follower,从而来帮助该follower进行同步

  • 线性一致性

    执行记录是线性的,历史记录中的某些操作的顺序和实际的请求实时顺序匹配,每次读的操作都是读的与之最近的(最新的)写的结果,则可以说明是线性一致性的。

    线性一致性要求历史记录中的读写操作是一个接一个的有序的,线性一致性的系统的特定就是无法提供过时的数据。

ZooKeeper


为什么需要ZooKeeper?

1.分布式系统的通用化API应该是什么形式?->zookeeper给出具有通用性的服务

2.N台服务器等同于N倍的性能吗?->leader可能会达到性能瓶颈导致并没有性能提升->考虑将读请求分发到各个follower中而不仅仅是leader,这样减少leader的负载,从而提升效率->不能用follower去接受client的请求,因为follower存储的信息不是最新的结果。

ZooKeeper可以让follower接受client的读请求,因此ZooKeeper不具备线性一致性,因为client可以读到过时的信息,即读操作不是线性一致的

  • 读写操作

    ZooKeeper的写操作只能发送给leader,当leader发送给大多数节点并收到成功的回复后再相应客户端,所有的replicated会按照同样的顺序更新状态,因此具有线性一致性。而读操作可以发送个任何一个replicated来相应,这就导致读操作返回的结果不一定是最新的,但是由于client FiFO的规定,索引client一定能读到自己写入的最新的操作。

  • ZooKeeper的一致性保证

    1.写请求是线性一致性的:即使有多个并发的请求,系统还是会表现的像是以某种顺序一次执行一个写请求,即写请求的请求顺序和实际执行顺序是一致的

    2.任何给定的client的操作执行顺序是由client决定的,即先进先出的client请求顺序,即写操作的顺序是由client发起请求的顺序决定的,读请求会附带上日志的id,来获取最新的日志下标,如果请求的replicate没有该日志就会延迟返回该结果。

    3.对于FIFO的请求顺序的要求就是如果发送一个写请求并随即发送一个读的请求给replicatied,则该replicated需要先更leader同步最新的写请求然后再响应该读请求,确保读写的顺序是和发送请求的顺序一致而得到的结果,但是这个顺序只是针对单个client。也就是client能够读到自己发送的最新的日志(因为知道最新的zxid),但是没法确保能够读到别人写入的最新的日志

    client在读取replicatede的日志时,会先发起一个exit的请求确认是否有该日志,并且在该replicated上在watch table为该日志设置一个watch,这个watch可以监视该日志,如果该日志被删除则会通知client

  • ZooKeeper的用途

    1.实现VM中的可替换的TEST-AND-SET

    2.帮助master配置相关的信息

    3.选举新的master

  • API

    ZooKeeper的API类似文件系统,app都有自己的从根目录往下生成的路径来区分

  • Znode

    代表了文件或者目录,共分为了三种

    1.常规Znode:由client显示的创建,使用和删除

    2.临时Znode:由client创建临时使用,当ZooKeeper认为该client已经断连(Session终止),则会删除该Znode

    3.具有顺序的Znode:同一时间如果有多个client同时创建sequential文件,那么这些文件序号不会相同,会随着生成时间序号逐渐递增

  • 操作

    create操作中需要写入flag参数来设置生成的Znode的类型

    delete,setData操作都除了必要的path参数还需要加入参数version,只有对应Znode的version能够对应上才会执行该操作,也就确保如果有多个该类请求,只会执行一次。

    exists,getData操作除了必要的path参数还可以设置参数watch,如果watch=true,那么该路径下的文件只要发生了变化(如更新、删除)都会通知给client

  • minitransaction加一操作(低负载)

    while true{
       data,version=getData("f")
       if true==setData("f",data+1,version)
       break
    }
    

    这段伪代码描述了如何在Zookeeper中实现增一。之所以能够满足的原因如下:

    虽然getData的读操作不能获取最新数据,但是在setData的写入操作中,可以通过参数version确认是不是获取的最新的数据,如果不是则不会执行该操作并且返回false,重新执行整个操作。

    虽然get和set并不是原子操作,但是同样可以通过version确认是不是写入的时候系统的数据还是读数据时候的值,如果不是则同样不执行操作并返回false

    显然当执行这段代码实现加一操作的服务器越多,那么越难得出现执行成功的服务器,因此不适用与高负载的情况。

    可以通过在失败后休眠一段随机事件从而错开各个服务器的请求来满足高负载的情况

  • 惊群效应下的锁(不可扩展)

    if create("f",ephem=T)
      return
    if exists("f",watch=T)
      wait
     again
    

    当client想创建临时文件失败后,则使用exists看是不是已经有文件,如果是则等待其余client退出,因为只能有一个client可以在对应路径上有自己的文件。然后重新生成文件。

    惊群效益就是指多个client同时发起同一种请求,则复杂度是(N2),因为同一时刻只能有一个请求能够满足,其余的需要重新请求,因此总的请求次数为N+(N-1)+(N-2)…+1,计算出来的复杂的为(N2)

  • Lock(避免惊群效应)(可扩展)

    n=create seq "f"  -EPHEM=true
    List f*
    if no lower file than n,retun //获得锁
    if exists(next lower file,watch=true)//监控小于自身的下一个文件,即100监控第99个,只用第99个退出后才会推出等待
    wait
    go back
    

    client通过生成临时的有序列的文件,然后列出所有f下的文件,如果自己生成的序列文件是当前所有文件中序号最小的,则获得锁。否则等待持有最小的文件的client使用完后删除该文件,然后重新回到第二步列出所有文件,查看是不是最小的。

    之所以能够避免惊群效应,因为每个进程只需要等待前一个进程推出(前一个序号的临时文件被删除),然后再重新获得锁,因此每个进程获取锁和释放锁的复杂度都是常数级别的,与具体的参与服务器数量无关。

CRAQ


  • 链式复制

    多个副本收到具有一致性写顺序的请求。服务器形成类似链状的结构,client发送给第一个服务器后,第一个服务器接收写入信息后发送给第二个,以此类推到最后一个接收请求后,返回成功的回复给client。读请求则会由client发送给最后一个服务器,然后返回给client。最后一个服务器确保了返回的信息是全部服务器都有的。因此具有线性一致性。

    但显然这个方法对于故障回复来说是很复杂的,发生故障后的服务器接收不到最新的结果,如果只是头或者尾的服务器发生故障,则直接删除,以第二个为新的即可。而如果是中间的,那么前一个结点需要发送请求到出现故障的后一个服务器上才行。

    这种方法的好处就在于第一个服务器只需要发送一次请求,不像raft的leader需要发送给所有的follower。但是坏处就在于如果有一个replicate的性能差,就会拖慢整个的效率。

    一般会设置配置服务器,来确认新的header

  • 线性一致性

    链式复制具有线性一致性

Aurora


  • EC2

    在本地的硬件服务器上运行虚拟机监视器,在这些虚拟机上运行多个服务实例(如WEB服务、数据库服务),将这些服务提供给用户。每个服务器都会挂载一个磁盘,而每个用户实例都能够获得一部分的磁盘空间。

    EC2能够很好的适配web服务,但是并不适合数据库的服务。因为如果EC2服务器崩溃了,那么就无法访问EC2上挂载的硬盘,如果是web服务则简单的重新启动一个新的EC2即可,但如果是数据库就会丢失数据库的数据,无法重新启动另外的EC2来恢复数据。

    后来推出了S3服务生成快照用于恢复数据,但是还是会丢失快照之间的数据

  • EBS(Elastic Block Store)

    EC2原本的磁盘称为本地存储。EBS通过两个EBS服务器组成,每个EBS服务器都挂载了磁盘,这样的服务器被称为EBS volumn。当EC2服务器发生写入请求后,这个写入请求也会通过网络发送给EBS服务器,然后这两个服务器通过链式复制到自己的磁盘上,然后最后一个服务器返回成功的信息给EC2。这样EBS就可以帮助EC2实例在崩溃中恢复数据,EC2服务器崩溃后,就可以重启另外的EC2服务器,然后再连接到原来的EBS上恢复数据。

    每个给定的EBS volumn只能有一台EC2实例或者虚拟机挂载。

    但是EBS会产生大量的网络流量,需要进行写的操作也很多,因此整个系统对网络的要求也很高。

    而且容错能力也不是很强,因为为了性能和降低链式复制的成本,一般EBS volumn所在的replica是在同一个数据中心,如果数据中心被摧毁,那么所有数据都会被破坏。

  • RDS

    将一个数据库复制到多个可用地区。

    EC2数据库的实例将日志和数据保存在EBS中,每当有新的写入操作后,会将写操作写入一个地区的两个EBS volumn中,此外数据库会把这些写操作透明地发送给第二个可用区域的一台机器(如EC2实例)中,这台机器的主要职责是复制主数据库中的写操作,该镜像服务器会将这些写操作复制到第二组的EBS服务器上。只有这两个区域的数据都完成写入操作后,那么这个新的写的操作就完成了。

    这种方法确实有更好的容错能力,但是需要很高的开销代价,速度也不快。

  • Quorum(法定人数)

    N个复制服务器,只要有W个回应就可以写,R个回应就可以读,W和R即是阈值,这里读写的服务器必须有重叠的,因此W+R>N(如W+R=N+1,有一台重叠)。这就确保每次涉及读请求的服务器中肯定有服务器已经接收到了最新的写请求。但是这样会导致读请求返回的值肯定会有不同的,因此每次写操作都会有新增一个版本号(版本号会逐渐递增),所以在处理读请求时,选择接受版本号最高的结果即可。

    无论时发起读还是写请求,只需将请求发送给所有的N个服务器,然后等待有阈值(写W个,读R个)个返回结果即可。

    通过设置不同的R或者W的值,可以有效满足不同的情况。 比如N=3,R=1,W=3,这种情况下由于R很小,就非常适合读请求较多的情况。同样类比写请求较多的情况。

  • AuRoRa

    一个数据库服务器,并且有6个replica服务器,每两个服务位于一个区域。数据库的每个写请求都会发送到6个replica中,这里传输的数据只有日志记录,从而降低传输负载。 只要这6个replica中返回确认的数量达到了与之,数据库服务器就可以继续运行,从而增加了性能。

    AuRoRa设定的Quorum系统,N=6,W=4,R=3

    W=4可以保证即使一个可用地区的服务器都崩溃了仍然可以运行写服务

    R=3可以保障即使一个可用地区的服务器以及另外一台服务器崩溃了仍可以读服务,此外当崩溃了三台服务器,虽然不能继续写,但是可以通过读重新设置一台新的服务器,通过读取的日志恢复。

    W=4也可以容忍有两台很慢的服务器临时不能使用

  • 容错目标

    即使一个可用地区的服务器挂掉后仍然可以继续写服务

    即使一个可用地区的服务器以及另外一个服务器挂掉仍然可以继续读服务

    部分存储服务器变慢或不可用的情况下服务依然能够继续运行

    快速重新复制,如果一个复制服务器挂了,能够通过剩下的replica快速生成一个新的replica

  • 数据库服务器

    存储服务器写入的时日志记录,但读取的时data page。服务器内部会保存数据库服务器的某一时刻的数据(缓存),因为写入的日志会追加在旧的data page的列表,但并没有应用,只有当数据库服务器或者恢复软件需要查看该page时,才会将所有的日志应用在旧的data page上生成新的data page

    有时不需要通过Quorum去实现读请求,因为数据库服务器会存储每个replicate的日志序号,因此只需要把请求发送给拥有最大序号(最新)的replicate即可。但是在数据库服务器崩溃以后,需要利用Quorum的读请求去读取数据来恢复自身,因为可能此时最长的replicate中可能包含了一些只执行了一半的事务。并且将大部分没有的会实行删除,通过日志存有的旧值实现回退操作。

  • AuRoRa如何处理大型存储

    将大型数据进行分块,每一块用一个含有6个replicate的服务器进行存储,每6个服务器为一个PG,针对每一条日志操作,会根据操作参与的数据所在的PG进行发送。

    每个存储服务器存储了多个不同的AuRoRa实例,因此在恢复时,可以通过选择多个不同的存储服务器从中并行读取对应的AuRoRa实例,从而快速恢复。

  • 只读数据库

    适用于读请求较多的情况,通过设置制度数据库,对于读请求可以直接查明所需的data page的位置,在不麻烦主读写数据库的情况下,将读请求直接发送给存储系统。只读数据库上也存有数据缓存来加快读取数据,并且主读写数据库会发送最新日志给只读数据库来更新日志,来反映数据库中最近的事务。

    只读数据库如果读取到正在执行的事务,则要么不显示,要么显示修改后的数据,绝不能是修改前的

Frangipani


  • 特点

    1.缓存一致性

    2.分布式事务

    3.分布式崩溃恢复

  • 构成

    多个工作站(电脑)上配置好了编译环境和Frangipani软件,通过RPC与远方的共享磁盘Petal进行交互,通过该磁盘给出的一个block号或地址对文件进行读写。这种构成主要为了实现多个工作站可以在系统中共享文件,但由于没有安全措施,所以比较适合小范围的可信任的群体。

  • write back cache

    写操作一开始会缓存在本地工作站,可以在本地对自己的目录和文件进行各种修改,然后等需要的时候再发送到共享磁盘中同步,这样无需每次修改都要发送RPC请求,从而增强了性能。

    这种设计还有的好处就是会把大部分的复杂度和CPU时间花费在了client端(本地工作站),只要新增新的用户工作站就能提升整体性能,具有可扩展性。

  • 挑战

    去中心化的缓存:

    1.缓存一致性:别人在缓存中进行的修改的数据,可以在我的缓存中立刻反映出其他人做的修改

    2.原子性:当不同的人在同一个目录下进行新增,希望双方的操作都能够被保存。

    3.单台服务器的恢复能力:单个工作站发生了崩溃,不会影响到同一个共享系统的其他用户

    • 缓存一致性

      引入lock服务器,保存了一张表存储不同的文件锁,该表存储了对应的文件信息和缓存信息。只有工作站持有该数据所对应的锁,才能够允许它对数据进行缓存。

      工作站先拿到锁,然后才能从petal上读取数据。如果对数据进行了修改,则在释放锁之前需要将修改后的数据写回到petal,返回成功后才能释放锁。

      工作站拿到锁后,每个锁都会有忙碌或空闲的状态,而且即使锁空闲了也不会立即释放,可能会等待很多都空闲了才会释放(除非有其他工作站请求)

      如何拿到锁,如果一个工作站请求的锁被别的工作站使用了,则lock服务器会发送一个revoke信息给持有该锁的工作站,如果该锁是空闲状态,那么可以将该锁对应的修改后的数据写回petal(如果有修改),然后再释放锁。如果是使用状态是使用中则需要等待用完。

      所以当两个工作站想要对同一个文件修改,第一个已经获得锁,第二个想要获得锁只有等第一个完成修改释放锁后才能获得锁,此时从petal中读取到的就是已经修改后的数据了。也就是每次读取的数据肯定是最新的。

      为了防止自身的数据崩溃,工作站会每30秒将缓存修改后的数据写回petal

    • 原子性、

      实现事务:本地工作站只有完成修改,其他工作站才能看到修改的内容,并且在执行修改的期间,需要去获取所有读取和写入所需的全部锁,直到完成了操作才会对这些锁进行释放。

    • 崩溃恢复

      在持有锁的阶段崩溃,就会导致部分修改完成一半,导致严重的问题

      预写式日志:当工作站需要执行复杂的操作,即需要将内容写入pedal前,会网petal中追加日志条目,这些日志描述了该工作站做的完整的一组操作。只有这些日志全部都安全写入petal后,工作站才会把写操作发送给petal服务器。

      每个工作站都有自己的工作日志,且工作日志存在petal上,每个工作站都只有自己的半私有日志。

      日志体 :日志编号,块编号,版本号,写入的数据

      什么时候返回日志:收到revoke消息返回部分日志到petal上,然后将所相关的写操作进行更新,然后返回消息给锁故武器。

      如果持有锁的服务器崩溃,此时lock服务器发送revoke请求没有相应,就会触发释放的操作,即判定该工作站崩溃,然后告知其他服务器该工作站已经崩溃,让其他服务器去通过检查petal中已经崩溃的工作站的日志(此时petal上有执行一半的事务才需要重演,否则不用),并重演它近来的操作,保证这些操作都是完成的,只有都完成后才会释放锁。

      为了防止重演操作会删除修改同名的文件,所以每个更新都会有版本号进行标识,因此同名文件不会被错误修改

  • 总结

    由于使用的是文件系统而不是数据库,所以比较适合小范围的可信用户,不适合大数据存储,而且主要的缓存一致性也不适合大量的数据。

分布式事务


ACID:

A:atomic原子性,要么都完成,要么都没完成

C:consistent一致性,修改前后数据综合一致

I:isolated隔离性,具有顺序的,事务之间看不到彼此的修改和中间值,智能看到最后的结果

D:Durable持久性,事务完成后的修改是持久性的

序列化:事务的并行和并发执行是有顺序的,即以相同的顺序执行相同的事务会得到相同的结果

  • 并发控制

    通过并发控制使使用同一数据对象的不同并发事务进行彼此隔离,从而获得有序性。

    • 悲观并发控制

      在事务使用任何数据之前,它需要先去获取该数据所对应的锁,适合经常发生冲突的情况。

      • 两段锁

        1.对任何数据进行读取或写入前,需要获取该数据对应的锁。(确保执行的可序列化)

        2.知道事务被提交或者中止后,事务才能释放掉他所获取的锁。(防止与其他事务的操作交叉执行,得到不正确的结果或者事务没完成得到幻读结果)

    • 乐观并发控制

      不用担心多个事务同时读取数据,只需对数据读写,只有在结束后检查是否有其他事务对执行的事务有影响,如果有则必须中止事务并重新执行。适合冲突较少的情况。

  • 原子性提交

    事务执行一半,一个服务器上修改了,一个服务器上没有修改,这种问题出现的原因就在于原子性没有得到保证。

    • 分布式两阶段提交

      前提:有一台电脑负责对事务进行协调(事务协调器),当两台不同的电脑(参与者)需要共同完成一个事务(需要对两台电脑上的数据都要进行修改),然后由事务协调器发送信息给这两个电脑让他们执行相应的操作,并且保证这两台服务器执行的操作要么都完成,要么都没完成。

      过程:事务协调器收到client 的请求后,把需要执行的事务发送给参与的服务器A和B,参与者获取相关锁执行该事务,然后事务协调器再发送prepare命令给A和B:

      如果两个都返回了ok即可以执行命令,那么事务协调器再发送commit请求给两个服务器,等两个服务器执行完后返回ACK给事务协调器,则事务执行完成。

      如果有一个回复了no,那么事务协调器会发送abord命令给两个服务器,让两个服务器回滚该事务。

      此外当服务器接收到commit或者abord的时候就可以释放相关的锁。

      为了防止再prepare阶段服务器返回ok后崩溃导致无法响应事务协调器发送的commit,因此参与方在收到prepare并返回ok后, 会提交所需的事务先持久化到磁盘上防止崩溃。同样为了防止事务协调器崩溃导致无法继续,则收到prepare的反馈后,也需要将该事务的信息持久化。

      当事务协调器发送prepare后长时间没有收到所有的服务器的回复,则可以单方面决定终止该事务。同样当参与者一直没有收到prepare的请求,也可以去中止这个事务。但是如果参与者返回prepare请求为ok后,却迟迟没有收到commit请求不能单方面的终止事务,因为可能只是该参与者没有收到commit,而其他的参与者已经执行commit。这种情况只有等待事务协调器进行修复。

      两阶段提交的坏处在于速度慢,需要大量的提交

Spanner


解决的是地域复制问题,即一个将数据分布式存储在不同的区域,从而增加容错性。

特点:

1.两阶段提交

2.使用Paxos

3.通过同步时间做到高效只读事务

4.外部一致性:执行在某一事务之后的事务可以看到前一个事务执行完成后的修改

数据中心通过不同的Key值对存储的内容进行分片,每一片数据都由相同的Key。然后不同地区都有不同的数据中心,不同的数据中心中相同的分片都是一个Paxos组,每个组都有leader和follower。

将数据分为多个副本存储在不同的地方有两个好处:一是可以防止某个数据中心遭到破坏导致数据丢失。第二就是可以便于不同的地区的人可以连接较近的服务器快速读取数据。

两个挑战:

  1. 如果从最近的数据中心读取的数据可能不是最新的,不满足外部一致性
  2. 如果事务涉及到多个Paxos组,则需要分布式事务
  • 读写型事务

    执行事务 x=x+1 y=y-1

    当有一个事务涉及到两个分片X和Y,每个分片都有自己的Paxos组,每个组都有自己的leader。每个服务器都有锁服务,每当对服务器上的一个data item进行读取或者写入时,需要将一把锁和这个data item进行关联

    client发送事务,并且使用唯一的事务id标记发送的消息。首先client会向分片X所属的paxos组中的leader发送一个读请求,然后该分片会对X加锁,并返回x的当前值。同样向Y发送一个读请求,加锁并且获取对应y。然后client在内部计算,得到x和y需要写入的值(即x+1和y-1),然后将需要更新的值发送给leader。然后client选择其中一个Paxos组(例如Y)作为事务协调器。然后把包含想要写入的新值和该事务协调器的Id(被选为事务协调器的Paxos分组的leader ID)发送给另外的Paxos分组(例如X),同时也会发送需要更新的值给指定的事务协调器(Y的leader)。然后下面同步执行:

    1.非事务协调器的Paxos的leader(X)收到请求后,会发送prepare消息给它的follower,并且写入Paxos的日志中,当收到大多数的OK的回复后,就会根据收到请求中的事务协调器的ID,发送YES给事务协调器(Y的leader),表示可以提交

    2.事务协调器的leader收到请求后,发送prepare消息给它的follower,等待收到大多数follower的确认消息,向自己发送一个YES。表示可以提交

    当事务协调器收到所有涉及的分片的Paxos中的leader的响应后,会讲commit请求写入自己的日志,然后,事务协调器(Y的leader)会发送commit给自己的follower,并且发送一个消息给其他Paxos分组的leader,告诉其可以提交了,然后这些分组的leader就也发送commit消息给自己的follower。当这些分片执行完commit操作后,就可以释放对应的锁。

    两阶段提交有一个问题在于当事务协调器如果持有锁的情况发生故障后,需要等待事务协调器恢复然后重新继续工作,但这就会导致参与者长时间阻塞。

    Spanner通过复制事务协调器解决了这个问题,这个方法基于Paxos的状态机复制机制,因为Paxos不管日志是否提交,都会复制日志给各个follower,所以即使leader崩溃了,任何一个剩下的follower都能接替原来leader的工作,即事务协调器的工作。

    读写事务由于需要不同地方的分片进行交互,所以整体处理的速度是较慢的。

  • 只读事务

    只读事务相比读写事务减少的开销:

    1.可以直接从就近的replica中读取数据,而不需要与leader交互(但可能不是最新的数据,需要一定的限制)

    2.不需要锁和两段提交,所以不需要发送给leader

    • 正确性约束

      1.事务执行是有序的(因为需要考虑前面是否由读写型事务):并发执行的结果和一次连续执行的这些事务的结果一致,对于只读事务,即该事务的所有读操作可以看到在它执行之前的事务的所有写操作执行的结果。

      2.外部一致性:等同于线性一致性,每个事务执行之前都能看到前一个事务执行的写操作的结果,只读事务看不到过时的事务。

      如果简单的直接执行只读事务,则可能事务中的不同操作穿插在不同读写型事务的中间,导致结果不具备有序性。

    • 快照隔离

      所有参与的服务器都有一个同步的时钟,每个事务都有一个时间戳(从它们上面的同步时钟获得),读写型事务的时间戳就是事务提交的时间,对于只读事务,它的时间戳是事务开始的时间(读请求携带的时间戳),如果所有的事务都是按照时间戳顺序执行,那么它们的生成的结果是一样的。一般每个事务中涉及的数据都有自己的时间戳。

      只读事务在发起读请求时就会携带一个时间戳,然后获取该对象的多个版本,找到比请求中的时间戳小但是时所有版本中最大的数据,即为最新数据。

      数据库是一个多版本的数据库,会记录同一个记录多个时间的副本,形成多个版本的数据。(如x@10=9,x@20=8,代表x在10时是9在20时是8)

      以上约束可以确保获得外部一致性。

      • SAFE TIME

        为了确保即使我们从非leader也能读取到新的数据,当读请求的时间戳大于请求的replicate中最大的时间戳,那么该replicate会推迟响应该请求,只有当它从leader出获得了大于或等于该请求的时间戳,才会对该请求做出回应。

  • 时间同步

    如何确保不同服务器上的时钟都是同步,同一时间能够读取到相同的值。

    如果时钟不同步会发生什么?

    读写型事务因为有锁和两段提交所以没有影响。

    对于只读性事务:

    时间戳取得较大:会有很久的延迟,但是最终的结果是正确的

    时间戳取得太小:会丢失部分最新的写操作,得到的是非常旧的数据,违反了外部一致性。

    通过UTC发送给GPS接收器,然后GPS卫星发送给GPS接收器,GPS接收器再与time master交互同步,本地服务器再与time master每分钟进行同步。显然这些信息传播的期间会有误差的出现。Spanner采用true time解决这个误差。

    • True Time

      TTInterval由最早时间earliestTIme和latestTime组成,应用程序查询时间得到的是TTInterval这个时间段,

    • 两条规则

      1.Start Rule:事务选择的时间戳等于latestTime,即只读事务和读写型事务设置的时间戳都等于latestTime

      2.Commit Wait:只读性事务实际提交事务之前需要延迟等待一段时间,等到当前的时间戳的earliestTime大于它选择的那个时间戳才提交,这个选择提交的时间戳必须小于下一个读事务的earliestTIme,也就确保该事物的提交时间戳一定小于后面读事务的时间戳。这就保证了读事务的latestTime一定小于前面的读写型事务的latestTime,并且也有一段时间确保读写型事务成功提交,避免了误差时间。

乐观锁并发控制


  • FaRM

    • 结构概述

      针对位于同一个区域的数据中心使用,所有的服务器都在一个数据中心里。采用了RDMA(远程数据直接传输)和乐观锁并发控制。

      利用ZooKeeper作为配置管理器,数据根据Key被分片,然后每个分片都会有一个primary和Backup进行存储。这些replicate只要有一个发生了变化,其余的都要跟着同步,而且读请求只能发给primary,这种容错机制保证只要有一个replicate存活,那么该数据分片就是可用的。

      客户端扮演事务协调器的角色。

    • 高性能

      数据分片是获得高性能的主要方式,不同分片并行计算。

      将所有的数据放在服务器的RAM中,并且是NVRAM(非易失性RAM)

      使用RDMA,即在不对服务器发出中断信号的情况下,通过网络接口卡(NIC)接收数据包并通过指令直接对服务器内存中的数据进行读写。使用了kernel bypass的技巧,应用层代码可以不涉及内核的情况直接访问网络接口卡。

      • NVRAM

        避免将数据写入磁盘从而大大提升了效率。为了防止供电故障导致的数据丢失,服务器上都配有电池,停电后可以提供电能支持。但是电池只能坚持约10分钟的时间,因此当电池供电发生时,FaRM服务器会立即停止所有操作,将每台服务器上的所有数据复制到服务器挂载的固态硬盘上,完成后再关机。等到恢复电后,就会重新读取磁盘的数据进行恢复。

        但是该方法只针对供电故障,对应其他的硬件或软件导致的故障,只能通过建立多个replica进行容错。

  • 网络

    传统RPC网络需要设计较多的交互和中断,还需要调用复杂的内核代码,传输速度较慢。

    FaRM通过两个设计来加快网络速度,一是Kernel Bypass,二是RDMA

    • Kernel Bypass

      让应用程序直接访问网络接口卡,而不用去调用内核机制,并且使用DMA直接访问内存的信息。这种处理需要最新的NIC,并且应用程序需要完成TCP的功能(如重传)。kernel bypass使用了DPDK工具包可以方便的写出这类程序。

    • RDMA(远程直接内存访问)

      需要特殊的网络接口卡才能支持。源主机可以通过RDMA系统发送一条特殊的消息告诉网络接口卡直接对目标应用程序地址空间的内存进行读写操作。CPU对于其中的写操作根本不知情。(One-Side RDMA)

      One-Side RDMA可以用于直接读取,但不能用于修改,因为还需要考虑replicate和分布式事务的问题

      因为使用了RDMA,所以考虑使用乐观并发控制与其兼容。

  • 乐观并发控制

    没有锁也能访问数据,但是不能直接存储数据,需要先缓存,事务提交时需要先验证,通过后才能提交,否则需要终止该事务。

    FaRM可以直接使用RDMA访问数据,读取速度非常快。读取之后则按照下述步骤尝试提交:

    1.事务协调器(客户端)将LOCK记录发送给需要写入新数据的分片的primary,该请求主要获得对应服务器上所有需要修改的数据的锁。如果不能全部获得,则需要终止事务。这里如果这些分片的数据发生了变化,那么在获取锁的时候会发现版本发生了变化,导致无法获得锁、

    2.事务协调器通过one-sided read进行验证只读(不验证读写事务,因为已经获得了锁)的事务的分片,检查它的数据是否被修改(上锁也算被修改,或者被修改数据)。(这一步就是相当于从对只读上锁变成了利用RDMA进行验证)

    3.事务协调器向所有的backup追加commit日志,等待ack回复

    4.事务协调器向每个primary的日志中追加日志条目,然后等待RDMA收到确认消息。primary收到消息则更新对应内容,并且释放锁。

    5.等所有的primary回复YES后,那么事务协调器发送TRUNCATE让所有的primary丢掉所有这个事务相关的日志条目。backup此时也会更新自己的数据。

Spark


mapReduce的进化版,类似于多个独立的MapReduce共同完成,运用了一种简单的编程方式完成了大数据的计算,适合对海量数据进行批处理,适合离线处理,不适合处理数据流。

窄依赖:各个worker相互独立运行

宽依赖:需要各个worker相互交互

为了防止一个worker出错,而且该worker的计算过程需要和其他worker交互得到,这样直接重新执行得到结果的代价很大(需要重新执行与之交互的worker)

Spark设置了checkpoint(检查点),每次执行transformation(信息交互,宽依赖的步骤之前进行的数据传递)前都需要把各个worker的数据存储在HDFS中

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值