foundation部分学习记录(更正更新中……)

foundation部分学习记录(更新中……)

从FDB的角度看,它对上层只提供有序+事务+KV存储的抽象。

设计原则

  • 模块化分割,尽量细分且模块之间相互解耦

例如事务系统内,其提交(write path)和读取(read path)是可以独立扩展的,系统中根据事务功能的不同,区分了很多角色(timestamp管理,冲突检测,提交的协调,logging..),每个角色也可以独立配置和扩展。而全局来看也把各个功能尽量拆分开来,减少耦合。

  • 快速failure快速恢复

与一般的数据库系统不同,它处理各种类型的failure方式都一致,就是发现failure后重启整个事务系统,通过recovery机制来修复failure,因此必须做到快速检测和快速恢复才行。生产环境中从出现问题->发现->主动shutdown->recovery,一般在5s以内。

  • 确定性模拟测试系统

提升质量,使得bug可被reproduce。

对外接口是典型的key/value(get/set/getrange..),事务机制是典型的OCC,开始时基于系统的快照做读取+修改,所有修改在client本地缓存,结束时带着read set/write set发起提交。由于需要缓存修改,系统对于Key/Value/事务的大小都是有限制的,这和client的缓存+in-memory的事务管理系统的缓存机制有关。
image-20230216002741793

如上图所示,FDB 的架构中规中矩,大的模块可以分成三部分:

  • 客户端接口 Client TTransaction
  • 控制平面 Control Plane
  • 数据平面 Data Plane

论文重点介绍的是 Control Plane 和 Data Plane。

Control Plane

Control Plane 负责管理集群的元数据,使用 Active Disk Paxos 来保证高可用。Control Plane 分成以下几个部分:

  • Coordinator

几个 coordinator 进程组成一个 paxos group,其中有一个 leader,称为 cluster controller。

Cluster controller 负责故障检测,管理各种进程的角色,汇集、传递整个集群的各种信息。同时,cluster controller 是整个集群访问的入口点,controller会创建另外几个单实例进程。Client 通过一个保存有 coordinator 的 IP:Port 的配置文件访问集群,并从 cluster controller 获取最新的 proxy 列表。

  • DataDistributor

DataDistributor 负责监控 StorageServer 的故障情况和数据的平衡调度。

  • Ratekeeper

Ratekeeper 通过控制单调递增时间戳的分配速度来进行过载保护。

Data Plane

Data Plane 大体上可以划分成三大部分:

  • Transaction System 负责实现 serializable snapshot isolation 级别的分布式事务,整个TS层是无状态的,便于发生failure时,快速整体重启。
  • Log System 负责日志的复制,保证系统的高可用。
  • Storage System 保存实际数据,或者说状态机,会从 Log System 异步拉取日志进行 apply。目前单机存储引擎是使用一个修改过的 SQLite。

其中事务管理系统(TS)负责写路径,storage系统(SS)负责读路径,是解耦的,可以各自独立扩展。

Transaction System 较为复杂,大体上可以分层三个模块:

  • Proxy 作为事务系统面向 client 的代理接口,事务请求都需要通过 proxy 获取 read version,获取 key ranges 对应的 storage server 的信息,提交事务。
  • Sequencer 负责分配递增的 read version 和 commit version。
  • Resolver 负责 SSI 级别的事务冲突检测。

从上面的架构图可以看到,读写路径是分离的,TS层+LS层和write path相关,而SS层和read path相关。这是其设计的核心思想,将功能尽可能细分为不同role,由不同服务进程负责,不同role各自独立配置和扩展。例如如果想提高读吞吐,扩展storage server,如果想提高写吞吐,扩展Resolver/proxy/LS。

日志复制

日志复制是每个分布式数据库实现高可用所必须的。

FDB 有两种数据需要复制:Control Plane 的元数据和 Data Plane 的用户数据。

Control Plane 的元数据日志复制使用了 Active Disk Paxos(Paxos 的一个变种)。基于 Paxos 复制日志实现系统的高可用是常规的业界做法,也是不依赖外部仲裁实现高可用、强一致的标准方法。

Data Plane 采用了同步复制的方式——每次复制给 f + 1 个节点,只有 f +1 个节点都成功才返回成功。这种方式只需要 f + 1 个副本就可以容忍 f 个副本丢失,而采用 Paxos/Raft 进行复制的话,需要 2 * f + 1 个副本。

看起来,同步日志复制的成本比采用 Paxos/Raft 的方式要低许多,但是这里 Log System 的选主和故障恢复、故障转移应该是要严重依赖 cluster controller 的仲裁。并且任意一个副本故障都会导致写失败,如果采用 Paxos/Raft 的方式,只有 leader 故障才会导致写失败。另外,这里的 membership change 是怎么做的?这个估计要看看代码才清楚。这其中还有什么坑呢?估计要深度使用过后才清楚。

事务

Data Plane 的分布式事务是为典型的 OLTP 场景设计的——水平扩展、读多写少、小事务(5 秒)。FDB 通过 OCC +MVCC 来实现 SSI 的事务隔离级别。

一个事务的基本流程大概如下:

  1. Client -> proxy -> sequencer 获取 read version。

  2. Client with read version -> storage 根据 read version 读取数据快照。

  3. 写请求在提交前会先缓存在本地。

  4. 事务提交:

    1. Client 发送读集合和写集合给 proxy。
    2. Proxy 从 sequencer 获取 commit version。
    3. Proxy 将读集合和写集合发送给 resolver 进行事务冲突检测。
    4. 如果事务冲突,则 abort 掉。否则 proxy 发送写集合给 log server 进行持久化,完成事务的提交。

    img

事务的执行流程,这里有几个关键的问题。

  1. 如何决定 read version?
  2. 如何进行事务冲突检测,以满足 SSI?
  3. 如何从故障中恢复?

如何决定 read version?

按照 SSI 事务的要求,通过 read version,必须能读到所有 commit version 小于等于 read version 的事务。对于已经提交的事务,这个问题不大。主要问题在于并发事务。

举个例子:

事务 A 和事务 B 是并发事务,A 获取 read vesion,准备读取数据;B 获取 commit version,准备提交事务。

如果 read version < commit version,事务 A 无论如何无法读到事务 B 的数据,所以没问题。

如果 read version >= commit version,事务 A 需要能够读取到事务 B 的数据,但是此时事务 B 可能还没提交…

如何解决这个问题呢?比较传统的做法是,commit 之前先对数据上锁,表示有 pending(待定) 的事务。读取的时候,需要检查数据有没有上锁,如果数据已经上锁,说明有事务正在写,则等待事务执行、或者推进事务 commit/abort、或者 abort 当前事务。

FDB 的做法是:从所有 proxy 收集最新最大的 commit version 作为 read version。而由于 commit version 的分配是全局单调递增的,所以可以保证,并发事务的 commit version 肯定大于 read version。

虽然从 proxy 收集 commit version 在实现上很容易做 batch 优化来提升性能,但是每次需要访问所有的 proxy,我对其扩展性和可用性保持怀疑。

如何从故障中恢复?

known committed version (KCV)

由于redo log apply是在后台持续进行的,因此本质上它将redo apply从recovery中解耦出来,等于持续在checkpointing,在recovery期间不需要做redo/undo apply,只是确认当前的log序列需要恢复到哪个位置即可!!后续基于log -> data的过程仍然是异步。这保证了recovery的速度。

具体流程:发现failure后,老Sequencer退出,新Sequencer启动,并从Coordinator获取老的TS的配置信息,包括老的LS的位置等,同时给Coordinator加个全局锁,避免并发recovery,然后关闭老的LS,禁止接收新的log写入,开始恢复,恢复完成后启动TS系统接收用户请求。

Proxy和Resolver都是stateless的,直接重启就可以,只有LogServer有log信息,恢复如下:
img

FDB 事务系统的核心进程是:Sequencer、Proxy、Resolver、LogServer,其中 Sequencer 是事务系统的“主控”:

  • 如果 Sequencer 挂掉了,会有一个新的 Seqencer 被重新拉起来。
  • 如果其它进程挂掉了,Sequencer 会自杀,然后会有一个新的 Seqencer 被重新拉起来。

新的 Sequencer 从 Coordinators 获取旧的事务系统的信息,包括所有的 Proxy、Resolver、LogServer,阻止它们处理新的事务。然后,重新组建新的事务系统(Proxy、Resolver、LogServer)。

事务系统恢复的时候,主要是要确定 LogServer 已经提交的最新的 log 的位置,论文中叫 Recovery Version,这个位置之前(包括这个位置)的日志已经提交,这个位置之后的日志可以直接忽略掉。

FDB 这种同步复制的方式,故障恢复其实很简单: 收集至少 m-k+1 个 LogServer 的最大持久化 LSN,然后取其中的最小即可。

  • m 是 LogServer 的总数
  • k 是日志同步复制的副本数。

FDB 的实现比上面这个方式稍微复杂一点,不过原理上是类似的。

FDB 只提供 get()、set()、getRange()、clear()、commit() 这几个简单的接口。FDB 认为自己实现了一个数据库的底层("lower half")—— 一个支持事务的分布式 KV。其他任何数据模型,比如关系模型、文档模型,都可以在这之上通过一层无状态的服务来实现。

基于一个单纯的分布式 KV 实现所有数据模型,这是一个很理想的架构,但现实可能没想象的好(主要是性能问题)。对这个话题感兴趣的,可以看看这篇文章:FOUNDATIONDB'S LESSON: A FAST KEY-VALUE STORE IS NOT ENOUGH。

FDB 给 C++ 做了扩展,增加了一些关键字用于“原生”支持异步编程,这套东西叫做 Flow。

工作流程:

可以看到FDB内部各个组件是有着相互的关联的:

Coordinator中存储着系统核心metadata,包括LS Server的配置,LS Server中则存储了Storage Server的配置。

运行中,Controller监控Sequencer,Sequencer监控Proxy / Resolver / LogSevers的状态。

bootstrap

系统启动时,Coordinator会选举出Controller,后者启动Sequencer,Sequencer则启动另外3组进程,然后从Coordinator中获取老的LS的配置,并从老的LS中获取SS的配置,利用老的LS执行必要的recovery过程,完成后老的TS系统就可以退休了,新的LS的信息写入Coordinator中,系统完成启动,开始对外提供服务。

reconfiguration

当TS系统发生failure或者配置变化时,Sequencer检测到后会主动shutdown,Controller检测到后会重启新的Sequencer从而形成新的TS,新的Sequencer会阻止老的TS再提供服务,然后走和bootstrap类似的recovery流程即可。

为了标识不同的TS系统,引入了epoch的概念,任何时候新老TS交替,epoch就要+1。

posted @ 2023-02-16 00:36  O_fly_O  阅读(29)  评论(0编辑  收藏  举报