聊聊数据库跨地域

需求

随着国际业务的普及,以及对服务实时性和数据安全性的要求日益增强,越来越多的业务需要跨地域提供服务,作为其数据基座的数据库,也遇到了越来越多的跨地域部署的需求。在PolarDB MySQL GDN(Global Database Network)[1]的维护和开发过程中,我们也与很多客户讨论过数据库的跨地域部署。总结起来,主要包括两个方面:

  1. 容灾:即使发生地域级别的故障,数据库依然能够通过自动或人工介入的Failover方式继续提供服务。这种场景下,灾备地域的数据库副本在发生Failover之前,可以只是冷备不提供服务的状态。同时,不同业务对Failover也会有不同的RPO(可接受的最大数据丢失量)的要求
  2. 多活:数据库在不同地域要同时提供服务,这种需要在跨地域甚至是跨国服务的业务上更为常见,主要需要的是在不同地域都能有较低的请求时延。有些业务只关心读时延,而其他一些对读写时延都有要求。

这两个需求通常会在同一个实例或业务上同时存在。除此之外,还会有一些合规的要求对数据的传输和存储位置提出限制。

天堑

数据库的跨地域部署,最大的挑战来源于地域之间的网络延迟,受制于光在介质中的传递速度,这个时间是跟距离相关的。并且这种因物理距离增加,而导致的高网络时延,在可预见的未来内都是无法逾越的天堑。如下图是一个对Google Cloud各地域间网络Ping时延的汇总[2]

Google Cloud各地域间网络Ping时延热力图

可以看出,不同的物理距离下,网络时延从几ms到几百ms不等,较远的,比如中欧之间的时延甚至会超过300ms。这个时延从数据库服务的角度来看是非常可观的。假设数据库内部每个请求,需要一次跨地域的网络往返(RTT),那么这个数据库单线程下能提供的QPS上限只有3次/秒。

对数据库实现的影响

其实在NoSQL的场景下,跨地域部署的支持还是比较常见的,因为NoSQL大多从设计上,就为了扩展性牺牲了数据库的关系型以及强一致等特性。了解了NoSQL的取舍后,我们来看关系型数据库面临怎样的挑战。对关系型数据库而言,如此高的跨地域网络时延,会带来什么样的问题和设计权衡呢?关系型数据库的核心是事务,事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)四个重要特性。其中一致性(Consistency)指的是业务利用数据库提供的特性来保证数据及数据间关系的正确。这里我们重点关注原子性(Atomicity)、隔离性(Isolation)和持久性(Durability),是否要在跨地域的维度上,实现全部这些特性?如果实现会有什么样的挑战?如何实现能尽量的降低跨地域高网络延迟的影响?就是数据库不同的跨地域部署模式,以及不同的产品实现需要权衡选择的。

跨地域复制(Durability)

持久性(Durability)指的是即使发生故障,在恢复之后已经Commit事务的修改仍然存在,在单机应对节点故障时,通常可以靠WAL或者限制刷脏顺序来实现[3],将这个需求扩展到跨地域上面,当发生地域级别的故障时,如果要保证切换后,已经Commit的事务修改全部存在,就一定要采用跨地域的同步复制方式,那么极高的网络时延,会大大延长写请求的处理时间。考虑到地域级故障概率很低,在没有那么强要求的业务上,通常会采用异步复制的方式,容忍Failover时,接近地域复制延迟的数据丢失(RPO > 0)。另一种折中的复制方式是Quorum复制,多个节点组成一个Group,读写需要在大多数节点成功,在三个地域的节点组成的集群中,相当于较近的地域同步复制,较远的地域异步复制。可以看出数据的复制策略是没有银弹的,一定是请求时延和可用性的权衡(CAP)。

跨地域并发控制(Isolation)

隔离性(Isolation)要求并行执行的事务可以满足某隔离性级别[4]的正确性要求。要满足正确性要求就一定需要对事务的操作做冲突检测,对有冲突的事务进行延后或者丢弃。根据检测冲突的时机的不同,也是按照乐观程度的不同可以简单分成三类:

无论是哪种方式,都可能需要跨地域交互来确定是否有访问冲突,理论上越是悲观的方式,如基于Lock的方式,越是会需要多的跨地域交互。除此之外,由于MVCC(Multi-Version Concurrency Control)可以提供避免只读事务和写冲突的机制优势,在跨地域冲突检测如此昂贵的场景中就显得格外重要,因此MVCC结合乐观程度的不同,分为MV2PL、MVTO和MVOCC的实现方式[5]

MVCC的可见性判断和基于Timestamp或Validation的冲突检测,都需要给事务定序,也就是分配一个序号来表明事务的前后。事务的定序在单机下很简单,比如采用递增的ID值。在分布式环境中,通常会采用时间戳(Timestamp),但单机的物理时间戳有个重大的缺陷,就是不准确,并且不同的机器也会有不同的偏差,更麻烦的是这个时间的偏差不可知,且没有一个可控的偏移边界。这样的时间戳是没有办法用来判断前后的。这就是分布式系统中的授时问题,常见的解决方案包括:

跨地域事务提交(Atomicity)

原子性(Atomicity)指的是提交成功的事务的所有修改都能保留,未成功提交的事务的所有的修改,都不可见。在单机环境下,数据库会在事务提交的时候写一个标记事务提交的WAL日志,并以该日志落盘作为事务提交的确定点。如果发生重启,没有看到提交记录的事务就会被标记为Abort,并通过Undo日志将其所有修改回滚掉。但如果事务涉及到不同的节点,需要所有的节点都对事务提交或者失败达成一致,工程上成熟的方案就是两阶段提交2PC(Two-Phase Commit)[6]

两阶段提交(2PC)流程示意图

如上图所示,事务提交被拆分为两个阶段,Prepare阶段和Commit阶段:

  1. Prepare阶段:Coordinator确定所有数据都完成复制后,向所有Participant发送Prepare请求,所有Participant对要提交的数据进行本地的Check,比如拿锁或者唯一性检查,确保一定可以提交的话,本地写日志并返回Yes,这是第一个不可回退点,之后就再也不可以更改了,并且需要一直持有比如锁这些保证能提交的资源。
  2. Commit阶段:Coordinator要确保收到所有的Participant的答复,只有全部是Yes才能确定Commit,本地写日志,并返回用户Commit成功,并通知所有Participant提交。这是第二个不可回退点,之后Coordinator重启还是Failover都不能改变这个决策。Participant收到通知,本地提交并释放事务占用资源。

可以看出,类比单机事务提交中的Commit日志落盘后的提交承诺,2PC其实就是将这个承诺的点,拆分成了两段,Prepare阶段拿到所有的Participant提交承诺,并在最后由Coordinator汇总并通过落盘提交日志,来做出全局的提交承诺。在跨地域部署的场景下,如果事务涉及到跨地域的节点,一次2PC需要两次完整的网络来回,显然会大大加大事务提交需要的时间。

汇总一下,我们构造一个实际不存在,采用同步复制 + 基于Lock并发控制 + 中心节点授时,并且所有需要同步的节点和授时中心都不在当前地域的极端情况,以一个事务的完整生命周期来看看,假设300ms跨地域的网络时延,会带来多大的问题: 极端情况下的跨地域事务时延分析

可以看出,事务的每个阶段都需要网络交互,300ms的跨地域网络时延被成倍的扩大,在这种极端情况下,一个事务的完成可能需要数秒之久,这显然是不可接受的。因此实践上,通常会做一些权衡选择以及设计优化,下面就以实际的跨地域部署方案来进一步介绍。

多集群部署

在不同的地域部署多套数据库集群,并在他们之间通过日志进行复制。这种方式是最简单、也是最通用的跨地域部署方式。几乎所有的已有数据库产品都可以支持。大多数部署采用Master集群提供读写,之后通过异步复制同步到一个或多个部署在其他地域的只读集群,也就是Master-Slaves的部署方式。如下图所示:

Master-Slaves部署架构示意图

这种方式的权衡,是避免复杂的跨地域事务,并通过异步复制的方式,避免对主集群造成影响。Slave集群提供MVCC的本地域低延迟读服务,以及RPO > 0的地域级别灾备需求。这种方式是一种大多数业务可以接受的权衡选择。通常用户需要选择一个主地域,并把所有的写都交给这个地域来完成。

Multi-Master

Master-Slaves架构只能满足读请求的本地低时延需求。那么如果每个地域都需要低时延写入,是否可以直接采用Master-Master架构呢?很多数据库支持双向复制通道,多个集群都提供写,再通过复制链路互相复制,是不是可以直接按照Master-Master的方式部署呢?,最终达到数据一致。如下图所示:

Multi-Master部署架构示意图

从业务的角度看,这么做业务的改造成本直观看上去似乎很小,但其实是有很多不安全因素的,因为这种架构下是没有跨地域事务支持的,所有数据库约束都可能失效,首当其冲的是主键和唯一索引的唯一性约束。业务上需要控制绝对的不重复;除此之外,对于不同节点写的冲突检测是完全失效的,并且数据库是没有办法处理这种冲突的,更严重的是,很多时候这种冲突是静默发生的,比如两边执行顺序不同导致最后数据不一致,这些错误并不会在当时暴露出来。业务上需要精心设计,并充分提醒未来的开发和运维人员,并且准备好数据不一致等数据问题的处理方案。因此,对于Multi-Master的方案,笔者的观点是:其可以作为暂时的权宜之计,但绝不应该作为长期的演进方向。如果实在没办法必须要用, 这里有一些建议:《If You Must Deploy Multi-Master Replication, Read This First》[7]

多集群部署的这种架构下,完全放弃了跨地域的事务,因此主集群的写入事务是完全不受影响的,跨地域的复制异步进行,如下图所示:

Master-Slaves架构下的跨地域时延

定位跨地域部署的数据库

近些年来,一些数据库从设计上就定位于支持跨地域的部署,并实现了跨地域的事务支持,这里介绍几个非常有代表性的知名产品,包括Spanner、 CockroachDB以及Amazon Aurora DSQL,有趣的是他们的事务隔离性实现分别选择了MV2PL、MVTO(结合轻量级锁机制)和MVOCC三种方式。接下来就分别看看他们的设计理念、架构以及跨地域事务的权衡选择。

Spanner

Spanner是Google开发的全球分布式关系型数据库,首次于2012年公开介绍[8]。Spanner是Share Nothing架构。一个Spanner集群由一组可以分布在不同数据中心甚至不同地域的SpanServer节点组成。数据会被切分为分片(Tablet),每个Tablet又通过Quorum复制形成包含多个副本的Paxos Group,这个Tablet Paxos Group的不同副本又放置在不同的SpanServer上。每个SpanServer可以维护成百上千个不同Tablet的副本。如下图所示:

Spanner架构示意图

一个SpanServer里除了维护自己负责的Tablet存储及Paxos Group复制之外,还实现了对Key加锁的Lock Table,及负责事务协调的Transaction Manager模块。每个Paxos Group Leader会在事务提交时,作为Participant与其他Paxos Group Leader完成2PC。还是以本文前面介绍的框架来看Spanner在复制(Durability)、事务提交(Atomicity)以及并发控制(Isolation)的实现:

跨地域复制(Durability)

Spanner采用Tablet Paxos Group的方式进行复制,Tablet的从副本可以放置在本地域,也可以放置(Placement)在其他地域。Spanner的Paxos实现采用了Leader Lease的实现方式,默认的租约时长为10s,写请求需要成功复制到大多数节点才能成功,读在Leader本地就可以完成。分片的放置策略定义了其时延水平和容灾级别,比如选择3副本在美东,2副本在美西。由于美东有大多数副本,美东的写入就可以以地域内的时延完成,但这样美东地域发生故障时将无法自动容灾。副本如何放置是跟业务需求息息相关的。Spanner提供了灵活的机制,比如将连续Key抽象为Directory,支持一个Tablet中包含多个Directory,也就是不同连续Key可以按照需求物理上放在一起。同时,将这种数据副本放置策略放开给用户指定。

跨地域并发控制(Isolation)

Spanner的并发控制采用基于Lock的2PL[9]来做读写事务之间的冲突检测,提供Serializability隔离级别。读写事务全部由Tablet的Paxos Leader提供服务,采用了非中心的Lock管理方式,由各个SpanServer维护其Leader副本的Key的Lock Table。但Spanner在这里做了一点乐观的策略,来减少冲突和跨地域访问。读写事务访问过程中,读请求会定位到对应的Leader加读锁访问,但写仅会缓存在Client上,直到Commit阶段才去拿所有的写锁,这样的好处是减少了写请求的跨网络加锁,缩短写锁阻塞其他读请求的时间,但问题是Commit的时候发现Lock冲突需要回滚。 Spanner采用了MVCC的方式来避免只读事务与读写事务之间的冲突,依赖Google TrueTime进行授时。读写事务在Commit前才分配Commit Timestamp,整个事务的写都会当成是这个时刻的原子写操作。这个时间戳会跟其修改的数据记录存储在一起,作为一个多版本的记录,只读事务会以自己的Read Timestamp跟记录的Commit Timestamp做可见性判断。 只读事务可以由Paxos Group Follower来提供MVCC读,只要该Follower的数据相对于其Read Timestamp足够新,足够新的意思包含两层,一层是这个Follower的Paxos Log跟到足够新,一层是其已经看到了所有这个Read Timestamp以下的事务Prepare及Commit,为此Follower会维护一个安全的时间戳点位Tsafe。

跨地域事务提交(Atomicity)

当事务涉及到多个Tablet,也就是多个Paxos Group时,事务提交的时候就需要所有的Paxos Group达成一致。也就是需要前面讲到的两阶段提交2PC(Two-Phase Commit)。其中一个Paxos Group被选为Coordinator,其Leader叫做Coordinator Leader,其他Paxos Group的Leader称为Participant Leader。2PC的流程如下:

汇总一下,假设Paxos Group及事务都需要跨地域的情况下,Spanner通过TrueTime实现本地时延获取Timestamp;将加写锁的操作推迟到Commit阶段,与Prepare阶段合并,通过一次网络交互完成;Commit阶段Coordinator给Participant的发送提交确认的网络交互,又可以跟Persistent需要的Paxos Quorum复制重合。如下图所示,不过实践上还是尽量避免跨地域的同步副本和跨地域的事务存在。

Spanner跨地域事务时延优化

CockroachDB

CockroachDB是一个分布式全球数据库实现[10][11]。也是Share-Nothing架构的数据库,非常类似于Spanner,数据也被划分为分片,在CockroachDB中称为Range,Range有多个副本,通过一致性协议Raft[12]组成Raft Group,副本分布在不同的节点上。每个节点可以负责多个Range副本。每个Raft Group又会在事务提交的时候,作为Participant与其他涉及到的Raft Group组成2PC,一同完成原子提交。我们继续沿用前面的分析框架:

跨地域复制(Durability)

跟Spanner相同,CockroachDB的Raft副本可以部署在同地域,也可以部署在不同地域。同样采用Leader Lease的实现方式,写请求需要写大多数,受跨地域的网络时延影响,读请求可以直接由Leader本地处理。因此,Range副本的放置策略同样会决定其时延水平和容灾级别。CockroachDB也将数据Placement的配置能力暴露给了用户,并且更进一步定义了规范的配置方式,用户可以通过CREATE TABLE或ALTER TABLE语句将表设置为REGIONAL BY TABLE、REGIONAL BY ROW或GLOBAL三种类型之一,并指定其部署地域:

跨地域并发控制(Isolation)

CockroachDB采用比Spanner更乐观的基于Timestamp Ordering的并发控制策略,不会提前对记录加锁,而是在真的访问记录的时候,才去判断冲突。 授时是CockroachDB面临的一个大的挑战,由于定位于开源产品,不能依赖TrueTime这种昂贵基础设施,因此CockroachDB采用的是类似于Lamport Clock的混合时钟(HLC)方式,时间戳由粗粒度的本机物理时间结合逻辑时间组成,并通过网络包在节点间传递同步,也不需要跨地域的网络交互。这种方式可以全局保持因果序,在单个节点内保持单调递增,并通过足够的消息交换控制偏差。因此在一致性上,CockroachDB也放弃了全局的Linearizability,只保证Single-Key的Linearizability,当然这在大部分场景是足够的。HLC通过配置max clock offset来处理时钟偏差,默认配置为500ms。 读写事务开始之前会先拿一个Read Timestamp以及一个Commit Timestamp,一个事务最终的写入的记录,都是结合其Commit Timestamp形成记录的多版本的。为了减少因为Timestamp冲突导致的事务回滚,CockroachDB提供了一个非常有趣的机制:提升Commit Timestamp。读写事务的读写都会路由到Raft Group Leader来处理,其基于时间戳的并发控制处理规则如下:

提升Commit Timestamp本身比较简单,由于下面要讲到的原子性保证的实现方式,只需要修改一个元信息的位置即可。但为了保证Serializability,其Read Timestamp也需要提升到相同的位置,这个时候需要检查当前事务之前所有的Read的可见结果,确保其不会因为Timestamp的提升发生变化,如果变化了,该事务就需要Abort或重启,这个检查过程叫做Read Refreshes,其实现方式类似于SSI的实现方式[13]只读事务,同样采用MVCC的方式,可以由Raft Group Follower提供,只读事务开始也会获取一个Read Timestamp并以此作为自己读的Snapshot,同样,提供读的Follower需要确保自己的Raft Log跟到足够新,并且已经看到了所有这个Read Timestamp以下的事务Prepare及Commit。CockroachDB通过Leader不断提升自己的关闭时间Close Timestamp来保证这一点。

跨地域事务提交(Atomicity)

CockroachDB将所有的修改当成在Commit Timestamp的一次原子提交,不论这些读写的时间戳有没有提升过,为此,事务提交的记录会在Commit的时刻结合Commit Timestamp重写一遍。事务的跨Raft Group提交同样采用两阶段提交2PC(Two-Phase Commit)的实现方式,由Gateway层担任Coordinator的角色。整个事务的过程包括三个阶段[14]

  1. Writes and Reads: Coordinator向各个Raft Leader发送读写请求,相互之间不冲突的请求会以并行发送,形成Write Pipelining。每条记录写入都会维护成一个intent,除了实际数据外,intent前面会有一个指针指向一个事务元信息,这个元信息在第一次写入的时候创建,包含事务的状态和Commit Timestamp
  2. Prepare:这里采用改良的2PC,CockroachDB叫做Parallel Commit,做法是增加一个Staging的事务状态, 跟实际的数据分发并行起来,因此可以减少一次网络往返的等待时间。
  3. Commit:事务状态从Staging改为Committed。之后异步将intent改写成MVCC记录,去掉指向事务元信息的指针,增加Commit Timestamp信息,并删除intents。

汇总一下,同样假设Raft Group及事务都需要跨地域的情况下,CockroachDB通过HLC实现本地时延获取Timestamp;通过Write Pipeline及Parallel Commit,将集中发生的写请求,及2PC Prepare阶段的网络交互并行起来,尽可能减少实际的等待时间;2PC Commit阶段提交确认消息,同样可以跟Persistent需要的Raft Quorum复制重合。如下图所示,同样实践上还是尽量避免跨地域的同步副本和跨地域的事务存在,比如CockroachDB提供三种表类型里,只有GLOBAL可以将Raft Group的Voter节点跨地域部署。

CockroachDB跨地域事务时延优化

Amazon Aurora DSQL

DSQL是Amazon Aurora推出的针对跨地域多活设计的产品,在2024年的Re:Invent上正式发布[15]。Aurora是Share Storage数据库[16],其存储层设计仅能跨可用区(AZ),不能跨地域。因此,DSQL的架构跟前面两个Share Nothing产品有较大的不同,其更像是前面的多集群部署的方式,只是在多集群部署方式上面加了一层分布式事务层,来实现跨地域的事务保证。DSQL目前只支持双地域的方式,两个地域都维护完整的一套节点和数据,其架构如下图所示:

Amazon Aurora DSQL架构示意图

DSQL的设计理念倾向于将模块拆分的更细,负责SQL解析执行的Query Processor、负责事务协调的Adjudicator、负责日志持久化及跨地域复制的Journal,以及负责数据存储和访问的Storage。都是独立的模块,相互之间需要通过网络交互。这种分层设计带来的直接好处是模块解耦,以及各模块可以独立弹性伸缩。比如,单个Storage节点维护的数据分片范围,和Adjudicator负责的数据范围分片就是不同的,如此一来他们都可以独立地调整节点数和负载均衡,独立地弹扩弹缩。这一点上DSQL的设计者因为有着虚拟化Serverless的工作背景,是比较激进的,甚至每个事务都会分配独立的虚拟机Firecracker来执行。但模块独立,带来的直接影响就是增加网络交互。我们这里还是重点关注事务实现的各个阶段中,跨地域的网络的处理和权衡。

跨地域复制(Durability)

DSQL中跨地域的复制是依赖Journal组件的,负责事务协调的Adjudicator在确认事务提交后,会写日志进Journal。官方透露的信息比较少,不过从实现效果上看,Journal中日志会有跨三个地域的副本,并通过一致性算法来保持数据同步。通常会在用户选择的两个DSQL地域中间位置找一个Region B,这个地域只部署Journal副本。写请求需要写大多数成功,因此写请求的时延是到Region B的网络时延决定的。因此,DSQL也能容忍地域级别的故障,如下图所示,当三个地域中的Region C故障后,Region A依然可以和Region B形成大多数并提供服务。

DSQL三地域高可用架构

跨地域并发控制(Isolation)

对于读写事务之间的冲突,DSQL采用的是基于Validation的OCC的实现方式。授时方式使用的是Amazon的Time-Sync Service,类似于Google的TrueTime,同样使用GPS结合原子钟,在全球范围内实现了较精确的物理时间。事务开始前会获取一个Start Timestamp,并以这个时间戳来做MVCC读,读请求都可以直接到当前地域的Storage对应节点上进行。所有的写都只缓存在Query Processor的内存中,直到Commit时才由对应的Adjudicator判断事务从开始到提交期间的所有写入是否存在冲突,一旦有冲突就需要回滚。为此DSQL限制单个事务的长度不能超过5分钟,而Adjudicator就需要将这五分钟的修改记录都缓存下来做冲突判断。这种OCC的选择最大的好处就是事务的整个生命周期中,除了Commit阶段外,完全没有跨地域的协调需求,无论读写都可以在当前地域内完成。对于事务读写多条语句,但又很少冲突的场景是有明显好处的;坏处是一旦发生冲突,整个事务回滚的开销会更大,不适合冲突较多的场景。

跨地域事务提交(Atomicity)

Adjudicator是事务提交的协调器,不同于其他组件在两个地域都有完整的副本。负责某个数据范围的Adjudicator在全局只有一个,所有修改对应Key的事务在提交的时候都需要到这个Adjudicator上去判断是否冲突。当一个事务修改的数据超出一个Adjudicator的覆盖范围的时候,就会涉及到多个Adjudicator的协调,DSQL采用的也是两阶段提交2PC(Two-Phase Commit)来协调多个Adjudicator的同步提交,如果对应Key的Adjudicator不在当前地域时,这里就会存在跨地域访问的情况。确认提交后,写Journal持久化,并异步分发到两地的Storage上去应用。如下图所示:

DSQL写入流程示意图

汇总一下,DSQL采用基于Validation的并发控制,事务内的读可以在本地域内Storage处理,写在事务提交前都只缓存在Query Processor内部,不受跨地域网络影响;同时采用类似于TrueTime的方式获取Timestamp,同样不需要跨地域网络;涉及到跨地域Adjudicator的事务,需要做跨地域的2PC,只要Journal Quorum落盘成功即完成持久化。

DSQL跨地域事务时延分析

总结

观点

PolarDB for MySQL的GDN产品长时间稳定服务了大量的全球业务用户,拥有物理复制低时延、全局域名、一体化产品等优势,近期又支持了跨地域分片就近写的能力[1],欢迎关注试用。

参考

[1] PolarDB for MySQL 全球数据库网络

[2] What is your ping, Google Cloud and Amazon AWS?

[3] 数据库故障恢复机制的前世今生

[4] 数据库事务隔离发展历史

[5] 浅析数据库并发控制机制

[6] Designing Data-Intensive Applications

[7] If You Must Deploy Multi-Master Replication, Read This First

[8] Spanner: Google’s Globally-Distributed Database

[9] B+树数据库加锁历史

[10] CockroachDB: The Resilient Geo-Distributed SQL Database

[11] Enabling the Next Generation of Multi-Region Applications with CockroachDB

[12] Raft和它的三个子问题

[13] Serializable isolation for snapshot databases

[14] Cockroach Labs Doc

[15] AWS re:Invent 2024 - Deep dive into Amazon Aurora DSQL and its architecture

[16] CloudJump II:云数据库在共享存储场景下的优化与实现(发表于SIGMOD 2025)

[17] Marc’s Blog

Table of Contents