进阶阅读约 3 分钟

DynamoDB 热分区

DynamoDB 把你的数据分散到许多物理分区上,每个分区都有自己的一份吞吐量。热分区指的是某一个键吸引的读取或写入远超它那份吞吐量所能承受 —— 于是发往该键的请求被限流,而表中其余部分却闲置着。

什么是 DynamoDB 热分区?

DynamoDB 热分区是指某一个分区键吸收的读取或写入远超它那份吞吐量所能承受,于是发往该键的请求被限流,而表中其余部分却闲置着。根因是键设计 —— 一个明星项、一个低基数键、今天的日期 —— 而不是表大小。解药是分散写入。

  • 根因是键设计,不是表大小。 某个分区键把流量集中起来 —— 一个明星用户、一个 status="OPEN" 标志、今天的日期 —— 就是陷阱所在。
  • 自适应容量有帮助,但不是修复手段。 DynamoDB 会自动再平衡热点,然而单个项或单个键仍可能超出一个分区所能承受的量。
  • 解药是分散写入。 给键加入熵(写入分片),或把热读取路径迁移到分布更均匀的访问模式上。
  • 从 SQL 过来,这没有对应物。 关系型表没有“某一行的索引值太受欢迎”这种概念 —— 而 DynamoDB 那套扁平的、按键计算吞吐量的模型有。

为什么会有分区

DynamoDB 是 2007 年那篇 Amazon Dynamo 论文的生产级继承者,那篇论文用一个分区化、横向扩展的模型换掉了单节点的 SQL 模型。数据按分区键的哈希分片到各个物理存储节点上。

每个分区承载有限的数据量,并提供有限的吞吐量。AWS 文档给出的硬上限是每个分区每秒 3,000 个读取单元和 1,000 个写入单元AWS —— 分区行为)。

那个上限就是问题的全部。你表的预置吞吐量是所有分区之和 —— 但任何一个键永远只落在一个分区上。

点名陷阱:流量堆到一个键上

只有当你的访问均匀分散在各个键上时,吞吐量才会被均匀分摊。一旦某个键拿到了不成比例的流量,它就会独自被限流,而表的整体容量却被闲置。

经典的热键形态:

  • 明星项 —— 某一个所有人都在读的用户、产品或租户。
  • 低基数分区键 —— 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 能跨越分片边界。你用读取的简洁性换来了写入的分布。

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

这与单表设计背后那种键重载的直觉是同一回事 —— 你为访问模式塑造键,而不是为数据“自然而然”该怎么摆放去塑造键。

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

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

它即时且免费 —— 但它受物理规律限制。自适应容量可以在键之间搬运热点;它无法把单个键推过单分区上限。一个真正的明星键仍会被限流。要越过那堵墙,靠的是分片。

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

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

大多数热分区最终都归结为“给键分片”或者“让自适应容量吸收它” —— 这张图只是告诉你身处哪条分支。

重新设计之前先诊断

看不见的东西修不了。限流会表现为 ProvisionedThroughputExceededException(预置模式),或者 CloudWatch 里的 ThrottledRequests 和每分区的 ThrottleCountAWS —— CloudWatch 指标)。

把它和 CloudWatch Contributor Insights for DynamoDB 配合起来用,后者直接给你访问最频繁的键排名 —— 这是按名字确认某个明星键的最快办法(AWS —— Contributor Insights)。

当你测试分片后的读取路径时,你会为每个分片手工构造 KeyConditionExpression。用 DynamoDB 表达式构建器生成它们而不打错字 —— 它会为每个分片输出精确的 PK = :pk AND begins_with(SK, :sk) 形态。

要避开的暗坑

  • 自增或单调递增的键。 把顺序 ID 和时间戳当作分区键,会把连续的写入路由到同一个分区。加一个哈希前缀。
  • 不必要地对读取密集型路径分片。 如果以读取为主且项很小,那么缓存或者一个带更均匀分布键的 GSI,往往胜过分片那种分散-收集的读取成本。
  • 把热分区和慢 Scan 混为一谈。 Scan 慢是因为它读取所有东西;热分区被限流是因为某一个键过载了。这是不同的问题 —— 参见 Query 对比 Scan

下一步

把分片后的键勾勒出来,然后用真实数据验证读取路径。在 DynamoDB 表达式构建器里构建每个分片的条件,并下载 DynoTable,对着你自己的表运行它们,看看实际是哪些分区在承受热点。

更新于