进阶阅读约 3 分钟

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,或者排序键是不断递增的,那样它就被钉在一个分区上。

给陷阱命名:堆在一个键上的流量

只有当你的访问在各个键之间均匀分散时,吞吐量才是被平均共享的。一旦某一个键拿到不成比例的流量,它就会单独被限流,而表的整体容量却用不上。

经典的热键形态:

  • 名人项——一个人人都在读的用户、商品或租户。
  • 低基数分区键——statuscountrytype。可取值少,就意味着只有少数几个分区在干所有的活。
  • 按时间分桶的键——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),同样这些写入就均匀扇出:

分区键分布

每行一个分区键值。重复某个值即可模拟热点键。

8 个桶8 个键
  • #0
    0
  • #1
    1
  • #2
    1
  • #3
    0
  • #4
    2
  • #5
    2
  • #6
    0
  • #7
    2

这是一个简化的教学用哈希,并非 DynamoDB 真实的内部哈希。DynamoDB 使用一个未公开的内部函数,且分区数会随表的增长而增加 —— 请仅将其用于建立直觉,理解不同的键如何分散、单个热点键又如何堆积。

这是用来建立直觉的教学用哈希,不是 DynamoDB 真正的内部哈希——真正的函数和分区边界是 AWS 内部实现。把它当作“均匀分散对比倾斜”来读,而不是对某个键会落到哪个物理分区的预测。

AWS 把这叫做写入分片,并且正是针对高速率、低基数的键推荐它(AWS —— 使用写入分片)。

这和单表设计背后那种的直觉是同一个——你是为访问模式塑形键,而不是为数据“天然”应该怎么摆而塑形。

让自适应容量去干轻松的那部分

DynamoDB 内置了自适应容量,在 re:Invent 2018 的“Amazon DynamoDB Under the Hood”(DAT401)分享里有讲。它会持续地把表的吞吐量朝着正在承受热度的分区重新分配,并会把一个持续过热的键隔离到它自己的分区上(键级隔离,AWS —— 突发与自适应容量)。

它即时且免费——但它受物理规律约束(自适应容量是怎么工作的)。自适应容量可以在键之间搬运热度,热度切分甚至可以在排序键边界处切分一个过热的项集合。单分区上限只有在以下三种情况下才是绝对的:单个过热的、一个不断递增的排序键、或一张带 LSI 的表——这些情况下一个名人键仍然会被限流。分片是确定性的修复;热度切分则缓慢且看机会,所以别指望等它。

一旦你在某个繁忙的键上看到限流,这就是决策路径:

否,多个键同一前缀单个键上出现限流?单个项过热?给键分片或缓存读取分区键基数低?对前缀做写入分片自适应容量多半能扛住

大多数热分区最终归结为“给键分片”或“让自适应容量吸收它”——图只是告诉你落在哪个分支上。

先诊断,再重设计

你没法修复你看不见的东西。限流会表现为 ProvisionedThroughputExceededException(预置模式),或表现为 CloudWatch 里的 ThrottledRequestsReadThrottleEvents/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,把它们跑在你自己的表上,看看到底是哪些分区在承受热度。

更新于