高级阅读约 3 分钟

DynamoDB 的请求路由是如何工作的

你发出的每一次读取或写入,都会先命中一队无状态的请求路由器。 路由器对你的 做哈希,把哈希值映射到拥有该键数据的存储节点, 再把请求转发过去。正是这一跳,让一次按键查找的成本保持一致 —— 无论表里存的是一千个项还是十亿个项。

DynamoDB 的请求路由是如何工作的?

DynamoDB 会将每个请求都经由一队无状态的请求路由器转发:路由器对你的 做哈希,把哈希值映射到拥有该分区的那一个存储节点,再把读取或写入转发过去。路由是键哈希的纯函数,因此一次查找的成本保持一致 —— 无论表里存的是一千个项还是十亿个项。

  • 请求路由器是入口大门。 它是一队无状态的路由器,接收你的 请求,对分区键做哈希,然后把它路由到持有该分区的存储节点 —— 无需扫描,也无需了解整张表。
  • 分区键决定一切。 路由是分区键哈希的 纯函数 —— 同一个键始终路由到拥有它的分区,所以 GetItem 是 O(1),而不是 O(表大小)。
  • 一主两副。 一次写入落在该分区的主节点上, 主节点在法定多数(三个副本中的两个)持久化之后确认写入。
  • 糟糕的键会击垮这套设计。 一个低基数或 键会把 流量灌向单个节点 —— 路由本身没问题,问题出在你的键上。

先从路由要解决的问题说起

从 SQL 过来,你脑中浮现的是一个查询规划器:它读取统计信息、挑选索引, 也许还会扫描。成本随它触及的数据量而增长。这套模型并不适合 一个必须在任意规模下都以个位数毫秒作答的键值存储。

DynamoDB 的答案是把单项查找变成一次直接寻址,而不是一次 搜索。分区键不是一个你用来过滤的列 —— 它是一个哈希 函数的输入,用来算出_数据在物理上存放在哪里_。没有统计信息,也没有规划器。

这就是你从关系式思维迁移出来时所接受的权衡:你放弃了 临时查询的灵活性,换来了常数时间的寻址。

认识请求路由器

请求到达时,并不会直奔存储。它会先命中一个请求 路由器 —— 一队无状态、可横向扩展的路由器,挡在整个服务的最前面。 (USENIX ATC '22 的 DynamoDB 论文描述了这队请求路由器。

路由器做三件事,自身不持有任何数据:

  • 对照 IAM 鉴权与授权该请求。
  • 对分区键做哈希以找到拥有它的分区。
  • 把请求转发给该分区的存储节点。

因为路由器是无状态的,服务会在负载上升时增加更多路由器。它们中没有一个是 瓶颈,也没有一个是单点故障 —— 这正是 2007 年 Amazon Dynamo 论文 围绕原始系统所构建的那个特性。

跟随一次读取穿过路由器

设想一张记录无人机机队遥测数据的表。项以 DroneId(分区 键)和 ReadingTs(排序键)为键,带有 BatteryPctAltitudeM 等属性。

你请求某架无人机在 6 月 23 日的读数:

PK = "DRONE#A19F"
SK begins_with "2026-06-23"

下面是路由器对它做的处理。下方的引导文字自上而下地追踪这个请求 —— 请把它当作一条向下流动的路径来读。

客户端:QueryPK = DRONE#A19F请求路由器(无状态集群)Hash(DRONE#A19F) 键空间槽位把槽位映射到拥有该键的分区该分区的主节点读取项DRONE#A19F

路由器对 DRONE#A19F 做哈希,把它映射到拥有该键的分区,再把 读取转发给该分区的主存储节点,由它返回项。

关键洞见:哈希指向的是_一个_分区,而无论这张表有 多少个分区。路由器从不查看其他分区,所以增加无人机 —— 以及分区 —— 并不会拖慢这次查找。

弄清分区到底是什么

分区是存储与吞吐量的单位。每个分区都有上限(大约 10 GB 加上读/写容量的一个固定切片),当分区超出任一上限时,DynamoDB 就会 拆分它。带有某个给定分区键的每个项一开始都落在一个 分区上;随后的按热度拆分可以按排序键范围切分这个集合(除非 有 LSI 或单调递增的排序键把它钉住),这正是让针对 同一个分区键的 Query 保持廉价的原因。

每个分区都被复制到分散在多个可用区的三个存储节点上:一个主节点 和两个副节点

节点角色处理可提供的一致性
主节点所有写入;强一致读取强(能看到自己的最新写入)
副节点最终一致读取;故障转移最终(可能落后于主节点)

一次写入交给主节点,主节点在法定多数(三个 副本中的两个)持久化之后确认写入。一次读取会被路由到主节点, 因此它反映最新写入。一次读取可能由 一个尚未追上的副节点来提供 —— 成本减半,可能读到旧值。

点名一个陷阱:热分区键

路由的好坏,取决于你的分区键。哈希会把键均匀铺开,所以如果 你的键具有高基数且流量均匀,负载就会分散到所有 节点上。破坏其中任一属性,你就会得到一个热分区

假设你用 Region 而不是 DroneId 作为那张遥测表的键。现在 us-east-1 里的每架无人机都共享同一个分区键 —— 于是它们的读写会哈希到同一个 键空间槽位,全都堆到同一个项集合上。路由器仍在完美地 履行它的职责;只是你把整个机队都灌进了单个分区的容量里。

你没法盯着路由器挑选节点,但你_可以_设计出路由良好的键。 当你在 表达式构建器里构建一个键条件时,你放在 PK = … 左侧的分区键,正是路由器将要哈希的那个值 —— 让这个 值保持高基数,正是让读取落在不同节点上的关键。

这如何回扣到你的访问模式

请求路由正是让单表设计 规则不容商量的那套机制:你围绕分区键建模,因为分区键 _就是_地址。这也是为什么 Query 胜过 Scan —— Query 经由路由器命中一个分区,而 Scan 会依次走遍每一个 分区。

二级索引拥有各自的分区和各自的路由:一个 GSI 由它自己的分区键路由,与基表的分区键无关, 这正是为什么即便表不热,GSI 也可能变热。

后续步骤

设计出能路由到多个节点、而非单个节点的键。在 表达式构建器里草拟 PK = … 条件,看清究竟是哪个值 会被哈希,然后下载 DynoTable,针对你自己的 表运行那些查询,看清每个键条件究竟返回什么。

更新于