写入多个表_多区域多主NoSQL云原生实现之全局表解析

我们在《客户案例分析:Netflix 全球多区域多活架构演进》一文中详细回顾了 Netflix 的全球多区域多活架构的技术演进,由于实现多活的状态数据双向复制,客户通常需要引入很多新的技术组件,同时考虑每个组件的可用性,性能和可靠性,实现和持续运营成本非常高,因此本文扩展解析云原生的 NoSQL 数据库 Amazon DynamoDB 提供的 Global Tables(全局表)如何帮助客户多区域双向复制状态数据,同时简化运维和屏蔽复杂的底层实现细节。

Global Tables 的功能还在不断演进中,本文所描述的技术细节都来自公开技术资料(见文末参考资料)的推演,不代表是官方真正的实现逻辑,仅供大家参考技术交流学习。

a01fb41883004f52d641139099537731.png

客户体验如何?

AWS DynamoDB 在移动、Web、游戏、电商、广告和物联网等需要任意规模且低延迟数据访问的场景中有广泛应用的键值及文档数据库,每天可处理超过10 万亿个请求,并可支持每秒超 2000万的峰值。和关系数据库类似,都是以“表”为核心的操作单位,不过底层是分布式架构,存储和性能都支持自动扩展,使用起来非常省心;而 Global Tables 在 2017年发布的新功能,是为了满足客户多区域容灾和全球扩展的需求,其基本用户体验如下:

681d2d5cbce3dc89865586bed84e532b.png

从用户角度,基本的几个事实是:

  • 多区域多活:通常两个区域的数据延迟在1秒以内,基于AWS全球骨干网进行数据传输,当一个区域发送故障的时候,用户可以选择切换到另外一个区域

  • 成本:没有额外的成本,仅仅是数据存储和跨区域数据传输费用

  • 简单有效的冲突处理:根据数据的最后更新时间,晚更新/写入的覆盖时间较早的,“last writer wins”

  • 基于流的异步复制:基于 DynamoDB 的 Stream 异步跨区域复制

  • 最终一致性:目前全局表数据满足最终一致性,对于同一个区域实现事务和强一致性读

  • 增加三个系统字段:为了实现多区域同步,系统在 Global Tables 表增加了3个系统字段:

    • aws:rep:deleting=true|false (对于删除操作的特殊标志)

    • aws:rep:updatetime(最后更新时间)

    •  aws:rep:updateregion (数据更新来自哪个区域)

1.1 监控

对于跨区域的数据同步,原生的CloudWatch 监控提供两个客户关注的指标:(1)ReplicationLatency,从一个副本表的更新项出现在DynamoDB 流中,到它复制到全局表的另一个副本中所经过的时间。(2)PendingReplicationCount写入到全局表中的一个副本表但尚未写入到另一个副本的项目更新数量。

6c6034a1deedc4a8be649b7a3a0749c8.png

1.2 用户体验小结

 对于用户而言,如何创建和使用 Global Tables 呢?Global Tables 从 API 角度来看是基于 DynamoDB 一个独立的功能,用户在多个区域有 2个以上需要复制的表,然后通过 API 或控制台把这些表添加到 Global Tables 中即可,副本表可以随时添加和退出,相对于普通的 DynamoDB表,加入到 Global Tables 的表需要满足(1)所有的表属于同一个 AWS 账号(2)表的名字和键必须都一致(3)副本表初始是空的(4)DynamoDB 流功能需要打开(5)表的数据只能全量复制,不可以有筛选的复制。

b2e415ca446a53a6f0ce1bb40cba242a.png

bf945da588c4f80354fa2dfa8972e36b.png

如何设计一个跨区域 DynamoDB 的双活复制架构?

对于复制技术,我们最熟悉的应该是关系数据的主从复制,比如 MySQL 基于二进制日志的主从复制基本架构如下,(1)从库一个线程读取主库的二进制日志,在本地存储上连续写入二进制日志副本,保存在 Relay 日志中(2)另外一个线程,依次读取日志,并重放 Relay日志中的事务到本地的副本中;底层依靠数据库日志,实现日志数据的有序处理和去重(保障独立事务在日志中有且出现一次);另外,由于系统的故障,处理如果出现中断后重启,线程需要知道处理的进度,这里就有一个检查点(Checkpoint)的概念,避免再次处理已经完成的事务,确保事务的正确性。

c82fc3464aa840e3f7428448ad140198.png

    对于 DynamoDB 服务而言,暴露给用户的并不是日志,而是重用了Amazon Kinesis Stream 的事件流。DynamoDB 的数据变更事件流提供客户一个灵活捕获数据表变更的编程方式,而流本身具备高可用、持久化、有序和去重等符合数据准确复制的原则;对于 DynamoDB 流而言,每个底层的存储分区对应流的一个分片,分片数量随着存储分区的变化而动态变化(Kinesis Client Library- KCL库可以自动处理父子分片,以保障数据的有序处理),对于 DynamoDB 表中修改的每个项目,流记录将按照对该项目进行的实际修改的顺序显示,而且保障每个流记录仅在流中显示一次,可以利用 Kinesis 的流处理 KCL 库进行自动的检查点设置和批量处理。

4e9c1f806443cbd3f0bff55e46af2c01.png

        回顾 Netflix EVCache 的跨区域复制的架构,我们发现很多类似的地方,及细微的差异(1)Netflix 利用定制的 EVCache客户端,在数据SET等操作的同时写入了一条变更消息到 Kafka 队列;而 DynamoDB 天然支持数据变更消息的流式存储,用户无感知(2)Netflix 和 DynamoDB 在目标区域都利用 NoSQL 的基本原语进行数据变更回放(3)Netflix 的 Kafka 队列中仅仅保存消息,没有数据本身,需要的时候直接读取 EVCache 内存数据,而 DynamoDB 流中可以直接读取变更消息和变更前后的数据(4)两者最核心的都是如何实现一个高效、高可用、低成本的跨区域“复制”组件。

c41bcf27c2b29802ac50a076e648d694.png

        虽然我们日常使用不需要太关注 Global Tables 的实现细节,但透过 AWS reinvent 的相关资料,我们可以更好的理解该功能模块的基本原理客观理解该功能对我们全球业务扩展的架构影响,下图是资源链接中 AWS 专家在介绍整个 DynamoDB 底层原理时披露出来的 Global Tables 核心架构,可以看出来设计一个可靠、可用、可维护并可以对客户开放的跨区域“复制”系统并不是一件容易的事情:

49b2fdc98c1f2aecfdbe5f947147b1cf.png

0810440936b85b3f0ffdfe05076c5627.png

多区域分布式系统的技术挑战

目前为止,我们可以理解到,我们要设计一个跨个 AWS 区域的多副本支持多主的 NoSQL 数据库,这不仅让我想到,2007 年亚马逊发表的那篇著名的 Dynamo 论文里面提到的实现一个分布式高可用的数据库技术挑战和应用的技术:

55eae8359de9938a1bb5005116e0bf8c.png

        数据多副本互相复制,对应的就是支持多写,多写的场景下,如何计算数据更新的全局顺序是我们实现“最后”写入有效的冲突处理的前提;另外,为了保障意外未知错误下,多区域副本的最终一致性,我们需要一个机制来对比各副本之间的差异,并将所有副本快速高效恢复到同一个状态;如上表所示,接下来我们来看看,版本向量(Version Vector)和梅克尔树(MerkleTree)在实现我们跨区域“复制”组件多主读写架构可能的运用。

279e38defc157d5aa786dffc8e0a51d9.png

时间戳、顺序和冲突处理

Global Tables 支持多副本多写意味着每个数据条目版本都会带一个时间戳,如果遇到同时写入同一个数据条目,那么带最新时间戳的条目最终会成功写入;那这里会有个非常关键的问题就是时间戳是如何赋值、传递和比较的?如下图,在 Region A中,应用新建了一条记录,在没有多区域多主的功能时,表的记录本身不需要保存时间戳,但考虑到复制组件会接受来自其他区域的数据条目,该条目会携带时间戳,当写入该条目的时候,理想情况是直接利用 DynamoDB 的条件写功能,只有当本地记录的时间戳比复制条目小的情况下,才会更新成功;那第一个考量点就是时间戳由哪个组件来分配最合理?

927721be3f9ae30585c6e1940e481af7.png

数据条目的时间戳是否可以从 DynamoDB Stream 流中的流数据条目中的事件发生时间赋值呢?还是数据更新请求到达 DynamoDB 的第一层 “RequestRouter” 后即被赋予一个时间戳?或者修改 DynamoDB 的客户端代码,在 PutItem 操作时,应用层赋值该时间戳?AWS 底层实现对于我们而言是一个黑箱,我们这里就不过多猜测,假定我们解决了每个数据条目都有时间戳的问题,那接下来我们就可以直接比较两个时间戳的前后顺序的吗?

f4ed089aede27b862a22f1d8bda3ca7f.png

        仔细琢磨下,我们会发现不同区域分配给数据条目的时间戳要能直接比较,那就是大家对标的是全局同一个物理时钟,但不同机器在同一个时间点取得的物理时钟值是有误差的,这个误差可以利用 NTP 来控制,不过误差级别也会在毫秒级;实际生活中物理时间有统一标准比如国际标准时间 UTC,但分布式系统中是缺乏一个标准的全局物理时钟的,根据 Dynamo论文,可以窥探亚马逊解决这个问题的思路是逻辑时钟,再利用时钟向量算法计算一个全局有序的事件,这样就可以确定两个事件的发生的前后顺序;

分布式系统里面一个很重要的概念是逻辑时钟,是1978年 Leslie Lamport 提出的 Lamport Timestamp 时间戳表示法,分布式系统中按照是否存在节点交互分为三类事件:(1)节点内部事件(2)发送事件(3)接受事件:事件 A happens-before 事件 B 记作 A->B,表示所有进程或节点同意事件 A发生在 B之前;因此三类事件的顺序关系就很容易定义出来(1)如果同一个进程中发生了事件 A和 B并且 A发生在 B之前,那 A->B(2)如果进程 P1发送一条消息 M1给到进程 P2,A代表发送 M1的事件,B代表接受 M1的事件,那么 A->B,由于消息传递需要时间(3)-> 具有传递性, A->B 并且 B->C 那么 A->C

c404816e14f81592003f7eff7cde1b1a.png

逻辑时钟的算法基本逻辑如下:(1)每个节点本地维护一个逻辑时钟LCi,通常初始化为0 (2)每个本地事件发送,LC= LCi+1,该时间戳就是事件发送的逻辑时钟(3)当节点i发送消息M给节点j的时候,把逻辑时钟放到消息里(4)当节点j收到消息M时,LC= max( LC, Mtimestamp ) + 1。如上图所示,假定三个进程,C发送消息给到 B,B 收到 B1消息时,在 0和 1(C1 带的时间戳)取大值再加1,因此  B1的时间戳是2;B2紧随 B1在同一个进程B中发送,因此时间戳加1,B2的时间戳变成 3;按照算法类似推导出其他消息的逻辑时钟值。该算法可以通过A->B表达,A的逻辑时钟比B事件的逻辑时钟要小,反过来,只比较两个事件的逻辑时钟无法判断事件A和B的先后,比如上图中的 C3和 B4,逻辑时钟值一样大,这种并发的场景,单纯的逻辑时钟算法无法处理。这种并发关系的表达可以用版本向量(VersionVector) 也被称为Vector Clock来解决;

cec7d8d65c320a0f351208605a1fc7c4.png

        和逻辑时钟类似,不同的地方是节点不仅仅记录自己的逻辑时钟,同时还记录其他节点的逻辑时钟,所以每个节点都用一个向量来保存这些信息;判断事件发生先后逻辑类似,假设有事件 b和 c发生在节点 B和节点 C上,版本向量记作 Tb和 Tc,如果 Tb[B] > Tc[B] 并且 Tb[C] >= Tc[C],那么事件 c 发生在b之前,记作 c->b;比如上图B的第三个事件(B:3,C:1)和C的第二个事件(B:3,C:2)就是类似的先后发生的事件关系;同时版本向量还可以表达同时并发的事件,定义如下,如果 Tb[B] > Tc[B] 但 Tb[C] < Tc[C],那么 b和 c同时发生,记作 ab,比如上图B中的第4个事件 b4(A:2,B:4,C:1)和 C的第3个事件 c3(B:3,C:3),其中 b4[B] > c3[B], 但 b4[C] < c3[C]。

4.1 逻辑冲突和部分更新的竞争场景

虽然技术上Global Tables确保了哪个数据条目最后更新事件会最终成功写入,但从业务逻辑上如果不做设计还是可能存在逻辑冲突;比如下图所示:

  1. 区域A 的应用在 T0 时刻更新了 Count 值为1,同时该值也通过复制组件成功复制到了 区域B,此时此刻,区域A和B中的 Count 值都为1

  2. 这个时候,区域A 和 B之间网络发生故障

  3. 网络故障期间,区域B 在 T2时刻成功更新了 Count 值为4

  4. 网络故障期间,区域A 在 T3时刻成功更新了 Count 值为8

  5. 网络恢复,区域B 会把最新的 Count值及时间戳传播到区域A的复制组件,但由于相对比区域A 本地的 Count值复制过来的时间戳比较早,所以写入失败,区域B 的Count值保持为8

  6. 区域A 也会将最新的 Count值 传播并发送给 区域B,由于时间戳比区域A 的 Count值时间戳要晚,所以,区域A 的 Count也更新成了 8

这样的更新在业务层面有可能逻辑上是有问题的,但目前系统没有给出明确的通知机制给到客户来感知并定制处理。

b8b69c0ec2741dbbf6dc13d432d95eb1.png

另外一个可能导致业务数据不一致情况是,Global Tables不支持部分字段更新,流里面的保存的数据条目是完整条目包含所有字段信息,如下图所示的样例更新条目;那如果应用几乎同时更新了两个区域的副本的同一个数据条目的不同字段,就有可能导致跟实际业务含义不一致的最终结果;比如A应用在区域A将商品订单的消费者地址从“上海” 改成 “南京”,A应用几乎同时在区域将同样的商品订单条目的下单时间从10月1日14点” 改成了10月1日15点”,业务上期待的正确结果应该该商品订单的内容,消费者地址变更为“南京”,下单时间变更为10月1日15点”;但是根据Global Tables的复制原理,区域A和区域B的更新都会互相复制,又由于是对同一条记录作修改,那最后的结果是按时间戳来判断,订单内容可能是“上海”10月1日15点”,或“南京”10月1日14点”

4.2 时间戳、顺序、冲突处理小结

        我们回溯经典的 Dynamo 论文,关于多副本多写的技术挑战,其中很重要的一块就是如何判断全局数据更新事件的顺序,从而引申到分布式系统的物理时钟,逻辑时钟原理探讨,为了解决逻辑时钟无法识别并发事件的问题,我们进一步拓展了版本向量的原理,以及如何识别和表达并发事件;在 Global Tables 目前的描述中,当系统发现对同一个数据条目的并发更新冲突的时候,采用了非常简化的时间晚的更新覆盖时间早的更新处理方法。另外,如果没有设计业务上的约束,双写的结果由于 Global Tables 的网络分区故障或不支持部分更新的限制,最终的结果可能会出现业务一致性问题。

2074266abe7f9f35f24c9d9fe478a03d.png

逆熵-未知错误下的差异性检测和同步

逆熵(Anti-Entropy) 是 Dynamo 论文中阐述解决发生未知错误下 DynamoDB 多个本地存储副本快速后台差异比较并同步达成一致的技术;同样对于一个多区域的多个副本同步情况,更有可能发生未知的不可自动恢复的错误,此时也可以利用同样的技术来手工触发多个区域副本之间的差异比较和快速后台同步。逆熵是利用梅克尔树来对比多个副本之间的差异,梅克尔树是一个 Hash 树,叶子节点存储的是数据条目,父节点是各自子节点所在键空间的 Hash 值;更多细节详情请参考梅克尔树和 Dynamo 论文等相关资料。

a671d39887129db88e24d9b05f195c1e.png

有实际客户案例吗?

其实早在 GlobalTables 原生功能出来之前,很多客户基于 DynamoDB Stream 和 Lambda 根据自己的业务需求实现过跨区域的表数据同步,比如2016年的 re:invent 大会上,Under Armour 为了满足 1880万全球会员的移动用户体验,将数据分为匿名数据和 PII(Personallyidentifiable information) 数据两类,PII 数据只能落在会员的 Home Region,而匿名数据可以全球多区域复制,而数据存储就在Amazon DynamoDB,整个架构基于 Labmda,CloudWatch,DynamoDB Stream等原生服务,在他们介绍经验的时候已经稳定运行1年之久,没有发生任何异常警报,美东和欧洲爱尔兰区域之间延迟在 50ms 左右,和日本区域之间 150ms 左右;在 2016年的公开演讲中,他们正在计划实施多区域多写以满足多区域的容灾需求。

6bce63c30f03969074454c8fc5cdce5c.png

d669631b91703ce9adc8eddbd305b692.png

总结

Global Tables (全局表) 的发布使得客户可以零成本(本身功能免费)快速规划和测试全球多区域分布的应用数据同步需求,虽然 Global Tables 本身简化了多主的冲突处理,但实际应用落地还是需要结合业务数据分类和读写规则,避免业务数据不一致的情况出现,或者可以有独立的逻辑来判断最终一致性的数据是否业务一致。另外,为了让客户更加理解 Global Tables 的能力,本文基于公开资料解析了部分实现多区域多主架构的技术挑战及 Dynamo 论文推荐的技术原理,该功能本身还在不断持续优化中,请关注官方资料了解更多详情。

参考资料:

  • Amazon'sDynamo 论文:

    • https://www.allthingsdistributed.com/2007/10/amazons_dynamo.html

  • 论文Time Clocks and the Ordering of Events in a DistributedSystem : https://lamport.azurewebsites.net/pubs/time-clocks.pdf

  • 论文 Logical Physical Clocks and ConsistentSnapshots in Globally Distributed Databases: https://cse.buffalo.edu/tech-reports/2014-04.pdf

  • 客户案例分享Cross-region replication with Amazon DynamoDBStreams 2016

  • AWS re:Invent 2018: Amazon DynamoDB Under theHood: How We Built a Hyper-Scale Database (DAT321)


客户案例系列:

  • 客户案例分析: Airbnb - 如何分析和衡量分布式支付系统的交易完整性

  •  客户案例分析:Airbnb单体到微服务改造之旅(1)

  •  客户案例分析:Airbnb单体到微服务改造之旅(2)

  • 客户案例分析:Netflix 全球多区域多活架构演进

近期原创:

  • 微服务失败重试(1)消息中间件自动重试和失败处理

  • 微服务失败重试(2)AWS 消息服务选型对照表

  • 微服务架构开发和平台演进

  • 当领域驱动设计(DDD)遇到亚马逊创新公式(下)

  • 当领域驱动设计(DDD)遇到亚马逊创新公式(中)

  • 当领域驱动设计(DDD)遇到亚马逊创新公式(上)

  • RDSMySQL 自定义数据库如何做压测?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值