我如何才能为向 DynamoDB 的大规模迁移实现更好的吞吐量?

How can I achieve better throughput for a large migration to DynamoDB?

我一直在 运行测试我们打算在今年夏天在我们的产品帐户中将大数据迁移到 dynamo。我 运行 测试将大约 32 亿个文档批量写入我们的 dynamo table,它有一个散列和 运行ge 键以及两个部分索引。每个文件很小,不到1k。虽然我们成功地在大约 3 天内完成了项目的编写,但我们对所体验到的 Dynamo 性能感到失望,并且正在寻求有关如何改进的建议。

为了进行此迁移,我们使用了 2 个 ec2 实例 (c4.8xlarges)。我们的迁移程序每个 运行s 最多 10 个进程;我们通过一些内部参数在进程之间拆分工作,并且知道某些进程会比其他进程 运行 长。每个进程在我们的 RDS 数据库中查询 100,000 条记录。然后,我们将它们分成 25 个分区,并使用 10 个线程的线程池来调用 DynamoDB java SDK 的 batchSave() 方法。每次调用 batchSave() 仅发送 25 个文档,每个文档小于 1k,因此我们预计每个文档仅向 AWS 发出一次 HTTP 调用。这意味着在任何给定时间,我们可以在服务器上拥有多达 100 个线程,每个线程使用 25 条记录调用 batchSave。在此期间,我们的 RDS 实例很好地处理了对它的查询负载,我们的 2 个 EC2 实例也是如此。在 ec2 方面,我们没有最大化 cpu、内存或网络输入或网络输出。我们的写入没有按哈希键分组,因为我们知道这会减慢 dynamo 写入速度。通常,在一组 100,000 条记录中,它们被分成 88,000 个不同的哈希键。我创建的 dynamo table 最初具有 30,000 的写入吞吐量,但在测试期间的某一时刻配置了高达 40,000 的写入吞吐量,因此我们的理解是在 dynamo 端至少有 40 个分区来处理这个问题。

在此期间,我们在对 dynamo 的 batchSave() 调用中看到了非常多变的响应时间。在我 运行 每个 ec2 实例 100 个线程的 20 分钟跨度内,平均时间为 0.636 秒,但中位数仅为 0.374,因此我们有很多调用花费的时间超过一秒。我希望看到从 EC2 实例到 dynamo 进行这些调用所需的时间更加一致。我们的 dynamo table 似乎配置了足够的吞吐量,EC2 实例低于 10% CPU,网络进出看起来很健康,但还没有达到极限。控制台中的 CloudWatch 图表(相当糟糕...)没有显示任何写入请求的限制。

在我获取这些采样时间后,我们的一些进程完成了它们的工作,因此我们 运行在 ec2 实例上减少了线程。当发生这种情况时,我们发现调用 dynamo 的响应时间得到了显着改善。例如当我们 运行 在 ec2 实例上使用 40 个线程而不是 100 个线程时,每个线程都调用 batchSave,响应时间提高了 5 倍以上。但是,即使增加了更好的响应时间,我们也没有看到写入吞吐量有所提高。似乎无论我们将写入吞吐量配置成什么,我们都没有真正看到实际吞吐量超过 15,000。

我们需要一些关于如何最好地在这样的 Dynamo 迁移中实现更好性能的建议。当然,我们今年夏天的生产迁移将是时间敏感的,到那时,我们将寻求迁移大约 40 亿条记录。有没有人对我们如何实现整体更高的吞吐率有任何建议?如果我们愿意在迁移期间为我们的主索引支付 30,000 个单位的写入吞吐量,我们如何才能真正实现接近于此的性能?

BatchWrite 延迟的一个组成部分是批处理中花费时间最长的 Put 请求。考虑到您必须遍历 DynamoDBMapper.FailedBatch 的列表直到它为空,您可能进展得不够快。考虑 运行 多个并行 DynamoDBMapper.save() 调用而不是 batchSave 以便您可以独立地为您编写的每个项目取得进展。

同样,Cloudwatch 指标是 1 分钟指标,因此您可能会遇到被 1 分钟 window 掩盖的消耗和节流峰值。默认情况下,SDK 将重试受限调用 10 times before exposing the ProvisionedThroughputExceededException to the client, making it difficult to pinpoint when and where the actual throttling is happening. To improve your understanding, try reducing the number of SDK retries, request ConsumedCapacity=TOTAL, self-throttle your writes using Guava RateLimiter as is described in the rate-limited scan blog post 并记录受限主键以查看是否出现任何模式,这一事实使情况更加复杂。

最后,table 的分区数量不仅取决于您在 table 上配置的读写容量单位的数量。它还取决于您存储在 table 中的数据量。一般一个partition最多存放10GB的数据,然后再split。因此,如果您只是写入 table 而不删除旧条目,则 table 中的分区数将无限增长。这会导致 IOPS 饥饿 - 即使您提供 40000 WCU/s,如果由于数据量原因您已经有 80 个分区,则 40k WCU 将分布在 80 个分区中,平均每个分区 500 WCU。要控制 table 中的陈旧数据量,您可以使用限速清理过程来扫描和删除旧条目,或者使用 rolling time-series tables(幻灯片 84-95)和 delete/migrate整个 table 的数据,因为它们变得不那么相关了。滚动时间序列 tables 比限速清理更便宜,因为您不使用 DeleteTable 操作消耗 WCU,而每个 DeleteItem 调用至少消耗 1 个 WCU。