DynamoDB 限流——为什么会发生,以及如何修复
限流是 DynamoDB 在告诉你某个限制被撞上了——但限制有四种,异常有三种,而针对其中一种成因的修复办法会让另一种更糟。提高表容量对热键毫无用处;切到按需同样对热键没用,而且它还会按自己的规则限流。本指南是那把总伞:你到底撞上了哪个限制、指标如何把它们区分开,以及每种成因各自匹配的修复办法。
DynamoDB 为什么在限流我的请求?
四种有文档记载的成因之一:某一个分区超出了它固定的每分区限制,即每秒 3,000 个读取单位或 1,000 个写入单位(热键——两种容量模式下都会发生);表超出了它预置的 RCU/WCU(预置模式);账户超出了它的区域级吞吐量配额;或者一张按需表在 30 分钟内的增长超过了此前峰值的两倍。修复办法取决于是哪一种,所以在调整任何容量之前先做诊断。
四种限流场景
AWS 自己的排查页面把限流恰好拆成四种情况:
- 键范围(分区)吞吐量超限——两种模式都会。每个分区的设计上限是每秒 3,000 个读取单位和 1,000 个写入单位(分区键文档),而且条目大小也要计入其中。没有任何表级设置能抬高它;只有键设计能把它分散开。这就是热分区那种情况,而且表在被限流的同时,看上去可能利用率低得离谱。
- 预置吞吐量超限——预置模式。消耗量压过了表(或某个 GSI)预置的 RCU/WCU,而那约 5 分钟的突增容量缓冲也已经花光。这里的修复阶梯在容量这一侧:自动扩缩、提高预置量,或者切换模式。
- 账户级配额超限。区域级的账户配额给总吞吐量设了上限——默认每张表 40,000 个读取单位和 40,000 个写入单位,预置模式下每个账户 80,000 RCU 和 80,000 WCU(配额);这些是初始默认值,可以通过 Service Quotas 调整,而按需表没有账户级吞吐量配额。
- 按需最大吞吐量超限。按需能即时容纳最高达此前峰值两倍的量;在 30 分钟内涨过两倍,它就可能限流(按需文档)。新建的按需表开箱就能承受每秒 4,000 次写入和每秒 12,000 次读取。对于一次有计划的阶跃式尖峰(发布、促销、迁移),用暖吞吐量(warm throughput)给表预热,而不是指望爬坡是渐进的。
三种异常,以及那个点名成因的字段
ProvisionedThroughputExceededException——预置模式的容量限流:“你超出了一张表或一个及以上全局二级索引所允许的最大预置吞吐量”。详见专门的错误页面。ThrottlingException——控制平面操作发得太快;以及在按需表上,任何速率过高的数据平面操作(双倍峰值规则背后就是这个异常——参见按需错误页面和 ThrottlingException)。RequestLimitExceeded——账户级的吞吐量限制:属于“联系 AWS Support”的地界,在它的错误页面上有讲。
这三种都被标记为可重试,而且现在都携带结构化的 ThrottlingReason 值,形式是资源 + 操作 + 限制——TableReadProvisionedThroughputExceeded、IndexWriteKeyRangeThroughputExceeded、TableWriteAccountLimitExceeded 等等(错误参考)。要读那个 ThrottlingReason,而不只是异常类名:它点出了资源(表还是索引)、操作方向,以及你撞上的是四个限制中的哪一个——那正好就是诊断结论。文档自己逼出的一处含糊:AWS 的各个页面对账户级限流到底表现为 RequestLimitExceeded、还是一个带 AccountLimitExceeded 原因的 ThrottlingException,说法并不一致,所以把你的处理逻辑挂在 ThrottlingReason 字符串上。
在你被限流之前,是什么在吸收负载
有两个内置机制在软化这些限制,而知道它们的边界正好解释了“昨天还好好的”:
- 突增容量为尖峰保留最多五分钟(300 秒)的未使用读写容量——但 DynamoDB 也可能“不经事先通知”把它用于后台维护,而且 AWS 明确指出这些细节可能变化。别按突增来设计;把它当作运气。
- 自适应容量会自动且即时地把吞吐量转向热分区,并且能把一个被频繁访问的条目隔离到它自己的分区上——但仅限于“流量没有超过表的总预置容量或分区的最大容量”。它重新平衡的是倾斜;它从不抬高 3,000/1,000 的每分区上限,而且当表带有 LSI 时它不会切分条目集合。当前的 AWS 排查页面很倚重 split-for-heat(按热度切分)——分区在持续热度下发生切分——那需要时间,而且对单个热键没有帮助。
从指标上诊断
CloudWatch 把请求和事件分开,而这个区别本身就完成了诊断(指标参考):
ThrottledRequests只要其中任何一个事件被限流,就把这次请求计为一次——一次打在带三个 GSI 的表上的PutItem是一次请求,却是四个写入事件。在一次批量里,只有每一个条目都被限流时它才递增。ReadThrottleEvents/WriteThrottleEvents各自计每一个被限流的事件——一次取 10 个条目的BatchGetItem就是 10 个GetItem事件。要看到某个 GSI 的写入限流,你必须同时用TableName和GlobalSecondaryIndexName去查这个指标——GSI 反压就是这样从表级仪表板里藏起来的。- 更新的按成因区分的事件指标(
WriteProvisionedThroughputThrottleEvents、ReadKeyRangeThroughputThrottleEvents、…AccountLimitThrottleEvents、…MaxOnDemandThroughputThrottleEvents)按同样这四种成因拆分计数——如果你所在的区域提供它们,它们会直接回答“是哪个限制”这个问题。
有一个陷阱:SDK 会自动重试被限流的请求——标准重试模式默认总共尝试 3 次(2026 年那次需要主动启用的重试改版把 DynamoDB 客户端改成 4 次尝试、间隔更紧)。因此轻度限流表现出来的是延迟,而不是错误;要盯限流指标,而不只是你的异常日志。
GSI 反压:指错了表的那种限流
如果有哪个 GSI 承接不了这份写入放大,“DynamoDB 就会限流对基表的写入,以维持数据一致性”(GSI 限流文档)——哪怕基表还有富余容量。异常里的 ResourceArn 指向的是索引,但失败的那个操作是你对基表的写入。每个索引都需要自己的容量规划(以及自己的自动扩缩策略);为什么 GSI 会限流基表写入把其中的机制走了一遍。
让修复办法对上成因
| 成因 | 什么能修复 | 什么不能 |
|---|---|---|
| 热键 / 分区 | 把负载分散开的键设计(热分区);给 split-for-heat 留出时间 | 提高表容量、切到按需 |
| 预置容量 | 自动扩缩、更高的最小值,或者按需 | 只靠重试——它们只会加压 |
| GSI 反压 | 给索引扩容;改用稀疏索引或调整投影 | 给基表扩容 |
| 账户配额 | 通过 Service Quotas 提额 | 表级设置 |
| 按需阶跃式尖峰 | 预热(暖吞吐量);把爬坡摊到 30 分钟以上 | 干等——双倍峰值的基准恢复得很慢 |
在 DynoTable 中操作
大多数自伤式的限流,都始于那些比看上去更贵的读取:一次带过滤器的 Scan 无论如何都要消耗整份读取。DynoTable 的运行前成本预览会在你花掉这笔钱之前显示:一条语句会变成 Query 还是 Scan、它命中哪个索引,以及一份读取成本估算——最便宜的限流修复办法,就是那次你没跑的昂贵读取。Scan 对比 Query 指南讲了两者的区别;免费的条目大小计算器会把一个真实条目换算成上面那些限制所用的 RCU/WCU 数字。
陷阱与后续步骤
- 重试会放大过载。退避已经内建在 SDK 里,但在 SDK 重试之上再套一层紧凑的应用级重试循环,只会把压力成倍地压在那个正吃力的分区上。
- 批量会藏住部分限流。只要还有条目成功,
BatchWriteItem就会返回未处理的条目而不是抛出异常——要检查UnprocessedItems,而不只是异常。 - 表级视图在 GSI 这件事上会撒谎。永远按索引来画限流事件的图;在反压期间,基表的仪表板看上去是干净的。
- 容量层面的修复要花上几分钟;键设计则是永久的。自动扩缩约 5 分钟才反应过来,配额提额要开一张支持工单,而一个热键会跟着你去到每一种容量模式——把力气花在能复利的地方:分区键是怎么工作的。
下载 DynoTable,在每条查询打到你的容量之前,先看到它的 Scan 对比 Query 计划和读取成本。