DynamoDB 的请求路由是如何工作的
你发出的每一次读取或写入,都会先命中一队无状态的请求路由器。 路由器对你的 做哈希,把哈希值映射到拥有该键数据的存储节点, 再把请求转发过去。正是这一跳,让一次按键查找的成本保持一致 —— 无论表里存的是一千个项还是十亿个项。
DynamoDB 的请求路由是如何工作的?
DynamoDB 会将每个请求都经由一队无状态的请求路由器转发:路由器对你的 做哈希,把哈希值映射到拥有该分区的那一个存储节点,再把读取或写入转发过去。路由是键哈希的纯函数,因此一次查找的成本保持一致 —— 无论表里存的是一千个项还是十亿个项。
- 请求路由器是入口大门。 它是一队无状态的路由器,接收你的 请求,对分区键做哈希,然后把它路由到持有该分区的存储节点 —— 无需扫描,也无需了解整张表。
- 分区键决定一切。 路由是分区键哈希的
纯函数 —— 同一个键始终路由到拥有它的分区,所以
GetItem是 O(1),而不是 O(表大小)。 - 一主两副。 一次写入落在该分区的主节点上, 主节点在法定多数(三个副本中的两个)持久化之后确认写入。
- 糟糕的键会击垮这套设计。 一个低基数或 键会把 流量灌向单个节点 —— 路由本身没问题,问题出在你的键上。
先从路由要解决的问题说起
从 SQL 过来,你脑中浮现的是一个查询规划器:它读取统计信息、挑选索引, 也许还会扫描。成本随它触及的数据量而增长。这套模型并不适合 一个必须在任意规模下都以个位数毫秒作答的键值存储。
DynamoDB 的答案是把单项查找变成一次直接寻址,而不是一次 搜索。分区键不是一个你用来过滤的列 —— 它是一个哈希 函数的输入,用来算出_数据在物理上存放在哪里_。没有统计信息,也没有规划器。
这就是你从关系式思维迁移出来时所接受的权衡:你放弃了 临时查询的灵活性,换来了常数时间的寻址。
认识请求路由器
请求到达时,并不会直奔存储。它会先命中一个请求 路由器 —— 一队无状态、可横向扩展的路由器,挡在整个服务的最前面。 (USENIX ATC '22 的 DynamoDB 论文描述了这队请求路由器。)
路由器做三件事,自身不持有任何数据:
- 对照 IAM 鉴权与授权该请求。
- 对分区键做哈希以找到拥有它的分区。
- 把请求转发给该分区的存储节点。
因为路由器是无状态的,服务会在负载上升时增加更多路由器。它们中没有一个是 瓶颈,也没有一个是单点故障 —— 这正是 2007 年 Amazon Dynamo 论文 围绕原始系统所构建的那个特性。
跟随一次读取穿过路由器
设想一张记录无人机机队遥测数据的表。项以 DroneId(分区
键)和 ReadingTs(排序键)为键,带有 BatteryPct、AltitudeM 等属性。
你请求某架无人机在 6 月 23 日的读数:
PK = "DRONE#A19F"
SK begins_with "2026-06-23"
下面是路由器对它做的处理。下方的引导文字自上而下地追踪这个请求 —— 请把它当作一条向下流动的路径来读。
路由器对 DRONE#A19F 做哈希,把它映射到拥有该键的分区,再把
读取转发给该分区的主存储节点,由它返回项。
关键洞见:哈希指向的是_一个_分区,而无论这张表有 多少个分区。路由器从不查看其他分区,所以增加无人机 —— 以及分区 —— 并不会拖慢这次查找。
弄清分区到底是什么
分区是存储与吞吐量的单位。每个分区都有上限(大约
10 GB 加上读/写容量的一个固定切片),当分区超出任一上限时,DynamoDB 就会
拆分它。带有某个给定分区键的每个项一开始都落在一个
分区上;随后的按热度拆分可以按排序键范围切分这个集合(除非
有 LSI 或单调递增的排序键把它钉住),这正是让针对
同一个分区键的 Query 保持廉价的原因。
每个分区都被复制到分散在多个可用区的三个存储节点上:一个主节点 和两个副节点。
| 节点角色 | 处理 | 可提供的一致性 |
|---|---|---|
| 主节点 | 所有写入;强一致读取 | 强(能看到自己的最新写入) |
| 副节点 | 最终一致读取;故障转移 | 最终(可能落后于主节点) |
一次写入交给主节点,主节点在法定多数(三个 副本中的两个)持久化之后确认写入。一次读取会被路由到主节点, 因此它反映最新写入。一次读取可能由 一个尚未追上的副节点来提供 —— 成本减半,可能读到旧值。
点名一个陷阱:热分区键
路由的好坏,取决于你的分区键。哈希会把键均匀铺开,所以如果 你的键具有高基数且流量均匀,负载就会分散到所有 节点上。破坏其中任一属性,你就会得到一个热分区。
假设你用 Region 而不是 DroneId 作为那张遥测表的键。现在
us-east-1 里的每架无人机都共享同一个分区键 —— 于是它们的读写会哈希到同一个
键空间槽位,全都堆到同一个项集合上。路由器仍在完美地
履行它的职责;只是你把整个机队都灌进了单个分区的容量里。
你没法盯着路由器挑选节点,但你_可以_设计出路由良好的键。
当你在
表达式构建器里构建一个键条件时,你放在
PK = … 左侧的分区键,正是路由器将要哈希的那个值 —— 让这个
值保持高基数,正是让读取落在不同节点上的关键。
这如何回扣到你的访问模式
请求路由正是让单表设计
规则不容商量的那套机制:你围绕分区键建模,因为分区键
_就是_地址。这也是为什么 Query 胜过 Scan ——
Query 经由路由器命中一个分区,而 Scan 会依次走遍每一个
分区。
二级索引拥有各自的分区和各自的路由:一个 GSI 由它自己的分区键路由,与基表的分区键无关, 这正是为什么即便表不热,GSI 也可能变热。
后续步骤
设计出能路由到多个节点、而非单个节点的键。在
表达式构建器里草拟 PK = … 条件,看清究竟是哪个值
会被哈希,然后下载 DynoTable,针对你自己的
表运行那些查询,看清每个键条件究竟返回什么。