本文要点
- 仅支持追加操作并完全有序的日志数据结构,是构建分布式系统的关键原语。很多 RDMBS 使用了更改日志(或称为“预写入日志”),它可以提高系统性能,在崩溃后提供 PITR,并可用于分布式复制。
- Amazon DynamoDB 团队实现了以 DynamoDB Streams(一种 Kinesis Data Stream)形式,提供 DynamoDB 的底层更改日志。团队在实践中发现了系统架构的多个趋势。使用 DynamoDB Stream 提供的构建模块,终端工程师可以更高效地实现这些趋势。
- 要正确地构建一个系统,尤其是分布式系统,理解按序处理和去重是关键。在分布式系统中,通常不可能实现全局有序处理。
- 在分布式系统中,尽管系统可以提供“一次有效”(Exact-Once)处理,但是实际上不可能实现“一次有效”的端到端交付。系统通常会使用检查点、去重和幂等过滤器实现。
- 检查点机制需要维护一个指针,指定日志中做读取或处理的最后一个交易,用于表示当前的处理状态。
在 2016 QCon 旧金山大会上,Akshat Vig 和 Khawaja Shams 做了名为“DynamoDB Streams 揭秘”的报告。报告的核心要点在于,提出了仅支持追加操作并完全有序的日志数据结构是构成分布式系统的强大原语。工程师要有效地使用日志,必须理解有序处理、去重和检查点机制等关键原则。在演讲中,他们深入阐述了上述理念,并给出了一些使用 DynamoDB Streams 作为终端用户服务的实例。这些实例有效地展示了 Amazon DynamoDB 数据存储技术所使用的底层更改日志。
Khawaja Shams 是 AWS Elemental 的工程副总。在演讲一开始,他探讨了长期存在于关系数据库技术和日志数据结构之间的关系。很多 RDBMS 都使用了更改日志。例如, MySQL 的二进制日志, PostgreSQL 预写日志等。数据库更改日志可以提高系统的性能、在崩溃后提供 PITR(基于时间点的恢复, Point-In-Time Recovery),并可用于数据复制。更改日志还有效地支持了通过在其它外部主机上复制数据而创建分布式数据库。例如,MySQL 通过二进制日志在主服务器和相关从属机之间复制数据。每个从属机需要维护两个复制线程。一个线程在本地从属机硬盘上连续写入主服务器的二进制日志副本,另一个线程依次读取日志,并将每个事务应用到本地复制的数据库副本。
要构建一个类似于主从复制的基本架构,我们需要了解几个基本原语。第一个也是最重要的原语就是“按序处理”(Ordering)。设想两个事务依次应用于数据库,其中第一个事务写入一个新条目,第二个事务删除该条目,最终导致数据库中没有新数据。如果不能保证按序处理,那么删除事务可能会首先执行(并未导致任何后果),然后才执行写入事务,这会导致数据库中存储了不正确的数据。第二个核心原语是“数据重复”(Duplication),即每个独立事务应该仅在日志中出现一次。如果并未强制按序处理,或是并未预防日志中出现数据重复,这可能导致在主服务器与从属机之间存在不一致。
Shams 提出了一个问题,这些核心原语为什么如此重要?并没有多少工程师会亲自去编写崩溃恢复工具,或者去实现一个分布式数据库。他进而指出,许多工程师的确是在构建分布式系统(例如基于微服务架构的系统),并且通常依靠日志处理和事件驱动架构实现数据共享(例如,通过使用 Apache Kafka 或 Amazon Kinesis )。因此,掌握这些基本的日志处理原语,无疑是至关重要的。
Vig 是一位 AWS 高级软件工程师。他在演讲中介绍了检查点的相关概念。在处理日志时,系统必须保存一个处理指针,用于指向已读取的或已处理的最新事务。检查点本质上表示了当前的处理状态。如果日志处理发生了中断(例如,消费系统发生了崩溃),这时我们可以查看当前的检查点,从其所指示的事务中恢复处理。 这不仅避免了额外做一些工作,并且通过避免再次处理已完成的事务,确保了事务的正确性。
检查点具有多种策略,每种策略都是在特异性和吞吐量之间进行权衡。例如,一种策略是在每个事务处理完成后再设置检查点,对每次最后的读取项设置一个指针。该策略需要为每个数据项写一个检查点,实现的成本很高。另一种策略是定期做检查点(例如每10 次读取后),而非对所有的数据项一视同仁。该策略只关注最近处理项,可降低需要检查点写入的成本一个数量级。
另一方面,在两个检查点之间发生的事务数量越多,那么在发生崩溃时就必须要读取(和处理)更多的事务。Shams 警告说,这不仅需要时间,而且在部分系统中,重新处理事务是不可接受的(这可能会让人感觉时光逆转了)。在此类情况下,唯一可行的策略就是对每个完成处理的事务建立检查点。
Vig 列出了一组趋势,这是 AWS 团队在当前软件体系结构中发现的。第一个趋势是优化全局延迟。当工程师试图实现在不同的地理区域中提供一致的体验时(例如,通过实现数据的跨区域复制),就需要考虑这个问题。有多种方法来可以实现全局延迟的优化。例如,工程师可以在应用中直接构建地理区域复制逻辑,也可以将事务写入到分布式队列中,然后在各个地理区域中做独立处理。但是,这些方法并非治标治本的。随着事务的吞吐量和所支持地理区域数量的增加,复杂性也会随之增加。
第二个趋势是预防出现逻辑腐败,即工程师应尽量防止出现非实质性的应用级问题。例如,如果新发布的应用意外错误地修改了数据,系统应可以提供恢复能力。典型的恢复方法包括使用 PITR 快照,以及使用延迟副本,这些副本维护了数据存储在各个时间点的副本(例如,在多个活动的备用数据库中,有一个数据库曾运行了一个小时,另一个数据库曾运行了两个小时)。当然,这两种方法的实施和维护,显然需要付出一些运营成本。
第三个趋势是对数据灵活查询的需求。工程师常常希望系统中的数据能同时支持 OLTP( OnLine Transaction Processing )和 OLAP( OnLine Analytical Processing )查询。如果在日志中保存一份真实数据,就可以支持对数据做多次处理,并且支持数据以多种方式物化。该方式常见于 ES(事件溯源,Event Sourcing)和 CQRS( command-query responsibility segregation )架构中。其中,数据物化使用了最适合每个查询用例的数据存储技术。例如,在使用图数据库做查询时,会涉及实现连通性或最短路径算法。这些图运算需要在 RDBMS 上使用关系查询实现,或是在键值存储上直接使用以标识符为驱动的检索。
第四个趋势是 EDA(事件驱动架构, event-driven architectures )正日益流行。工程师正越来越多地致力于事件流处理系统的构建中,其中不少是并发系统。FaaS( function-as-a-service )无服务器应用就是此类架构的一个实例。在其中,工程师编写由事件触发的函数,例如写入到对象存储、来自 API 网关的请求等。
此后,Shams 和 Vig 将话题一转,介绍为了让终端工程师可以解决在之前演讲中提出的趋势,AWS 的 DynamoDB 团队是如何构建一些工程师使用功能模块。他们给出的解决方案就是实现了 AWS DynamoDB Streams ,将 DynamoDB 的更改日志以 Amazon Kinesis Stream 形式全面提供给工程师。遵循在前面演讲中提出的原则,DynamoDB Streams 提供了高可用、持久化、按序处理和去重等原语的实现。需要指出的是,在 2017 AWS re:Invent 大会上,Amazon 发布了用于 DynamoDB 跨区域复制的终端用户服务 Global Tables ,并自动化了 DynamoDB 的按需备份。这些服务应该就是使用上述原语构建的。
Shams 和 Big 举例展示了一个使用 DynamoDB Streams 构建的投票应用。通过使用 PUT IF NOT EXISTS
命令,可在 DynamoDB 中实现乐观并发。在投票应用中,乐观并发用于防止出现重复投票问题,该问题可能会由于逻辑错误而意外发生。如果PUT IF NOT EXISTS
命令检测到应用试图做出重复写入的尝试,那么该写操作将会失败,以免生成任何相应的 DynamoDB Streams 条目。为支持投票者在应用中更改投票,应用使用了条件PUT ,指定的条件是不同候选人的标识ID。如果满足条件,那么新的写操作(即更改选票)将会成功,进而生成一个DynamoDB 流条目,其中包含更新的NewImage(指修改后的在整个项)和以前的OldImage(指修改前的整个项)。
应用在使用Kinesis 处理DynamoDB 流时,会调用 Kinesis 客户软件库在日志中做检查点。正如Shams 和Vig 在前面的演讲中所提到的,这可用于防止重复处理数据。Shams 警告说,在分布式系统中,只有当进程在单个系统中生成唯一的序列号时,才可能实现全局按序处理,而这种做法常常会限制吞吐量。应用可以尝试使用时间戳实现全局按序处理,前提是假定了系统中的所有进程访问的是同一个经配置的时钟,该时钟是可靠并且一致的。但是这一假设通常并不成立。这里需要提及,AWS 最近发布了 Amazon Time Sync Service 。该服务提供了“高度精确并可靠的时间参考,Amazon EC2 实例可直接访问”。
但是在系统中,偏序处理的实现(即按序处理 DynamoDB 表中的每个独立条目)相对容易,因为独立条目的互斥信息是写入到 Kinesis 的同一分片上。只要应用能按同一分片中的数据顺序处理数据,即可实现偏序处理。
在结束演讲时,Vig 指出对 DynamoDB Stream 可以使用 At-Most Once(最多处理一次)或 At-Least Once(至少处理一次)语义,可以不仅限于一次。在分布式系统中,尽管系统可以实现“一次有效”(Exactly-Once) 处理 ,但事实上不可能只实现“一次有效”的端到端交付。通常,我们会使用去重或幂等(idempotency)过滤器实现。
InfoQ 提供了演讲“DynamoDB Streams 揭秘”的完整视频。
关于作者
Daniel Bryant 正引领组织和科技领域的变革。他目前的工作内容包括,通过引入更好的需求收集和规划技术增强组织内部敏捷,关注敏捷开发内构架的相关性,促进持续的集成 / 交付。Daniel 目前的技术专长主要是“DevOps”工具,云 / 容器平台,以及微服务实现。他还是伦敦 Java 团体(LJC)的领导者,为开放源码项目提供帮助,为 InfoQ、DZone 和 Voxxed 等知名技术网站撰写文章,并定期出席 QCon、JavaOne 和 Devoxx 等国际会议。
查看英文原文: Demystifying DynamoDB Streams: An Introduction to Ordering, Deduplication and Checkpointing
评论