谈谈Spanner和F1

前言

本文不是一篇Spanner的介绍文章,主要想对于Spanner和F1解决的几个有表明性的问题作一个归纳和梳理。接下来的行文安排将主要以问答的形式展开。对Spanner和F1不熟悉的盆友能够参考最后一节列出的引用。html

Spanner概览

Spanner作到了什么

  • 严格的CP系统以及远超5个9的可用性git

  • 基于2PC协议的内部顺序一致性github

  • 外部一致性,支持读写事务、只读事务、快照读算法

  • 全球化的分布式存储系统,延迟是可以接受的数据库

  • 基于模式化的半关系表的数据模型缓存

Spanner的数据模型

directory是什么

在一系列键值映射的上层,Spanner 实现支持一个被称为“目录”的桶抽象,也就是包含公共前缀的连续键的集合。一个目录是数据放置的基本单元。属于一个目录的全部数据,都具备相同的副本配置。 当数据在不一样的 Paxos 组之间进行移动时,会一个目录一个目录地转移。安全

directory和tablet的关联

一个 Paxos 组能够包含多个目录,这意味着一个 Spanner tablet 是不一样于一个 BigTable tablet 的。一个 Spanner tablet 没有必要是一个行空间内按照词典顺序连续的分区,相反,它能够是行空间内的多个分区。这样作可让多个被频繁一块儿访问的目录被整合到一块儿,也就是说多个directory会被映射到一个tablet上。服务器

Spanner中的表

每一个表都和关系数据库表相似,具有行、列和版本值。每一个表都需 要有包含一个或多个主键列的排序集合。主键造成了一个行的名称,每一个表都定义了从主键列到非主键列的映射。当一个行存在时,必需要求已经给行的一些键定义了一些值(即便是 NULL)。网络

如何描述表之间的关联

Spanner中的表具备层次结构,这有些相似于传统关系数据库中一对多关系。户端应用会使用 INTERLEAVE IN 语句在数据库模式中声明这个层次结构。这个层次结构上面的表,是一个目录表。目录表中的每行都具备键 K,和子孙表中的全部以 K 开始(以字典顺序排序)的行一块儿,构成了一个目录。这样作是想把想关联的表放在一样的位置,利用数据的局部性提升性能,毕竟单个Paxos组内的操做比跨paxos组要来的高效。架构

Spanner的软件架构

universe和zone的概念

一个 Spanner 部署称为一个 universe。假设 Spanner 在全球范围内管理数据,那么,将会只有可数的、运行中的 universe。咱们当前正在运行一个测试用的 universe,一个部署/线上用的 universe 和一个只用于线上应用的 universe。

Spanner 被组织成许多个 zone 的集合,每一个 zone 都大概像一个 BigTable 服务器的部署。 zone 是管理部署的基本单元。zone 的集合也是数据能够被复制到的位置的集合。当新的数据中心加入服务,或者老的数据中心被关闭时,zone 能够被加入到一个运行的系统中,或者从中移除。zone 也是物理隔离的单元,在一个数据中心中,可能有一个或者多个 zone, 例如,当属于不一样应用的数据必须被分区存储到同一个数据中心的不一样服务器集合中时,一个数据中心就会有多个 zone 。

sapnner与paxos

spanner数据的实际存储依靠于分布式文件系统Colossus,在一个存储分区分区单元tablet上运行一个Paxos状态机,基于Paxos实现备份和容错。咱们的 Paxos 实现支持长寿命的领导者(采用基于时间的领导者租约),时间一般在 0 到 10 秒之间。当前的 Spanner 实现中,会对每一个 Paxos 写操做进行两次记录:一次是写入到 tablet 日志中,一次是写入到 Paxos 日志中。

Paxos的领导者租约

Spanner 的 Paxos 实现中使用了时间化的租约,来实现长时间的领导者地位(默认是 10秒)。一个潜在的领导者会发起请求,请求时间化的租约投票,在收到指定数量的投票后,这个领导者就能够肯定本身拥有了一个租约。一个副本在成功完成一个写操做后,会隐式地延期本身的租约。对于一个领导者而言,若是它的租约快要到期了,就要显示地请求租约延期。另外一个领导者的租约有个时间区间,这个时间区间的起点就是这个领导者得到指定数量的投票那一刻,时间区间的终点就是这个领导者失去指定数量的投票的那一刻(由于有些投 票已通过期了)。Spanner 依赖于下面这些“不连贯性”:对于每一个 Paxos组,每一个Paxos领导者的租约时间区间,是和其余领导者的时间区间彻底隔离的。

为了保持这种彼此隔离的不连贯性,Spanner 会对何时退位作出限制。把 smax 定义为一个领导者可使用的最大的时间戳。在退位以前,一个领导者必须等到 TT.after(smax)是真。

上面是论文中关于领导着租约的解释,这里咱们的问题是为何须要一个长期的leader lease,不加入这个特性是否能够?下面是我我的的一些见解:

  • Multi-Paxos协议自己在只有一个proposer的时候,对于每一个instance能够将两阶段变为一阶段,提升Paxos协议的效率。固然这只是微小的一方面,毕竟谷歌使用的Paxos实现应该是标准的变体。

  • Leader变更频率降低有助于减小在操做中查询leader的次数,有助于减轻元信息管理服务的压力

  • 这种作法的前提一定是故障的频率低,若是故障时常发生,一个较长的租约时间将使得故障的发现和处理变慢

Spanner的并发控制

True Time和外部一致性

外部一致性保证了什么

若是一个事务 T2 在事务 T1 提交之后开始执行, 那么,事务 T2 的时间戳必定比事务 T1 的时间戳大。

True Time 提供了什么

TrueTime 会显式地把时间表达成 TTinterval,这是一个时间区间,具备有界限的时间不肯定性。表示一个事件 e 的绝对时间,能够利用函数 tabs(e)。若是用更加形式化的术语,TrueTime 能够保证,对于一个调用 tt=TT.now(),有 tt.earliest≤tabs(enow)≤tt.latest,其中, enow 是调用的事件。

这里须要再问一个问题:

每一个节点在同一时刻调用TT.now()等到的区间是相同的吗?显然不是,若是那样不就等同于获得一个全球相同的时间点了,但这不影响外部一致性正确的证实。

如何基于True Time提供外部一致性

咱们先来说作法:是基于时间戳分配和commit wait实现的。

Start. 为一个事务 Ti 担任协调者的领导者分配一个提交时间戳 si,不会小于 TT.now().latest 的值,TT.now().latest的值是在esierver事件以后计算获得的。要注意,担任参与者的领导者, 在这里不起做用。第 4.2.1 节描述了这些担任参与者的领导者是如何参与下一条规则的实现的。

Commit Wait. 担任协调者的领导者,必须确保客户端不能看到任何被 Ti 提交的数据,直到 TT.after(si)为真。提交等待,就是要确保 si 会比 Ti 的绝对提交时间小。

那么证实以下:

读事务

读事务能够分为只读事务读当前最新的值,以及快照读。前者能够经过分配时间戳转变为后者,因此咱们先讨论快照读的实现。

快照读的实现

首先问一个问题,读必须走Paxos Group 的Leader吗?不是的,首先在同一个Paxos Group内,写操做时间戳的单调性是毫无疑问的,那么只须要找到足够新的副本。这就有一个新问题:

如何判断一个副本是否足够新?

每一个副本都会跟踪记录一个值,这个值被称为安全时间 tsafe,它是一个副本最近更新后的最大时间戳。若是一个读操做的时间戳是 t,当知足 t<=tsafe 时, 这个副本就能够被这个读操做读取。这种情形下小于t的写操做一定已经被该副本catch up了(基于Paxos协议)。

如何维护tsafe

这一小节内容请先参阅读写事务部分。

tsafe能够从两个值推导而来,Paxos状态机最大的apply操做的时间戳(这一部分象征着已经生效的写),和事务管理器决定的tasfe(TM),tsafe=min(tsafe(Paxos),tsafe(TM)),后者是什么意思呢?

若是如今该replica没有参与任何事务中,那么理应这一部分不形成任何影响,因此tsafe(TM)=无穷大。(记住tsafe越大,读越容易经过)。若是当前replica参与了一个读写事务Ti,那么本Paxos组的leader会分配一个准备时间戳(见后文读写事务),那么理应tsafe比全部正在参与的事务Ti的准备时间戳都小,而且是知足这个条件时间戳里的最大一个。答案呼之欲出了。

可是,上述的方法依然存在问题,一个未完成的事务将阻止tsafe增加,以后的以有读事务将被阻塞,即便它读的值和该事务写的值没啥关系。这里有点相似于Java对ConcurrentHashMap作的优化了——分段加锁,好了,咱们继续接着往下看。咱们能够对一个范围的key range来维护参与事务的准备时间戳,那么对于一个读操做,只用检查和它冲突的key range的准备时间戳就好。

tsafe(Paxos)也有一个问题,那就是缺少Paxos写的时候,也无法增加。若是上次的apply时间在读被分配的时间戳t以前,接下来没有Paxos写的话,快照读t无法执行,这就尴尬了。能够为每个instance n预估一个将分配给n+1的时间戳。那这里要有一个问题了,预估的这个时间须要知足什么条件呢?我的认为首先是要超过期间戳的最大偏差值。

如何为只读事务分配时间戳?

以前提到过对于只读事务能够经过分配时间戳来转化为快照读,那么该如何分配呢,首先看这种分配须要知足什么?

  • 知足写后读一致性,也就是其分配的时间戳要大于全部已提交事务的时间戳

在一个事务开始后的任意时刻,能够简单地分配 sread=TT.now().latest。但这样有一个问题,对于时间戳 sread 而言,若是 tsafe 没有增长到足够大,可能须要对 sread 时刻的读操做进行阻塞。择一个 sread 的值可 能也会增长 smax 的值,从而保证不连贯性。为了减小阻塞的几率,_Spanner 应该分配能够保持外部一致性的最老(小)的时间戳_。

分配一个时间戳须要一个协商阶段,这个协商发生在全部参与到该读操做中的 Paxos 组之间。其过程以下:

  1. 对于单个Paxos组内的读操做,把 LastTS()定义为在 Paxos 组中最后提交的写操做的时间戳。若是没有准备提交的事务,这个分配到的时间戳 sread=LastTS()就很容易知足外部一致性要求

  2. 对于跨Paxos组的读操做,最复杂的作法是基于多Paxos Leader的LastTS()协商得出;但实际采用的简单作法是采用TT.now().latest,容许某种程度上的阻塞。

写事务

不管事务读不读,都按读写事务来处理

单Paxos组内的读写事务

这种状况下只须要给读写事务分配时间戳,进行快照读,最后发生在一个事务中的写操做会在客户端进行缓存,直到提交。由此致使的结果是,在一个事务中的读操做,不会看到这个事务的写操做的结果。这种设计在 Spanner 中能够很好地工做,由于一个读操做能够返回任何数据读的时间戳,未提交的写操做尚未被分配时间戳。

至于读写事务如何分配时间戳,请参照True Time一章。

多Paxos组的读写事务

  1. 客户端对须要读的Paxos组Leader申请加读锁,按分配时间戳读取最新数据。

  2. 客户端向leader持续发送“保持活跃信息“,防止leader认为会话过期

  3. 读操做结束,缓冲全部写操做,开始2PC,选择协调组发送缓冲信息

  4. 对于参与协调的非领导者而言,获取写锁,会选择一个比以前分配给其余事务的任什么时候间戳都要大的预备时间戳,而且经过 Paxos 把准备提交记录写入日志。而后把本身的准备时间戳通知给协调者。

  5. 对于协调者,也会首先得到写锁,可是,会跳过准备阶段。在从全部其余的、扮演参与者的领导者那里得到信息后,它就会为整个事务选择一个时间戳。这个提交时间戳s必须大于或等于全部的准备时间戳,而且s应该大于这个领导者为以前的其余全部事务分配的时间戳,以后就会经过 Paxos 在日志中写入一个提交记录。

  6. 在容许任何协调者副本去提交记录以前,扮演协调者的领导者会一直等待到 TT.after(s),知足提交等待条件。

  7. 在提交等待以后,协调者就会发送一个提交时间戳给客户端和全部其余参与的领导者。每一个参与的领导者会经过 Paxos 把事务结果写入日志。全部的参与者会在同一个时间戳进行提交,而后释放锁。

注意,这里的每一个协调者其实都是一个Paxos组,2PC自己是一个反可用性的协议(也就是它要求没有故障才能顺利完成),可是Paxos协议可以在协调者故障时快速选出新主,因为信息已经经过Paxos日志同步了,新主能够继续参与2PC过程,提高了可用性。

模式变动事务

模式变动的难点是什么?

原子模式变动。使用一个标准的事务是不可行的,由于参与者的数量(即数据库中组的数量)可能达到几百万个。在一个数据中心内进行原子模式变动,这个操做会阻塞全部其余操做。

Spanner的作法

一个 Spanner 模式变动事务一般是一个标准事务的、非阻塞的变种。首先,它会显式地分配一个将来的时间戳,这个时间戳会在准备阶段进行注册。由此,跨越几千个服务器的模式变动,能够在不打扰其余并发活动的前提下完成。其次,读操做和写操做,它们都是隐式地依赖于模式,它们都会和任何注册的模式变动时间戳t保持同步:当它们的时间戳小于 t 时, 读写操做就执行到时刻 t;当它们的时间戳大于时刻 t 时,读写操做就必须阻塞,在模式变动事务后面进行等待。

那么,接下来又有几个问题:

  • 如何为模式变动选择时间戳?选择的时间戳须要知足哪些条件?

  • 上述的方法会形成服务不可用吗?若是是,不可用的时间区间是多少?

  • 两次模式变动之间在重合的时间段里有重合组的时候,经过什么方式协调?乐观仍是悲观的并发控制?会形成死锁吗,会致使在分配的时间戳以后没法按时完成吗?仍是说在准备阶段就侦测这种状况,阻塞下一次模式变动?

关于模式变动的细节在spanner的论文中并无详细说起,而是在另外一篇文章中阐述,若是以后有时间的话会单独就模式变动再写一篇博客来回答这些问题。

Sapnner和CAP原理的讨论

CAP原理存在的误导

“三选二”的观点在几个方面起了误导做用,

  • 首先,因为分区不多发生,那么在系统不存在分区的状况下没什么理由牺牲C或A。

  • 其次,C与A之间的取舍能够在同一系统内以很是细小的粒度反复发生,而每一次的决策可能由于具体的操做,乃至由于牵涉到特定的数据或用户而有所不一样。

  • 最后,这三种性质均可以在程度上衡量,并非非黑即白的有或无。可用性显然是在0%到100%之间连续变化的,一致性分不少级别,连分区也能够细分为不一样含义,如系统内的不一样部分对因而否存在分区能够有不同的认知。

CAP理论的经典解释,是忽略网络延迟的,但在实际中延迟和分区紧密相关。CAP从理论变为现实的场景发生在操做的间歇,系统须要在这段时间内作出关于分区的一个重要决定:

  • 取消操做于是下降系统的可用性,仍是

  • 继续操做,以冒险损失系统一致性为代价

依靠屡次尝试通讯的方法来达到一致性,好比Paxos算法或者两阶段事务提交,仅仅是推迟了决策的时间。系统终究要作一个决定;无限期地尝试下去,自己就是选择一致性牺牲可用性的表现。

CAP理论常常在不一样方面被人误解,对于可用性和一致性的做用范围的误解尤其严重,可能形成不但愿看到的结果。若是用户根本获取不到服务,那么其实谈不上C和A之间作取舍,除非把一部分服务放在客户端上运行。

“三选二”的时候取CA而舍P是否合理?已经有研究者指出了其中的要害——怎样才算“舍P”含义并不明确。设计师能够选择不要分区吗?哪怕原来选了CA,当分区出现的时候,你也只能回头从新在C和A之间再选一次。咱们最好从几率的角度去理解:选择CA意味着咱们假定,分区出现的可能性要比其余的系统性错误(如天然灾难、并发故障)低不少,打个比方你在单机下就永远不会假设分区,同一机架内部的通讯有时咱们也认为分区不会出现。

Spanner声称同时达到CA

纯粹主义的答案是“否”,由于网络分区老是可能发生,事实上在Google也确实发生过。在网络分区时,Spanner选择C而放弃了A。所以从技术上来讲,它是一个CP系统。咱们下面探讨网络分区的影响。考虑到始终提供一致性(C),Spanner声称为CA的真正问题是,它的核心用户是否定可它的可用性(A)。若是实际可用性足够高,用户能够忽略运行中断,则Spanner是能够声称达到了“有效CA”的。

实际上,c差别化可用性与spanner的实际可用性是有差距的,即用户是否确实已经发现Spanner已停掉了。差别化可用性比Spanner的实际可用性还要高,也就是说,Spanner的短暂不可用不必定会当即形成用户的系统不可用。

另外就是运行中断是否因为网络分区形成的。若是Spanner运行中断的主要缘由不是网络分区,那么声称CA就更充分了。对于Spanner,这意味着可用性中断的发生,实际并不是是因为网络分区,而是一些其它的多种故障(由于单一故障不会形成可用性中断)。

综上,Spanner在技术上是个CP系统,但实际效果上能够说其是CA的。

分区发生时会如何

上面已经提到过了,对于分布式系统的节点来讲,它很难感知分区,它所能感知的只有延时。那么这里有几个重要问题:

  • 如何判断分区

  • 分区问题的主要来源

  • spanner在面对分区的时候作出了什么样的选择

首先,考虑单Paxos组内事务,出现分区后会出现两种情形:

  • 大多数成员可用,选出了新Leader,事务继续运行。但处于少数人的一侧将再也没法更新它的tsafe了,在某个时间戳以后的读操做没法在该分区被服务,牺牲了部分可用性,但数据在多数人中保持一致。

  • 没法维持一个多数人群体,那么事务将暂停,新的事务不会被接受,系统不可用

再考虑跨Paxos组的事务

对跨组事务使用2PC还意味着组内成员的网络分区能够阻止提交。舍弃可用性保证系统数据是安全的。

对于快照读,快照读对网络分区而言更加健壮。特别的,快照读能在如下状况下正常工做:

  • 对于发起读操做的一侧网络分区,每一个组至少存在一个副本

  • 对于这些副本,读时间戳是过去的。

若是Leader因为网络分区而暂停(这可能一直持续到网络分区结束),这时第2种状况可能就不成立了。由于这一侧的网络分区上可能没法选出新的Leader(译注:见下节引用的解释)。在网络分区期间,时间戳在分区开始以前的读操做极可能在分区的两侧都能成功,由于任何可达的副本有要读取的数据就足够了。

F1概览

F1是基于Spanner之上的一个分布式类关系数据库,提供了一套相似于SQL(准确说是SQL的超集)的查询语句,支持表定义和数据库事务,同时兼具强大的可扩展性、高可用性、外部事务一致性。接下来主要从几个方面来简要说一说F1是怎么在Spanner之上解决传统数据库的诸多问题的。

F1的基本架构

F1自己不负责数据的存储,只是做为中间层预处理数据并解析SQL生成实际的读写任务。咱们知道,大多数时候移动数据要比移动计算昂贵的多,F1节点自身不负责数据的底层读写,那么节点的加入和移除还有负载均衡就变得廉价了。下面放一张F1的结构图:

F1架构图

大部分的F1是无状态的,意味着一个客户端能够发送不一样请求到不一样F1 server,只有一种情况例外:客户端的事务使用了悲观锁,这样就不能分散请求了,只能在这台F1 server处理剩余的事务。

F1的数据模型

F1支持层级表结构和protobuf复合数据域,示例以下:

F1数据模式示例

这样作的好处主要是:

  • 能够并行化,是由于在子表中能够get到父表主键,对于不少查询能够并行化操做,不用先查父表再查子表

  • 数据局部性,减小跨Paxos组的事务.update通常都有where 字段=XX这样的条件,在层级存储方式下相同row值的都在一个directory里

  • protobuf支持重复字段,这样也是为了对于array一类的结构在取数据时提高性能

最后,对于索引:

全部索引在F1里都是单独用个表存起来的,并且都为复合索引,由于除了被索引字段,被索引表的主键也必须一并包含.除了对常见数据类型的字段索引,也支持对Buffer Protocol里的字段进行索引.

索引分两种类型:

  • Local:包含root row主键的索引为local索引,由于索引和root row在同一个directory里;同时,这些索引文件也和被索引row放在同一个spanserver里,因此索引更新的效率会比较高.

  • global:同理可推global索引不包含root row,也不和被索引row在同一个spanserver里.这种索引通常被shard在多个spanserver上;当有事务须要更新一行数据时,由于索引的分布式,必需要2PC了.当须要更新不少行时,就是个灾难了,每插入一行都须要更新可能分布在多台机器上的索引,开销很大;因此建议插入行数少许屡次.

F1的模式变动

模式变动的难点

同步的模式变动可行吗?

显然是不行的,这违反了咱们对可用性的追求。

然而在线的、异步的模式变动会形成哪些问题呢?

全部F1服务器的Schema变动是没法同步的,也就是说不一样的F1服务器会在不一样的时间点切换至新Schema。因为全部的F1服务器共享同一个kv存储引擎,Schema的异步更新可能形成严重的数据错乱。例如咱们发起给一次添加索引的变动,更新后的节点会很负责地在添加一行数据的同时写入一条索引,随后另外一个还没来得及更新的节点收到了删除同一行数据的请求,这个节点还彻底不知道索引的存在,天然也不会去删除索引了,因而错误的索引就被遗留在数据库中。

算法的基本思想

在F1 Schema变动的过程当中,因为数据库自己的复杂性,有些变动没法由一个中间状态隔离,咱们须要设计多个逐步递进的状态来进行演化。只要咱们保证任意相邻两个状态是相互兼容的,整个演化的过程就是可依赖的。

算法的实现

F1中Schema以特殊的kv对存储于Spanner中,同时每一个F1服务器在运行过程当中自身也维护一份拷贝。为了保证同一时刻最多只有2份Schema生效,F1约定了长度为数分钟的Schema租约,全部F1服务器在租约到期后都要从新加载Schema。若是节点没法从新完成续租,它将会自动终止服务并等待被集群管理设施重启。

定义的中间状态:

  • delete-only 指的是Schema元素的存在性只对删除操做可见。

  • write-only 指的是Schema元素对写操做可见,对读操做不可见。

  • reorg:取到当前时刻的snapshot,为每条数据补写对应的索引

演化过程:

absent --> delete only --> write only --(reorg)--> public

F1的事务支持

F1支持三种事务

  • 快照事务

  • 悲观事务

  • 乐观事务

前面两类都是Spanner直接支持的,这里主要讲讲乐观事务,分为两阶段:第一阶段是读,无锁,可持续任意长时间;第二阶段是写,持续很短.但在写阶段可能会有row记录值冲突(可能多个事务会写同一行),为此,每行都有个隐藏的lock列,包含最后修改的timestamp.任何事务只要commit,都会更新这个lock列,发出请求的客户端收集这些timestamp并发送给F1 Server,一旦F1 Server发现更新的timestamp与本身事务冲突,就会中断自己的事务。

其优点主要以下:

  • 在乐观事务下能长时间运行而不被超时机制中断,也不会影响其余客户端

  • 乐观事务的状态值都在client端,即便F1 Server处理事务失败了,client也能很好转移到另外一台F1 Server继续运行.

  • 一个悲观事务一旦失败从新开始也须要上层业务逻辑从新处理,而乐观事务自包含的--即便失败了重来一次对客户端也是透明的.

但在高并发的情景下,冲突变得常见,乐观事务的吞吐率将变得很低。

F1的查询处理

F1的查询处理有点相似于Storm一类流式计算系统,先生成能够由有向无环图表示的执行计划,下面给出一个示例:

F1查询处理示意图

F1的运算都在内存中执行,再加上管道的运用,没有中间数据会存放在磁盘上,但缺点也是全部内存数据库都有的--一个节点挂就会致使整个SQL失败要从新再来.F1的实际运行经验代表执行时间不远超过1小时的SQL通常足够稳定。

F1自己不存储数据,由Spanner远程提供给它,因此网络和磁盘就影响重大了;为了减小网络延迟,F1使用批处理和管道技术,同时还有一些优化手段:

  • 对于层级表之间的join,一次性取出全部知足条件的行

  • 支持客户端多进程并发接受数据,每一个进程都会收到本身的那部分结果,为避免多接受或少接受,会有一个endpoint标示.

  • 对protobuf数据的处理,即便是只取部分字段,也必须取整个对象并解析,这也是为了换取减小子表开销作出的权衡。

参考文章