Spanner是Google支持高可用的全球关系型数据库[CDE+12]。Spanner可以管理大规模的复制数据,这种大规模不仅体现数据量方面,还体现在事务数量方面。 Spanner为写入其中的每条数据分配全局一致的“真时”时间戳,客户端可以在整个数据库上无需锁定的执行全局一致性读取。
CAP理论[Bre12]指出,在C,A,P三个期望的属性中,你只能选择两个:
- C:一致性,本文中我们可以认为是可串行性;
- A:可用性,指读取和更新的100%可用;
- P:网络分区,指对网络分片的容忍。
基于CAP理论,选择要抛弃的字母,将会得到三种系统:CA,CP和AP。需要注意的是,你不是必须选择3个属性中的2个,许多系统具有零个或一个CAP属性。
对于“全球规模”的分布式系统,分区通常被认为是不可避免的——尽管这并不是共识[BK14]。一旦你认为分区是不可避免的,任何分布式系统都必须准备放弃一致性(AP)或放弃可用性(CP),这不是任何人都想要的选择结果。实际上,CAP理论的出发点是让设计者认真对待这种权衡。这里给出两个重要的警告:首先,在真正发生分区的时候,你只需要丧失系统的部分功能而不是全部——当然你必须付出一些代价[Bre12]。其次,CAP理论申明的是100%可用性,而本文讨论的有趣之处是真实生产环境中涉及可用性本身的折中和权衡。
Spanner是CA系统
作为一个全球规模分布式系统,Spanner声称同时具有一致性和高可用性,这意味着Spanner系统不会发生分区,这很让人怀疑。这是否意味着Spanner就是CAP定义的CA系统?从技术上来说,直接的答案是“否”,但从效果上来说,答案可以是“是”——即从用户的角度,可以认为Spanner是一个CA系统。
纯粹主义者从技术上来看,答案当然是“否”。因为分区确实可能发生,并且实际上在谷歌也发生过。特别是在发生分区的时候,Spanner的策略是选择C而放弃A。所以,技术上Spanner是一个CP系统。下面我们将具体展开对分区的探讨。
考虑到Spanner永远提供一致性,Spanner宣称是CA系统存在的真正问题是用户对可用性的态度——即用户是否会对可用性吹毛求疵。如果Spanner的实际可用性很高,宕机事件对用户来说可以忽略,则Spanner可以证明“实际上是CA系统”声明的正当性。宣称CA系统并不意味着100%的可用性(Spanner没有提供也不会提供),而是类似5个或更多“9”的可用性(1/106的故障或更少)。反过来,数据库可用性的真正石蕊试纸是用户(希望自己的服务高可用)——即用户是否需要编写额外的代码来处理宕机异常。如果用户没有编写额外代码,也就是说用户是假设数据库高可用的。而基于Spanner内部已有的大量用户,我们可以知道用户认为Spanner是高可用数据库。
关于可用性的数据
在我们深入研究Spanner之前,重新回顾Chubby的演进历程是值得的。Chubby也是一个提供一致性和可用性的全球规模(广域网络)系统。原始的Chubby论文[Bur06]提到在700天内出现了30秒或更长时间的9次宕机,其中6次与网络相关(如[BK14]中所讨论的)。这样的可用性低于最好的5个9可用性,现实一点说,如果我们假设每次宕机的平均值为10分钟,这样的可用性低于4个9,更有甚者,如果宕机时间是小时级的,则可用性将会低于3个9。
对于锁定和一致的读/写操作,得益于网络,架构和运维的各种改进,现在全球地理分布的Chubby单元提供99.99958%的平均可用性(30s+的宕机时间)。从2009年开始,由于“卓越”的可用性,Chubby的SRE工程师开始强制定期停机,以确保我们继续了解Chubby对故障的依赖和影响。
在Google内部,Spanner提供与Chubby类似的可用性水平。也就是说,好于5个9。云版本的Spanner具有相同的基础,但添加了一些新的功能,所以在实际生产环境中可能会稍微低一点。
上面的饼图揭示了Spanner事故的内部原因分类。事故可能是一个意外事件,但并非所有事故都是宕机,一些事故可以很容易地掩蔽。图表中的权重是事故出现的频率而不是事故的影响。频率最高的事故类别(User)是用户错误导致的,例如负载过高或者配置错误,并且这些事故大多数只会影响到该特定用户,而其余类别则可能会影响到区域中的所有用户。Cluster类别的事故反映底层基础架构中的非网络问题,包括服务器和电源的问题。Spanner通过使用多副本机制来自动规避这些事件。然而,有时SRE也需要介入以修复损坏的副本。Operator类别的事故是由SRE引起的事故,例如配置错误。Bug类别的事故则是导致一些问题的软件错误。这些事故都可能导致或大或小的宕机,曾经两起最大的宕机事故都是软件错误,并且同时影响到特定数据库的所有副本。其他一麻袋的各种问题大多数只发生一次。
Network类别的事故(8%以下)是由网络分区和网络配置问题造成的。一部分集群节点和另外一部分集群节点分开的情况从来没有发生过,也没有发生过Spanner的法定数节点出现在分区中集群数量较少一边的情况。但我们确实遇到过个别数据中心或区域与其他网络断开的情况。还有就是一些配置错误导致临时预留带宽不足和一些与硬件故障相关的延迟。我们曾经遇到一个问题:其中单方向的通信失败导致一个奇怪的分区发生,然后必须通过关闭一些节点来解决。到目前为止,网络事件没有引起大的宕机事故。
总而言之,要声明“实际上是CA”系统,那么,这个系统必须处于这种相对概率状态:1)系统必须在生产环境中具有非常高的可用性(以便用户可以忽略异常),以及2)系统由于分区引起的宕机故障次数应该处于一个比较低的份额。Spanner满足两者。
网络才是根本
许多人认为,Spanner通过使用真时(TrueTime)可以绕过CAP理论。真时(TrueTime)是一种能够使用全局同步时钟的服务。虽然真时(TrueTime)很棒,却对于实现CA系统没有太大贡献,它的实际价值会在下文讨论。如果说Spanner真有什么特别之处,那就是谷歌的广域网。在多年的运营改进的基础上,在生产环境中可以最大程度的减少分区发生,从而实现高可用性。
首先,Google的系统运行在自己的私有全球网络之上。Spanner不在公共互联网上运行——实际上,Spanner的每个数据包只流过Google控制的路由器和链路(不包括到远程客户端的边缘链路)。此外,每个数据中心通常具有至少三个将其连接到私有全球网络的独立光纤,以确保每对数据中心的路径分集(路径多样性)。同样,在数据中心内部存在设备和路径的冗余。因此,通常灾难性事件,例如切断光缆,不会导致分区或宕机。
因此,带来分区的真正风险不是网络路径断开,而是某种类型的广泛配置或软件升级,这些配置或升级会同时切断多个网络路径。这才是真正的风险,Google一直在努力防止和减轻这种风险。一般的策略是限制任何特定更新的影响范围(“爆炸半径”),以便当我们需要不可避免地推送一个错误的更改时,只影响部分路径或副本,然后我们必须在其他任何更改之前修复这些问题。
虽然Google的基础网络可以大大减少分区的风险,但它不能提高光速。跨越广域网的一致操作具有显著的最小往返时间,这个时间可以达到几十毫秒,当跨越大陆时则更长(1000英里的距离约为5百万英尺,每纳秒½英尺,因此最小值为10毫秒)。为了平衡区域内延迟和容灾的需求,Google定义“区域”的范围只具有2ms往返时间。Spanner通过事务流水线管理来缓解延迟,但这并不能减少单事务延迟。对于读操作来说,由于能够使用全局时间戳和本地副本(如下所述),延迟通常较低。
具有弱一致性的模型具有较低的更新延迟。然而,没有长程往返,弱一致性的模型也有一个较低的持久性窗口,因为在数据复制到另一个区域之前,灾难可以同时摧毁本地和远程多个副本。
网络分区期间发生什么
为了理解分区,我们需要更多地了解Spanner的工作原理。与大多数ACID数据库一样,Spanner使用两阶段提交(2PC)和严格的两阶段锁定,以确保隔离和强一致性。2PC已经被称为“反可用性”协议[Hel16],因为2PC要求所有成员节点必须参与工作(即必须可用)。为了缓解这个问题,Spanner为每个2PC成员节点建立Paxos组,这样就算部分Paxos组成员宕机,2PC的“成员”还是高可用的。同时,数据也被分成组,形成放置和复制的基本单元。
如上所述,一般来说,当发生分区时,Spanner会选择C而放弃A。在实际生产环境中,这是基于以下考虑:
- 使用Paxos组来实现更新的共识。如果领导节点由于分区而不能维持仲裁(法定数),则更新被停止,并且系统不可用(通过CAP定义)。最终,在多数节点的分区,会重新选举新的领导节点。
- 对跨组事务使用2PC意味着分区成员可以阻止事务提交。
分区在生产环境中最可能的结果是,有法定数节点的分区在选举新的领导节点之后继续工作,即服务继续可用,但是少数节点分区一方的用户则无法访问服务。这是差别可用性的一个案例:少数节点分区一方的用户可能有其他重大问题,比如失去连接,或已经宕机。这意味着构建在Spanner之上的多区域服务即使在分区期间也能够良好工作。一些Paxos组完全不可用的可能性也是有的,但是不大。
只要所有已经建立联系的组都具有基于法定数选举的领导节点,并都位于分区的一侧,则Spanner中的“事务”可以工作。这意味着有些事务会完美工作,有些事务会超时,但系统总是一致的。Spanner的实现属性是,任何读取的返回都是一致的,即使事务稍后中止(包括超时等任何原因)。
除了常规事务之外,Spanner还支持快照读取——从过去的特定时间读取。 Spanner随时间维护数据的多个版本,每个版本都有一个时间戳,因此可以使用正确版本准确地回答快照读取。特别地,每个副本知道它被写入的时间(必须的),并且任何副本可以在该时间戳之前单方面地回答读取(除非它太旧了,并且已经被垃圾收集)。类似地,很容易在同一时间跨多个组读取(异步)。快照读取根本不需要锁。事实上,只读事务被实现为在当前时间(在任何最新的副本)的快照上读取。
因此,快照读取使分区更加健壮。特别地,快照读取将会在下列条件下起作用:
- 正在初始化的分区中每个组至少存在一个副本,以及
- 这些副本的时间戳是过时的。
如果领导节点由于分区而失效,并且持续与分区同时失效,则第二条可能不成立,因为不可能在分区的这一侧上选举新的领导节点。在发生分区期间,读取在分区开始之前时间戳处的数据,很可能在分区的两侧上都成功,因为任何可达副本的数据都满足需求(译者注:看来可用性还要看用是否需要最新的数据)。
关于真时(TrueTime)
一般来说,同步时钟可以避免分布式系统中的通信。Barbara Liskov提供了带有许多示例的详细概述[Lis91]。对于我们的目标来说,真时(TrueTime)是一个有界非零误差的全局同步时钟:它返回一个时间间隔,这个时间间隔包含了调用执行的实际时间。因此,如果两个间隔不重叠,则我们知道调用一定是实时排序的。如果间隔重叠,我们将无法知道调用的实际顺序。
Spanner的一个精妙之处在于它从分布式锁获得串行性,从真时(TrueTime)获得外部一致性(类似于线性化)。Spanner的外部一致性不变量是指,对任何两个事务,T1和T2(即使在地球的两侧):
如果T2在T1提交后开始提交,则T2的时间戳大于T1的时间戳。
借用Liskov[Lis91,第7节]的话:
可以使用同步时钟可以降低违反外部一致性的概率。本质上来说,主服务器保存租约,对象是整个备份副本组。备份节点发送到主服务器的每条消息将授予主服务器租约。如果主存储器保存有来自多数备份节点的未到期租约,则主数据库可以单方面进行读取操作。......
该系统中的不变量是:每当主服务器节点执行读取时,它必须保存有来自大多数备份节点的有效租约。如果时钟不同步,这个不变量将不会成立。
Spanner使用真时(TrueTime)作为时钟,可以确保不变量成立。特别地,在事务提交期间,领导节点必须等待,直到确认提交时间是过去的(基于误差范围)。这种“提交等待”在生产环境中不是长时间的等待,并且与(内部)事务通信并行地进行。一般来说,外部一致性需要单调增加时间戳,而“等待不确定性”是一种常见的模式。
Spanner通过更新租约,延长领导节点的选举时间,通常为10秒。就像Liskov所讨论的,每当法定数节点同意一项决定时,租约被延长,因为参与节点刚刚验证了领导节点是有效的。当领导节点故障时,有两个选项:1)等待租约过期,然后选择新的领导节点,或2)重启旧领导节点,这可能更快。对于一些故障,我们可以发出“最后一个”UDP数据包来释放租约,这是用来加速租约的到期。由于计划外的故障在Google数据中心中很少见,所以长期租约很有意义(译者注:乐观方法)。租约还确保领导节点之间的时间单调性,并且使得群组参与节点能够在租约时间内提供读取(即使没有领导节点)。
然而,真时(TrueTime)的真正价值在于它在一致性快照方面的能力。回望过去,多版本并发控制系统(MVCC)[Ree78]的历史已经很悠久了——单独保存旧版本,读取操作从旧的版本中读取数据,而不需要考虑当前的事务操作。这是一个非常有用和被低估的属性:特别是,Spanner快照是一致的(对于快照时间),因此无论系统保存什么样的不变量,快照也将会保存。这是真的,尽管你不知道什么是不变量!本质上来说,快照来自于连续事务之间,并且反映了快照时间之前的所有内容(更多的就没有了)。如果没有事务一致的快照,系统很难从过去的某个时间点重新启动,因为事务部分提交的内容可能违反了一些不变量或完整性约束。正是缺乏一致性,有时系统很难从备份中还原——冲突的数据需要手动恢复。
例如,考虑使用MapReduce在数据库上执行大型分析查询。如果使用Bigtable作为数据库,尽管Bigtable也存储数据过去的版本,但数据分片的时间标签是“锯齿状”的,这使得结果不可预测,有时还会导致不一致(特别是对于最近的数据)。如果使用Spanner作为数据库,同一个MapReduce操作可以选择精确的时间戳,并获得可重复和一致的结果。
真时(TrueTime)还使跨多个独立系统记录快照成为可能——只要使用(单调递增)真时(TrueTime)时间戳提交,与快照时间达成一致,并随时间存储多个版本(通常在日志中)。这不仅限于Spanner:您可以制作自己的事务系统,并且两个系统(甚至k系统)上的快照是一致的。一般来说,在这些系统上你需要一个2PC(同时持有锁)来与快照时间达成一致并确认成功,但系统不需要就其他事项达成一致,可以完全不同。
你还可以使用时间戳作为令牌在工作流中传递。例如,如果对系统进行更新,你可以将更新的时间戳传递到工作流的下一个阶段,以便确定系统是否反映该事件后的时间。在发生分区的情况下,就不一定了。在这种情况下,下一个阶段实际上应该等待保持一致性(或继续工作保持可用性)。如果没有时间戳令牌,很难知道你需要等待什么。当然,传递时间戳不是解决这个问题的唯一方法,但这是一种优雅的,鲁棒的方式,同时确保最终一致性。当不同的阶段不共享代码且具有不同的管理员时,这特别有用——两者可以在没有通信的情况下对时间达成一致。
快照是关于过去的,Spanner也可以对未来时间达成共识。Spanner的一项功能是,你可以对未来schema变更的时间达成一致。这允许你暂停应用新schema的更改,以便能够同时服务两个版本。一旦你准备好了,你可以选择一个时间点,在所有副本上以原子方式同时切换到新的schema(你也可以选择之前的时间点,但你不可能在目标时间之前准备好)。至少在理论上,你可以进行未来的操作,例如可见性计划的删除或更改。
真时(TrueTime)本身可能受到分区的阻碍。真时(TrueTime)的来源是GPS接收器和原子钟的组合,两者都可以通过它们自身的微小漂移来保持精确的时间。由于每个数据中心都有“主时间服务器”(冗余),因此分区的两侧很可能继续享有准确的时间。然而,独立的节点需要到通过网络连接到时间服务器,否则它们的时钟将会漂移。因此,在分区期间,基于本地时钟漂移的速率,独立节点的间隔随着时间缓慢地增长。基于真时(TrueTime)的操作,如Paxos领导节点选举或事务提交,因此必须多等待一段时间,但操作仍能够完成(假设2PC和法定数仲裁的通信工作良好)。
Eric Brewer
VP, Infrastructure at Google
Professor, UC Berkeley
结论
作为一个全球规模的分布式系统,Spanner只是提供了一致性和大于5个9的可用性,但Spanner宣称是一个“实际上是CA”系统是合理的。与Chubby一样,如果你能够控制整个网络(尽管全球范围内不常见),CA组合在实际生产环境中是可能的。即使这样,仍旧需要网络路径的大量冗余,处理相关故障的架构规划,以及计划周密的运维,特别是系统升级的时候。在发生宕机的情况时,Spanner选择一致性而不是可用性。
Spanner使用2PC来实现串行化,使用真时(TrueTime)实现外部一致性,无锁一致性读取,以及一致的快照。
【本文是51CTO专栏作者石头的原创文章,转载请通过作者微信公众号补天遗石(butianys)获取授权】