亚马逊工程师如何将现有超大规模 NoSQL 数据库改造支持大规模分布式事务?
寻寻觅觅,终于发现一个利用 Amazon Time Sync 服务高精度参考时钟,实现事务特性的一个云服务,看着看着就感受到亚马逊团队非常接地气根据客户的反馈,依靠 10 年+分布式系统的经验,不断创新,非常厉害!
2018 年,亚马逊云科技宣布 Amazon DynamoDB 支持事务:事务操作支持同一个区域(Region)单表或多表操作,但不支持在全局表(Global Tables)的跨区域操作。事务支持数据 原子性、一致性、隔离性和持久性(ACID),客户因此可以在一个业务逻辑中实现数据的事务写操作(插入、更新、或删除)
通过这一新特性,DynamoDB 提供了多种读写选项以满足不同的应用程序要求,为实现复杂的数据驱动业务逻辑提供了巨大的灵活性给开发人员;在事务过程中,数据条目不会被锁定;DynamoDB 事务提供可串行化的隔离性(serializable isolation),如果一个数据条目在事务之外被修改,正在进行的事务会被取消,并抛出一个异常,其中包含导致异常的数据条目及其详细信息。
1-好奇心
通过学习高精度时间同步服务 《被忽略的云厂商首个微秒级 µs 精度的时钟同步服务?!(下)》以及 Google 的 TrueTime,非常好奇有没有哪个云服务利用了真实的物理时间和类似 Spanner 的原理实现分布式事务?《多区域多主NoSQL云原生实现之全局表解析》一文中我们学到,DynamoDB 自身底层实现以及跨区域同步的 Global Table 功能,都是基于逻辑时钟和时钟向量算法的实现;《沃纳博士解读 Amazon S3 强一致性技术实现》 Amazon S3 从最终一致性提升到写后读的强一致性读,对于广泛以 S3 为数据湖的现代分析系统再也不用通过外部元数据和额外的复杂逻辑来保证数据应用的一致性,如何实现呢?S3 团队在 S3 元数据持久层,引入新的 “见证人 Witnesses”机制,即一个新的复制逻辑,允许系统可以对每个 S3 对象的操作顺序进行推理和判断,用户读 S3 对象时就可以通过元数据来判断该对象是否最新或过时,而且只跟踪内存数据的有限最新状态变化,巧妙实现了可被验证,不影响 S3 本身性能的强一致性实现;
NoSQL 云数据库服务因其简单的键值操作、高可用性、高扩展性和可预测的性能而受欢迎,这些特性通常被认为与支持对分区数据进行原子和可串行化更新的事务相矛盾;DynamoDB,它使用时间戳排序协议,DynamoDB 不需要严格的时钟同步,也不像 Spanner 那样假设任何时间同步误差窗口。它不需要多版本并发控制(MVCC),也避免了任何锁方案,例如 2PL (two-phase locking)或确定性锁。DynamoDB 使用的时间戳排序方法非常简单:为事务分配时间戳,并让这些时间戳定义序列化顺序,即具有较小时间戳的事务必须显示为先于具有较大时间戳的事务执行。因此,数据库的任务是中止任何违反这种假象的事务。我们将看到,通过这种简单的方法,DynamoDB 在避免更复杂协议的复杂性的同时,提供了 ACID 事务。
2.设计目标
DynamoDB 发布 6 年后,根据用户的反馈,期望有 ACID 事务支持,以简化开发者的编程复杂度,2007 年 Dynamo 论文强调设计初衷是永远可写,重性能可用性可扩展性,因此牺牲了强一致性,这对于增加 ACID 事务支持设计的考量,就需要继续保持高可扩展性;
可扩展性定义意味着 1)无限增长和 2)可预测性能的组合。许多云数据库服务采用简单的数据模型而不保证一致性或隔离性的原因,是为了满足这些可扩展性要求,当一个数据库不关心一致性或隔离性时,我们可以通过简单地对数据进行分区并在需要时继续添加新分片来水平扩展系统;另一方面,额外的一致性或隔离性意味着随着扩展会增加延迟。
总结来说,DynamoDB 事务的设计目标包括:
原子性和串行化
可扩展性
无限增长
可预测的性能
不使用 MVCC(原因是底层存储不支持,客户为存储付费,MVCC 带来额外的存储成本不容易处理)
避免对非事务操作的影响
3-DynamoDB 读一致性和事务模型简介
当您的应用程序向 DynamoDB 表写入数据并收到 HTTP 200 响应(OK)时,这意味着写入已成功完成并已持久保存;DynamoDB 提供读提交(read-committed)隔离并确保读操作总是返回项的提交值;读操作永远不会呈现一个最终未成功的写操作的数据条目视图;读提交隔离并不阻止在读操作之后立即修改该条目。
最终一致性读:所有读操作的默认读一致性模型;响应可能不反映最近完成的写操作的结果
强一致性读:GetItem、Query 和 Scan 等读操作提供了一个可选的 ConsistentRead 参数,设置成 True,DynamoDB 会返回包含最新数据的响应,反映所有之前成功的写操作的更新;限制:强一致性只支持表和本地索引(LSI),不支持 GSI 和 DynamoDB Stream
事务读:TransactGetItems,一个包含最多 100 个 GetItem 操作 的批量同步操作,支持同一个区域的跨表读取;如果有正在进行的写事务,并且跟 TransactGetItems 涉及的一部分的数据条目有重叠,则读事务将被取消。限制:操作的数据条目总大小不能超过 4MB
事务写:TransactWriteItems,是一个同步且幂等的写操作,它可以将最多 100 个写操作组合成一个要么全部成功要么全部失败的事务操作。限制:操作的数据条目总大小不能超过 4MB
DynamoDB 提供了如下两个典型的事务操作 API,TransactGetItems,支持读请求 和 TransactWriteItems,支持 Put/Delete/Update 和 Check 四个操作;
DynamoDB 并没有提供 Read-Write 事务操作 API,比如我们不能执行一个事务,包含读取 Key1 的值,并将 Key2 的值更新成 Key1 的值;但我们可以利用 Check 和 Update 操作的组合来实现;
4-整体架构
所有发送到 DynamoDB 的操作都会到达一组前端主机,称为请求路由器 (Request Router,简称 RR),请求路由器会对每个请求进行认证(Authentication System),并根据访问的键(Key)将请求路由到适当的存储节点(Storage Node,简称 SN),键范围到存储节点的映射由元数据子系统(Metadata System)维护。
与非事务请求类似,每个事务操作最初也由请求路由器接收,请求路由器会对请求进行需要的认证和授权,并将其转发到一组事务协调器(Transaction Coordinator,简称 TC),DynamoDB 中不仅仅有一个事务协调器,而是一组无状态的事务协调器,该 TC 集群中的任何协调器都可以管理任何事务的执行;事务协调器将事务拆分为 Item (数据条目)级别的操作,并运行一个分布式协议,这些数据条目的存储节点会参与进来;
5-写事务实现
RR 识别到该请求是一个事务操作,将请求路由到 TC 事务协调器,TC 执行一个两阶段协议 2PC 确保事务内的所有写操作都以原子方式和正确的顺序执行;事务协调器 TC 在第一阶段准备所有的项目,在第二阶段,如果事务涉及到的所有 SN 存储节点接受,TC 就会提交该事务并指示 SN 存储节点执行写操作指令;如果任何存储节点不能接受该事务,那么事务协调器将取消该事务。在决定提交事务之后,每个参与事务的存储节点 SN 在其本地 Item 上执行所需的写操作,并将事务的时间戳记录为 Item 的最后写入时间戳,对于完成第一阶段检查了前提条件但未写入的 Item,其时间戳也会更新。
一个 SN 存储节点基于两件事决定是否接受一个事务:
事务代码中指定的检查语句(Check 操作)
与其他事务的冲突检查结果(后续章节我们展开这方面探讨)
为了实现写事务的时间戳顺序, DynamoDB 为每个 Item 记录写操作的时间戳。SN 存储节点还为每个在进行中的事务持久化每个事务的元数据,包括事务的标识符和时间戳,这些元数据关联到事务相关的的 Item 数据,并被保留到分区相关的修改期间,例如分区拆分,这确保这些分区相关变更不会干扰事务的执行,并可以并行发生;关于事务的这些额外信息在两阶段协议期间更新和检查,一旦事务完成就可以丢弃。
TC 上的事务状态存储在持久化日志(Ledger)中,它本身也是一个 DynamoDB 表;有一个恢复管理器处理 TC 的失败并继续记录在日志中的待处理事务;TC 确保被接受的事务变更有且提交一次(Exactly Once)到所有存储分片;它会一直发送执行事务决定直到所有相关 SN 节点都确认,因此,为了避免重复变更,SN 存储节点必须对来自 TC 的消息进行重复数据删除。
已删除的 Item 需要特殊处理,因为一旦删除,就不再有最后写入时间戳,为了避免为已删除的项目维护墓碑,这样做会带来高存储成本和频繁创建和删除的数据 Item 的垃圾回收成本;DynamoDB 记录分区级最大删除时间戳;
当一个数据 Item 被删除时,如果删除事务的时间戳大于当前的最大删除时间戳,则将分区最大删除时间戳设置为该删除事务的时间戳;当 SN 存储节点对不存在的项目收到准备(Prepare)消息时,它会比较新事务的时间戳和最大删除时间戳,以决定是否接受或拒绝该事务。新增分区级最大删除时间戳记录提供了正确和高效的解决方案;
6-读事务实现
读事务也使用两阶段协议来执行,但是与前面提到的写事务以及其他系统实现采取了不一样的路径;
标准的时间戳排序方案对每个数据 Item 维护一个读时间戳;为读事务中的操作更新此时间戳会将每个读操作转变为对持久化数据的更昂贵的写操作,为避免这种延迟和代价, DynamoDB 设计了一种用于执行读事务的两阶段无写操作协议。
在协议的第一阶段,事务协调器 TC 读取读事务中所有相关的数据 Item,如果这些 item 中任何一个正在被另一个事务写入,则该读事务被拒绝;
否则,在其对事务协调器 TC 的响应中,SN 存储节点不仅返回数据 Item 的值,还返回其当前提交的日志序列号(Log Sequence Number),数据 Item 的当前提交 LSN 是 SN 存储节点执行并确认给客户端的最后一次写入的序列号,LSN 单调递增。
在第二阶段,数据 Item 被再次读取,如果两阶段之间数据 Item 没有更改, 即 LSN 没有变化,则读事务成功返回,如果任意一个数据 Item 在两阶段协议之间被更新了,则读事务被拒绝。
无论失败还是成功,SN 存储节点都会返回 LSN,通过这样做,TC 事务协调器能够在不重启整个事务的情况下对所有数据 Item 进行新一轮读操作,如果数据 Item 正在被写事务准备,SN 存储节点简单地拒绝读操作。
7-非事务操作
事务和非事务操作通过完全独立的路径进行,这种设计试图满足设计目标中提到的“对非事务操作无影响”的要求。
8-事务的冲突检查
冲突检查是每个参与事务的 SN 存储节点,2PC 的第一阶段完成的,DynamoDB 不使用 2PL、MVCC 或确定性方法(deterministic approach),相反,它通过时间戳排序来实现一个简单的乐观实现方法来序列化事务(serialize transactions)可串行化(serializability)简单来说,事务必须以顺序执行的方式出现,数据库允许内部并发执行事务,但是外部观察者必须以某种顺序看到事务,顺序本身无关紧要,即任何顺序都是可以接受的,DynamoDB 使用事务的物理时间戳来排序它们;
在 DynamoDB 中,串行化顺序是由 TC 分配的物理时间戳(真实时间)定义的。
物理时间戳完全排序了事务,当我们说这个顺序“定义了串行化顺序”时,它意味着存储节点必须以一种对客户端(观察者)角度看来,事务是基于此顺序执行的方式进行操作。
因此,如果执行事务违反了对数据库的这种认知,DynamoDB 会中止它,为了确保我们遵循这个顺序,从 TC 接收事务请求的节点遵循一组规则来决定接受/拒绝每个事务。
9-时间戳排序协议与 Amazon Time Sync Service
时间戳排序用于定义事务的逻辑执行顺序;在接收到事务请求时,事务协调器(TC)使用其当前时钟的值为事务分配一个时间戳,为了可扩展处理事务请求工作负载,有大量的无状态事务协调器并行运行,不同的事务协调器为不同的事务分配时间戳;只要事务根据它们被分配的时间执行,串行化就能实现。
一旦时间戳被分配和前置条件检查之后,参与事务的存储节点可以在没有协调的情况下执行它们各自部分的事务,每个存储节点独立负责确保涉及其数据条目的请求以适当的顺序执行,并拒绝不能适当排序的冲突事务。
即使事务协调器(TC)的时钟不那么精确,串行化也仍然成立,但是更准确的时钟会帮助更多成功的事务和符合真实时间的序列化顺序,协调器集群中的时钟与 Amazon Time Sync Service 保持高度同步;
然而,即使拥有完美同步的时钟,由于网络中的消息延迟、事务协调器(TC)的故障和恢复以及其他系统问题,事务仍可能以无序的方式到达存储节点(SN);存储节点(SN)使用 TC 分配并记录在本地的时间戳来处理以任何顺序到达的事务请求。
10-哪些冲突导致事务被拒绝?
通常有三种情况,新的写事务会被拒绝:
跟已经完成(Completed)的事务冲突
跟已经被接受(Accepted)的事务冲突
事务的 Check 检查没通过(前提条件等)
10.1-新的写事务情形
一旦 SN 存储节点收到提交新写事务 TxNew 的请求,SN 存储节点会将其时间戳与当前已完成事务时间戳进行比较,因为 DynamoDB 使用物理时间戳顺序来定义序列化顺序,如果新事务请求的时间戳比任何已经完成的事务的时间戳都要小,节点会直接拒绝该事务;
但有一种可优化的情况存在,比如 SN 存储节点数据 Item 最近的一个事务是一个没有 Condition 的 Put 或 Delete 事务操作,那么哪怕新事务的时间戳比该完成的事务时间戳还要小,系统还是会接受该新事务请求,原因是新事务发生在 Put 或 Delete 之前,改变不了任何存储节点的数据状态,也就是这两个事务不存在“冲突”,交给存储节点直接处理掉;
10.2-并发事务情形
除了已完成的事务,SN 在接受事务前,还会检查新的事务与已接受的(但未完成的)事务是否冲突,如果新的事务 TxNew 的时间戳小于一个已接受的事务 Tx2 的时间戳,我们必须拒绝它;
同样,也存在一些可优化场景,比如如果 Tx2 没有带任何条件,那么接受带条件检查通过的 TxNew 就不会与 Tx2 有冲突。
与已完成的事务不同,即使 TxNew 有更大的时间戳,SN 存储节点仍可能因已接受的事务 Tx2 而拒绝该事务;这种情况发生在 TxNew 事务条件检查受到 Tx2 影响时;我们没有对已完成事务有这个额外的拒绝 TxNew 的情形,是因为已完成事务的效果已经更新到存储中,所以我们可以确定 TxNew 的检查会被正确评估;然而,对于 Tx2,结果还没有发生,所以如果我们继续评估 TxNew 的检查,我们可能会错误地接受 TxNew,而它本该被拒绝。
10.3-读事务情形
与写事务类似,如果一个读事务的时间戳小于 SN 节点中已完成事务的时间戳,我们应该中止该读事务,否则,我们可能会读取时间戳更大的事务结果数据,这违反了时间戳定义的顺序,换句话说,时间戳较小的读事务正试图读取过去的值;由于 DynamoDB 不维护旧版本,我们别无选择,只能中止该事务,如果 DynamoDB 支持了多版本并发控制(MVCC),它可以接受该事务,并返回读事务所请求的旧版本。
对于已接受的事务,规则是相反的,即如果读事务的时间戳小于已接受事务的时间戳也没关系,因为读事务不会读取已接受事务的结果,也不应该读取。然而,如果有已接受事务的时间戳小于读事务的时间戳,那么我们要么拒绝该读事务,要么等待已接受的事务生效后再执行我们的读操作;
10.4-非事务操作情形
DynamoDB 的设计目标之一是不影响非事务操作,对于单个 GetItem 操作来说,这个目标是满足的,即单个 GetItem 操作不需要做任何改变,我们可以直接读取该项的最新写入值。
但是,PutItem 操作在实现了事务的 DynamoDB 中可能会被拒绝;具体来说,如果 PutItem 操作可能影响已经接受但尚未执行完成的事务状态,则必须拒绝该 PutItem 操作;此外,如果 PutItem 操作本身是一个条件 PutItem,而其条件可能受到已接受但未完成的事务的影响,我们也必须拒绝该 PutItem 操作,因为我们不知道已接受事务的 Item 数据最终的值,所以无法继续评估 PutItem 的条件,注意,PutItem 操作可能仅因已接受但未完成的事务而被拒绝,已完成的事务永远不会导致 PutItem 操作被拒绝。
11-存储节点事务消息乱序对事务顺序的影响
我们前面的章节讨论了,在 2PC 的第一阶段中,DynamoDB 如何检查和判断冲突,来决定是否接受一个新事务;但对于写事务,我们在 2PC 的第二阶段也必须小心处理,因为 TC 的消息时间戳顺序跟 SN 存储节点收到的消息顺序有可能不一致(网络,设备等各种影响),目标是要确保事务执行不违反时间戳定义的顺序。
具体来说,有两件事我们需要小心处理:
我们必须忽略时间戳小于当前版本时间戳的 Put 操作
对被并发事务访问的同一个 Key 的数据条目的 Update 操作必须延迟
要理解第一种情况,考虑 Tx1 和 Tx2 都对 a 和 b 进行 Put 操作,它们存储在两个不同的数据存储节点中;Tx1 和 Tx2 在两个节点上都被接受,但写操作消息(2PC 的第二阶段)跟事务被分配的时间戳顺序不一致的顺序到达存储节点;
如果我们继续执行 Put 操作,我们将最终得到 a=2 和 b=1 的情况,这是不可接受的,因为它不是任何串行执行的结果。为避免这种情况,如果该 Key 的数据 Item 当前版本具有比新更新更高的时间戳,节点必须简单地忽略对该 Item 的任何 Put 操作。
在这个例子中,节点 b 必须忽略 Tx1 的 Put 操作,所以我们将最终得到 a=2 和 b=2,这符合时间戳定义的 Tx1、Tx2 的顺序。
对于第二种情况;它与上图的例子类似,只是 Tx2 有两个 Update 操作而不是 Put;在收到 Tx2 的写操作消息时,如果我们继续应用更新,我们将最终得到 a=2 和 b=1,这也不符合 Tx1 和 Tx2 的时间戳顺序;为避免这种情况,如果一个待处理的事务与较小时间戳访问同一个数据条目,Update 操作必须延迟提交。
12-总结
根据客户需求,将一个大规模的 NoSQL 数据库 DynamoDB 改造支持分布式事务,简化事务相关的开发者复杂度,相信任何一个团队都会压力山大,亚马逊的工程师,坚持不影响现有用户的体验,坚守可扩展性以及可预测高性能底线,利用高精度真实时间和时间戳排序算法,采用乐观锁,巧妙的在数十个核心微服务的 NoSQL 数据库系统中增加了 ACID 事务能力;对于架构师和用户,吃透了 DynamoDB 的架构、设计原则以及不断增加的新特性的各种权衡,是非常幸福的一件事情,并且可以将这些实践经验用在自己的系统中,相信这个 2012 年发布,历经 11 年不断演进的经典 NoSQL 数据库是您业务全球扩张的可靠的支撑!
参考资料
论文: Distributed Transactions at Scale in Amazon DynamoDB
Video : USENIX ATC '23 - Distributed Transactions at Scale in Amazon DynamoDB
Announcing Amazon DynamoDB Support for Transactions - Nov 27, 2018
Video : FAST '19 - Transactions and Scalability in Cloud Databases—Can’t We Have Both?
Amazon DynamoDB: ACID Transactions using Timestamp Ordering
版权声明: 本文为 InfoQ 作者【薛以致用】的原创文章。
原文链接:【http://xie.infoq.cn/article/f488ef87fd40dec02859d6f7c】。文章转载请联系作者。
评论