DynamoDB 物理分区
物理分区是 DynamoDB 实际存放你数据的单位:一片 SSD,跨可用区复制,保存你键空间的一个切片。你的表是一个逻辑上的东西。分区才是字节——以及吞吐量限制——真正所在之处。
DynamoDB 分区是如何运作的?
DynamoDB 把你的表存放在多个物理分区上——跨可用区复制的 SSD 切片。每个分区上限约为 10 GB、每秒 3,000 个读取单位、每秒 1,000 个写入单位。你的的哈希决定一个条目落在哪个分区上,而 DynamoDB 会随着分区增长或变热而自动分裂它们。
- 每个分区上限约为 10 GB 存储、每秒 3,000 个读取单位、每秒 1,000 个写入单位。这些上限是每分区的,不是每表的。
- 你的的哈希挑选分区。键相同的条目落在一起;单个热——或者一个单调递增的排序键——就是把某一个分区钉死的东西。
- DynamoDB 替你分裂分区——按大小,也按持续的热度——包括在排序键边界上分裂一个键的条目集合,除非一个 LSI 或一个不断递增的排序键挡住了它。
- 在还有富余容量时限流就是那个信号。当你的表处在 5% 使用率时出现
ProvisionedThroughputExceeded错误,说明单个分区已经打满了。
一个条目如何找到它的分区
DynamoDB 把你的分区键值喂给一个内部哈希函数。哈希输出挑选物理分区。同一个键进去,同一个分区出来——每一次都是。
从 SQL 过来,这没有对应物。没有你要调优的索引 B 树,没有你手工指派的分片键。放置是一个你不控制、也永远看不见的哈希。
共享同一个分区键的条目构成一个,一起存储并按排序键排序。这就是为什么对一个键的 Query 是便宜的——它读取一个分区上一段连续的区间。(参见 Query vs Scan。)
拿一个游戏的对局事件存储来说。表的键是 arenaId(分区)和 eventKey(排序):
# Item
arenaId = "ARENA#7f3a"
eventKey = "EVT#1719100800#a91c"
playerTag = "Nightjar"
dmgDealt = 412
竞技场 7f3a 的每一条事件都哈希到同一个分区,并按排序键顺序堆叠。对"读取这场对局的时间线"很棒。但如果那一个竞技场揽下了全部流量,就是个隐患。
每个分区都强制执行的三个上限
单个分区被设计为最多提供:
| 上限 | 每分区 | 计为 |
|---|---|---|
| 存储 | ~10 GB | 原始条目字节 |
| 读取容量 | 每秒 3,000 个读取单位 | 1 RU = 一次 4 KB 的强一致读取 |
| 写入容量 | 每秒 1,000 个写入单位 | 1 WU = 一次 1 KB 的写入 |
来源:AWS《分区键设计最佳实践》指南。
条目大小会放大这套数学。一个 20 KB 的条目每次强一致读取耗费 5 个读取单位,所以一个分区在限流之前只能服务约 600 次这样的读取/秒——不是 3,000。写入成本每 1 KB 向上取整,读取成本每 4 KB 向上取整。
陷阱在于:这些是_分区_限制,不是_表_限制。你的表可以预置为 40,000 WCU,却仍然限流,因为所有写入都在猛捶一个上限为 1,000 的分区。
分区如何分裂
DynamoDB 在两种情况下自动增加分区。你从不运行任何命令。
按大小分裂。当一个分区填充到接近 ~10 GB 时,DynamoDB 把它的键范围一分为二,并把一半条目移到一个新分区。存储透明地增长;你的读取和写入全程照常工作。
为热度分裂。当一个分区承受接近其吞吐量上限的持续流量时,DynamoDB 分裂那个热键范围,好让每一半都落在自己的分区上。AWS 把这叫作 split-for-heat(为热度分裂)机制。那些自行停下的短暂限流突发,往往意味着 split-for-heat 起效了——尽管短暂的尖峰也可能只是突发容量耗尽了。
分裂在众多键之间腾出空间,而 split-for-heat 甚至能在排序键的某处切开一个键的条目集合。它无法分散的,是单个热_条目_、一个不断递增的排序键,或一个被 LSI 钉住的集合。
为什么一个热键能赢过分裂器
这里是那个陷阱。分裂重新分配的是分区键的_范围_。如果你的流量集中在一个键值上,每个请求都哈希到同一个分区,就没有剩下的范围可分了。
如果竞技场 7f3a 是一场锦标赛决赛,拉着每秒 4,000 次写入,而其他每个竞技场都空闲着,你就会在 1,000 处限流——而 split-for-heat 在这里救不了它,因为带时间戳前缀的 eventKey 是单调的,所以每一次新写入都落在一个狭窄排序键范围的前沿,没有可切下来的东西。较新的 KeyRangeThroughputExceeded 限流原因恰恰点名了这件事:是一个分区的键范围、而非整张表,超出了它的限制。
修复在数据模型里,不在容量滑块上。对热键做写分片:追加一个小后缀,让一个逻辑竞技场铺开到 N 个物理分区上。
arenaId = "ARENA#7f3a#3" # shard 0..9, chosen per write读取随后在各分片间扇出,并在客户端合并。你可以在动一行应用代码之前,用 DynamoDB 表达式构建器对每个分片原型化键形状和 Query。
一个细微之处:LSI 例外
有一种情况下,存储_确实_是按分区键封顶的。没有时,一个条目集合会跨越它服务自身存储字节和吞吐量所需的任意多个分区——数十亿个排序键值都没问题。
加上一个 LSI,一个分区键的整个集合就必须装进单个 10 GB 的分区里,因为 LSI 共享它。这就是 GSI vs LSI 里讲到的每 PK 悬崖——也是大多数团队转而选择 GSI 的又一个原因。
设计得让分区保持凉爽
你真正能控制的杠杆是分区键。挑一个相对于行数有许多不同值的键,好让流量均匀铺开。(更多模式见单表设计。)
- 高基数键。一个每用户或每租户的键,胜过一个人人同时猛捶的每天或每状态的键。
- 留意已知的热键。一个"当前锦标赛"或"今天"的值,在你上线之前就是一个集中风险,而不是之后。
- 对不可避免的热键做分片。当一个键必须承受超额流量时,加个后缀是标准的逃生舱。
在还有富余容量时限流,就是你的信号:某一个分区热了。检查那个倾斜的条目集合,并在 DynoTable 里演练一套分片键布局——把它指向你自己的表,在 SQL Workbench 里对分区键做 GROUP BY,看清哪些键占了主导,并在它把你叫醒之前建模好修复方案。