DynamoDB 热分区:如何发现与修复
DynamoDB 把你的数据分散到许多物理分区上,每个分区各自分到一份吞吐量。热分区指的是某一个键吸走的读或写远超它那份能提供的量——于是对该键的请求被限流,而表的其余部分却闲着。
什么是 DynamoDB 热分区?
DynamoDB 热分区是指某一个吸走的读或写远超它那份吞吐量能提供的量,于是对该键的请求被限流,而表的其余部分却闲着。成因是键设计——一个名人项、一个低基数的键、今天的日期——而不是表的大小。解药是分散写入。
- 成因是键设计,不是表大小。某一个把流量集中起来——一个名人用户、一个
status="OPEN"标志、今天的日期——才是那个陷阱。 - 自适应容量有帮助,但它不是根治办法。DynamoDB 会自动重新平衡热度,可单个项或单个键仍然可能超过一个分区所能提供的量。
- 解药是分散写入。给键加入熵(写入分片),或者把热读取路径迁到一个分布更好的访问模式上。
- 从 SQL 过来,这没有对应物。关系表里没有“某一行的索引值太受欢迎”这种概念——DynamoDB 那种扁平的按键吞吐模型才有。
分区为什么会存在
DynamoDB 是 2007 年那篇亚马逊 Dynamo 论文的量产继承者,它用一个分区化、横向扩展的模型换掉了单节点 SQL 模型。数据按分区键的哈希分片到各个物理存储节点上。
每个分区容纳有限的数据量,并提供有限的吞吐量。AWS 记载了一个硬上限:每个分区每秒 3,000 个读取单位、1,000 个写入单位——在 us-east-1,预置模式和按需模式下都是同一个软上限(AWS —— 分区行为)。计费模式不会抬高这个物理上限;它只改变表级开销的计量方式。表级成本用定价计算器算,再用 Contributor Insights 看限流到底是分区受限还是表受限。
那个上限就是全部要点。你表的吞吐量是所有分区之和。一个键的项集合从一个分区开始,热度切分可以在排序键边界处把它切到多个分区上——除非表带有 LSI,或者排序键是不断递增的,那样它就被钉在一个分区上。
给陷阱命名:堆在一个键上的流量
只有当你的访问在各个键之间均匀分散时,吞吐量才是被平均共享的。一旦某一个键拿到不成比例的流量,它就会单独被限流,而表的整体容量却用不上。
经典的热键形态:
- 名人项——一个人人都在读的用户、商品或租户。
- 低基数分区键——
status、country、type。可取值少,就意味着只有少数几个分区在干所有的活。 - 按时间分桶的键——
PK = "2026-06-23"。今天的每一次写入都锤打同一个分区;昨天那个则永远冷着。
从 SQL 过来,这些都无所谓。一个热门值上的 B 树索引没什么问题。而在 DynamoDB 里,那个热门值就是物理放置的单位,所以受欢迎就变成了一道吞吐量悬崖。
一个实例:名人排行榜
假设你运营一个全球游戏排行榜。分数存在一张这样建键的表里:
PK = "BOARD#global"
SK = "PLAYER#<playerId>"
读取取分数前 N 名;写入在每场比赛后更新某玩家的 currentScore。全球榜里的每一行都共享同一个分区键——BOARD#global——所以每一次读和写都落在单个分区上。
再加上一个有两百万在线观众、疯狂点刷新看自己排名的主播,那个分区就冲破了 3,000 读取单位。你会在全球榜上收到 ProvisionedThroughputExceededException,而表里每一个其他排行榜都闲着。
自伤的地方就在 BOARD#global 这次坍缩:你把一个逻辑上的单一排行榜,建模成了一个物理上的单一键。
分散写入:给键做分片
修复办法是制造基数。给分区键追加一个分片后缀,让一个逻辑排行榜扇出到 N 个物理分区上:
PK = "BOARD#global#<shard>" -- shard = playerId mod 10
SK = "PLAYER#<playerId>"
现在写入分散到十个分区而不是一个上——十倍的写入余量。代价是:读取整个排行榜必须命中全部十个分片再合并,因为没有哪个单独的 Query 能跨越分片边界。你用读取的简洁换来了写入的分散。
自己看看差别。把一个重复的键粘进下面的可视化工具,每次写入都落进同一个桶——那就是热分区。加一个分片后缀(BOARD#global#0 … #9),同样这些写入就均匀扇出:
这是用来建立直觉的教学用哈希,不是 DynamoDB 真正的内部哈希——真正的函数和分区边界是 AWS 内部实现。把它当作“均匀分散对比倾斜”来读,而不是对某个键会落到哪个物理分区的预测。
AWS 把这叫做写入分片,并且正是针对高速率、低基数的键推荐它(AWS —— 使用写入分片)。
这和单表设计背后那种的直觉是同一个——你是为访问模式塑形键,而不是为数据“天然”应该怎么摆而塑形。
让自适应容量去干轻松的那部分
DynamoDB 内置了自适应容量,在 re:Invent 2018 的“Amazon DynamoDB Under the Hood”(DAT401)分享里有讲。它会持续地把表的吞吐量朝着正在承受热度的分区重新分配,并会把一个持续过热的键隔离到它自己的分区上(键级隔离,AWS —— 突发与自适应容量)。
它即时且免费——但它受物理规律约束(自适应容量是怎么工作的)。自适应容量可以在键之间搬运热度,热度切分甚至可以在排序键边界处切分一个过热的项集合。单分区上限只有在以下三种情况下才是绝对的:单个过热的项、一个不断递增的排序键、或一张带 LSI 的表——这些情况下一个名人键仍然会被限流。分片是确定性的修复;热度切分则缓慢且看机会,所以别指望等它。
一旦你在某个繁忙的键上看到限流,这就是决策路径:
大多数热分区最终归结为“给键分片”或“让自适应容量吸收它”——图只是告诉你落在哪个分支上。
先诊断,再重设计
你没法修复你看不见的东西。限流会表现为 ProvisionedThroughputExceededException(预置模式),或表现为 CloudWatch 里的 ThrottledRequests、ReadThrottleEvents/WriteThrottleEvents,以及 ReadThrottleEventsForKeyRange/WriteThrottleEventsForKeyRange——那些针对分区上限的专门计数(AWS —— CloudWatch 指标)。
再配上 DynamoDB 的 CloudWatch Contributor Insights,它会直接给你被访问最多的键排名——按名字确认一个名人键的最快方式(AWS —— Contributor Insights)。而如果你还不确定热键到底是不是原因——DynamoDB 会因为四种截然不同的原因限流——那就从限流指南开始,让指标去指认你真正撞上的那个限制。
当你在测试分片后的读取路径时,你会手工为每个分片构建 KeyConditionExpression。用 DynamoDB 表达式构建器 生成它们且不打错字——它会为每个分片发出精确的 PK = :pk AND begins_with(SK, :sk) 形态。
要避开的陷阱
- 不断递增的排序键。一个单调的排序键(时间戳、序列号)会把每次新写入都逼到同一个项集合的同一端,而热度切分帮不上忙——该集合仍被封顶在 1,000 个写入单位。给排序键加入熵,或者给分区键分片。
- 不必要地给读密集路径分片。如果读占主导且项很小,一个缓存或一个键分布更好的 GSI 往往胜过分片那种分散-聚合的读取成本。
- 把热分区和慢
Scan搞混。Scan慢是因为它读取一切;热分区被限流是因为某一个键过载。是不同的问题——参见 Query 对比 Scan。
后续步骤
先画出分片后的键,再拿真实数据验证读取路径。在 DynamoDB 表达式构建器 里构建每个分片的条件,然后下载 DynoTable,把它们跑在你自己的表上,看看到底是哪些分区在承受热度。