TL; DR

  • DynamoDB很棒,但分区和搜索很难

  • 我们建立了交流发电机和迁移服务,使生活更轻松

  • 我们开放了一个边车来索引Elasticsearch中你应该使用的DynamoDB表。 这是代码。

当我们三年多前开始使用Bitbucket Pipelines时,我们几乎没有使用NoSQL数据库的经验。但作为一个希望快速提高质量的小团队,我们决定将DynamoDB作为具有出色可用性和可扩展性特征的托管服务。三年过去了,我们已经学到了很多关于如何使用和如何不使用DynamoDB的知识,并且我们已经构建了一些可能对其他团队有用的东西或者可以被不断增长的平台吸收的东西。

NoSQL还是NoSQL?就是那个问题

首先,关系数据库不是狼人,NoSQL不是银弹。关系数据库多年来一直服务于大规模应用程序,并且它们的扩展范围远远超出许多人的预期。例如,Atlassian的许多团队继续选择Postgres而不是DynamoDB,并且有很多完全正确的理由这样做。希望本博客将重点介绍选择一种技术而不是另一种技术的一些原因。从较高层面来看,它们包括诸如操作开销,表的预期大小,数据访问模式,数据一致性和查询要求等考虑因素。

分区,分区,分区

很好地理解  分区如何工作  可能是使用DynamoDB成功的最重要的事情,并且必须避免可怕的  热分区问题。出现这种错误可能意味着重组数据,重新设计API,全表迁移,或者在系统达到临界阈值时的某个时间更糟糕。当然,对表的分区没有可见性; 你可以根据表的吞吐量和大小来计算它们,但它是不准确的,繁琐的,而且我们发现,如果你设计了分布良好的密钥作为最佳实践开发人员指南,那么这在很大程度上是不必要的  提示。幸运的是,我们花时间了解get get的分区,并设法避免任何这些问题。作为额外的好处,我们现在能够利用自动扩展而无需考虑分区边界,因为即使分区发生变化并且吞吐量在它们之间重新分配,请求仍保持均匀分布。

吞吐量,爆破和节流

DynamoDB表的读取和写入受到 为表配置的吞吐量容量的限制  。吞吐量还决定了表的分区方式,它会影响成本,因此值得确保您不会过度配置。DynamoDB允许在开始限制请求之前在短时间内突破吞吐量限制,并且虽然受限制的请求可能导致应用程序中的操作失败,但我们发现由于默认重试配置,它很少会这样做适用于DynamoDB的  AWS开发工具包。这尤其让人放心,因为   DynamoDB中的自动扩展会因设计而延迟,并且允许吞吐量超过容量,足以发生限制。

仅当实际工作负载持续升高(或抑制)持续几分钟时,DynamoDB自动缩放才会修改预配置的吞吐量设置。Application Auto Scaling目标跟踪算法旨在将目标利用率保持在您选择的值或接近您选择的值。

桌子的内置爆破容量可以缓解突然的短暂的活动高峰。

您仍然希望在超出吞吐量时设置警报,以便您可以在必要时进行相应的监视和操作(例如,自动扩展有一个上限),但我们发现突发容量,默认SDK重试,自动扩展和  自适应吞吐量  非常有效地结合,因此很少需要干预。


图片标题


图像来源

交替器:DynamoDB的对象项映射库

在Pipelines获得绿灯并开始重构可怕的alpha代码之后,我们构建的第一件事就是交流发电机:DynamoDB的内部对象项映射库(类似于ORM)。Alternator从应用程序中抽象出AWS SDK,并提供基于注释,被动(RxJava - 尽管目前仍在使用阻止AWS API,直到SDK的v2变得稳定)接口,用于与DynamoDB交互。它还通过Hystrix增加了断路功能,从而消除了早期版本系统中存在的大部分样板代码。


@Table( name = "pipeline", primaryKey = @PrimaryKey(hash = @HashKey(name = "uuid")) ) @ItemConverter(PipelineItemConverter.class) public interface PipelineDao { @PutOperation(conditionExpression = "attribute_not_exists(#uuid)") Single<Pipeline> create(@Item Pipeline pipeline); }


移民局

当然,DynamoDB表是无架构的。但是,这并不意味着您不需要执行迁移。除了在表中添加或更改属性的典型数据迁移之外,还有许多功能只能在首次创建表时配置,例如本地二级索引,这对于查询和排序除了以外的其他属性非常有用。首要的关键。

管道中的前几次迁移涉及编写定制代码以将大量数据移动到新表并将其与应用程序中经常复杂的更改同步,以支持新旧表以避免停机。我们很早就了解到,迁移策略可以消除大量的摩擦,因此迁移服务就诞生了。

迁移服务是我们为在DynamoDB表中迁移数据而开发的内部服务。它支持两种类型的迁移:

  1. 用于在现有表中添加或修改数据的相同表迁移。

  2. 用于将数据移动到新表的表到表的迁移。

迁移的工作方式是扫描源表中的所有数据,将其传递给变换器(特定于发生迁移)并将其写入目标表。它使用  并行扫描  在表的分区中均匀分布负载,并在最短的时间内最大化地完成迁移,从而实现此目的。

图片标题

然后,表到表的迁移将附加到源表的  流,  以使目标表保持同步,直到您决定切换到使用它。这允许应用程序直接切换到使用新表,而无需在迁移期间同时支持新旧表。

查询DynamoDB:一个心痛和痛苦的故事

DynamoDB通过本地和全局二级索引提供有限的查询和排序功能  。表限制为五个本地二级索引,每个索引与表的主键共享相同的分区键,并且一次只能对一个索引执行查询操作。这意味着在通过电子邮件地址分区的“用户”表上,查询操作只能在电子邮件地址和另一个值的上下文中执行。

全局二级索引以支付第二批吞吐量为代价来移除分区密钥要求,并且仅支持最终一致的读取。两种类型的索引对于许多用例都是有用且充分的,并且管道继续广泛使用它们,但它们不满足某些应用程序的更复杂的查询要求。在Pipelines中,这种需求主要来自我们的REST API,它通常允许客户端同时对多个属性进行过滤和排序。

我们的方案

我们第一次尝试解决这个问题,MultiQuery,已经内置到交流发电机中。通过这种方法,我们查询了多个本地二级索引(LSI)并将结果聚合在内存中,允许我们在单个API请求中对最多五个值(最大LSI数)进行过滤和排序。虽然这在当时起作用,但随着我们的表的大小和指数增长,它开始遭受相当可怕的性能下降,因此您过滤的值越多。在替换多个查询之前,我们的端到端测试存储库(具有大量管道的存储库)上的管道列表页面需要花费几十秒的时间来响应并在分支上进行过滤时始终超时。

搜索DynamoDB内容的常见模式是在搜索引擎中对其进行索引。方便的是,AWS提供了一个  logstash插件,  用于在Elasticsearch中索引Dynamo表,因此我们开始使用此插件创建索引服务,结果令人鼓舞。查询性能大大提高了预期,但logstash插件需要花费大约11个小时来索引700,000个文档。

对logstash插件的一些分析以及我们已经在迁移服务中构建了本质上是高性能索引器的实现使我们用自定义索引器实现替换了logstash插件。我们的索引器主要基于迁移服务的相同扫描/流语义,并利用Elasticsearch的批量索引API,在27分钟内成功扫描了近700万个文档。

用于dynamodb的边车索引器图像来源

Indexer Sidecar

自定义索引器后来被重新打包为sidecar,允许任何服务应用程序无缝索引Elasticsearch中的DynamoDB表。初始扫描和正在进行的流式传输阶段都是高度可用的,并且可以通过租用/检查点机制(为扫描定制,为流的标准Kinesis客户端)进行恢复。


图片标题

我们目前利用 Bitbucket代码搜索团队构建的优秀  Elasticsearch客户端来查询索引,并开始研究内部库,它与交流发电机一样添加RxJava和Hystrix。

这是包含代码和自述文件的repo  。

最后的想法

如果您以前没有使用过NoSQL,那肯定需要转变思维,但总的来说,我们使用DynamoDB的经验是积极的。事实证明,该平台在过去三年中非常可靠(我记不起由此造成的一次重大事故),我们的查询要求带来了最大的挑战。有些人可能会说,首先选择关系数据库是合理的,我不会强烈反对它们。但我们已经设法通过一个解决方案克服了这个问题,这个解决方案大部分都是从日常运营中抽象出来的。从好的方面来说,我们不必在所有时间运行解释查询或处理格式错误的SQL,复杂的表连接或缺少索引,我们并不急于回去。