Amazon DynamoDB是一种完全托管的 NoSQL 数据库服务,旨在实现可扩展性和高性能。它是 SaaS 公司最常用的数据库之一。我们选择 DynamoDB 的原因与其他所有人相同:自动扩展、低成本、零停机时间。但是,在规模上,DynamoDB 可能会出现严重的性能问题。
SaaS 应用程序通常遵循多租户架构,这意味着每个客户都会收到一个软件实例。在规模上,由于 Amazon DynamoDB 中的数据分区不均匀,这通常会导致热键问题,这可以通过两种允许系统扩展的解决方案来解决。将 Amazon DynamoDB 用于多租户解决方案时,您需要知道如何有效地对租户数据进行分区,以防止随着应用程序随时间扩展而出现性能瓶颈。
本文讨论了早期 SaaS 公司在实现高速增长时面临的一个潜在问题,以及可用于应对 Amazon DynamoDB 后续挑战的两种解决方案。
一个朴素的分区键和嘈杂的邻居问题
在流量高峰期间,可能会遇到从 DynamoDB 引发的几个读写限制异常。经过进一步调查,应该清楚的是,受限制的 DynamoDB 错误与请求流量中的大峰值相关。
要了解原因,我们需要了解 DynamoDB 项目的存储方式以及服务实施的硬性限制。首先,重要的是要记住 DynamoDB 跨多个分区存储您的数据。DynamoDB 表中的每个项目都将包含一个主键,其中包含一个分区键。此分区键确定该项目将存在的分区。最后,我们必须注意,每个分区最多可以支持 3,000 个读取容量单元 (RCU) 或 1,000 个写入容量单元 (WCU)。如果“热”分区超出这些硬服务限制,则表的整体性能可能会降低。
第一次使用 DynamoDB 进行开发时,通常很想实现一个使用租户 ID 作为其分区键的数据模型,这会引入“嘈杂的邻居问题”。当一个租户(或一组租户)发出大量请求时,系统将从其分区中获取大量记录。在用户快速增长的情况下,您可能会看到来自某些租户的请求量出现显着峰值。因此,请求的这种峰值最终会限制整个表,这会减慢系统速度并可能影响所有用户。
如果您在 DynamoDB 实例中发现了这个问题,下一步是寻找解决方案来克服应用程序的性能瓶颈或停机风险。虽然租户 ID 是一种自然但幼稚的分区键设计,但由于上述服务限制,它根本无法扩展。因此,您需要一种能够支持更高吞吐量的新分区键设计。使用 DynamoDB 进行多租户数据建模有两种理想的策略。这两种关键设计策略都支持一组严格的访问模式,并且应该根据您的应用程序的要求来实施。
随机分区键策略
增加吞吐量的第一个也是最明显的策略是使用随机分区键。对于不经常更新的租户数据,此策略使我们基本上可以完全回避写入吞吐量问题。例如,考虑一个需要将租户的传入请求存储到 Dynamo 表中的应用程序。假设 request_id 是随机的且具有高基数,则分区键将具有相当高的读写吞吐量。每个请求本质上都有自己的分区,因此我们可以实现每秒 3000 次单独的请求项读取(假设项为 4KB 或更小)。对于像 API 请求这样的实体来说,这是非常高的读取吞吐量,并且可以很好地服务于我们的用例。
这种分区键设计的主要缺点是不可能获取给定的所有项目,这是多租户应用程序的常见访问模式。然而,对于项目不需要多次更新的用例,随机分区键提供了本质上无限的写入吞吐量和非常可接受的读取吞吐量。
分片分区键策略
为了能够检索给定租户的所有项目,您需要将租户的分区拆分为多个较小的分区或分片,并将其数据均匀地分布在这些分片中。这种分区分片技术需要一些重要的特性。在项目写入时,您需要能够从给定范围计算分片键时间。分片功能需要具有高基数并且分布良好。如果不是,那么您仍然会得到一些热分区。分片范围和项目大小将最终决定您的最终吞吐量。最后,在读取项目时,您遍历已确定的租户分区的分片范围,并检索给定租户的所有项目。
例如,假设我们希望能够为给定租户支持每秒 10,000 次写入。如果我们可以保证每个项目是 4KB 或更小,并且我们假设分片范围为 10,那么我们可以使用一个简单的随机数生成器,给定我们选择的范围,来
export const getHashFronRange = (range: number) > { const min = 1; const math = Math.floor(range); return Math.floor(Math.random() * (min - max + 1)) + min; };
然后在项目写入时,我们只需计算租户的分片并将其附加到分区键。
const shard = getHashFronRange(PARTITION_SHARD_RANGE); await put({ Item: { pk: `${tenantId}/${shard}`; sk: Date.now(); // ...rest item ConditionExpression: "attribute_not_exists(pk)", TableName: JOB_TABLE, });
其他注意事项
- 首先考虑访问模式和应用程序要求
- 将项目大小保持在 4KB 以下,并将有效负载卸载到 S3
- 当心 GSI 及其限制表格的能力
文章评论