高级阅读约 3 分钟

从 Dynamo 论文到 DynamoDB

2007 年的《Dynamo: Amazon's Highly Available Key-value Store》论文,与 你今天调用的 DynamoDB 共享一个名字和一个目标 —— 在任意 规模下都可预测的性能 —— 但它们不是同一个系统。论文描述的是一个 内部的、最终一致的存储,由你自己运行。DynamoDB 则是一个托管服务,它 保留了经验教训,却扔掉了大部分机制。

DynamoDB 是基于 Dynamo 论文的吗?

部分是。DynamoDB 从 2007 年的 Amazon Dynamo 论文取用了它的名字和核心目标 —— 在规模上可预测的性能和高可用性 —— 并几乎原封不动地保留了哈希的思想。但它是一个不同的托管系统:论文中的向量时钟、gossip 成员管理,以及可调的读/写法定人数都已消失,被 AWS 自有的内部机制所取代。

  • 论文解决的是可用性,而非易用性。 它的任务是在 假日流量高峰期间绝不拒绝一次写入,哪怕代价是返回一次陈旧的读取。
  • DynamoDB 保留了形态,替换了内部机制。 按键的哈希 分区、跨可用区复制、水平扩展 —— 但冲突解决的 内脏(向量时钟、gossip、读修复)已经不在了。
  • 你不再调节旋钮了。 论文里的 NRW 变成了一个 选择:ConsistentRead 为真或为假。其余由 AWS 掌管。
  • 心智模型依然有回报。 了解这段血脉能解释为什么 Scan 昂贵、为什么 GSI 读取会滞后 —— 两者都源自最初的设计。

论文实际上在解决什么

Amazon 的购物车不能宕机。一个在负载下拒绝 写入 —— 或在某个副本失败时阻塞 —— 的关系型数据库是无法接受的。2007 年的 Dynamo 论文选择了可用性优先于一致性:总是接受写入, 之后再调和分歧。那个取舍是下文一切的根源。

要在没有单一主节点的情况下做到这一点,Dynamo 必须自己回答两个问题: 一个键住在哪里,以及一次读取或写入生效之前必须有多少副本达成一致?

一致性哈希:一个键住在哪里

论文把每个节点放在一个哈希环上。一个键的位置是它的 键的哈希;它由顺时针方向的下一个节点拥有,并复制到随后的 N-1 个节点。添加或移除一个节点只会重新洗牌它邻居的键 —— 而非 整个数据集。这就是一致性哈希,也是 DynamoDB 几乎 原封不动保留的那个思想。

DynamoDB 仍然对你的做哈希,以决定由哪个物理分区存储 该项目。选一个低基数的分区键 —— 比如只有两个值的 STATUS —— 那么每个具有相同值的项目都会落到同一个分区。那就是这个大坑,也是那个环的直接后果:哈希把 相同的键送往相同的归宿。

法定人数:多少副本必须一致

论文的第二个旋钮是法定人数(quorum)。有 N 个副本时,一次写入一旦 有 W 个副本确认就算成功,一次读取会咨询其中 R 个。设 R + W > N,那么任何读取 都至少与一个持有最新写入的节点重叠 —— 强一致性。把它们设得 更低,你就用新鲜度换取速度和正常运行时间。

Dynamo 运行“宽松的”法定人数:如果目标节点宕机,写入会去往一个 替身,并在之后交还(提示移交,hinted handoff)。冲突的版本会被 标上向量时钟,并在读取时由应用来调和。

DynamoDB 保留了什么、又改变了什么

DynamoDB 继承了目标和分区方式,然后删去了那些让 原始系统难以运维的部分。

关注点2007 年 Dynamo 论文今天的 DynamoDB
键的放置一致性哈希环分区键的哈希 → 托管的分区
复制N 个节点,由你选择3 份副本跨可用区,由 AWS 固定
一致性旋钮RW 法定人数调节一个标志:ConsistentRead
冲突解决向量时钟,读取时应用侧合并区域内无需 —— 写入通过一个主副本串行化;只有全局表跨区域时才最后写入者胜出
成员管理对等节点间的 gossip 协议完全托管;对你不可见
多键操作无 —— 纯键值Query、GSI、事务叠加在其上

论文的 API 是两个调用:get(key)put(key, value)。DynamoDB 在同一个 键值内核之上添加了排序键、索引和查询 —— 这就是为什么 Query 廉价(一个分区),而 Scan 不廉价(它走遍那个环 曾经创建过的每一个分区)。

一次写入如何行进,今昔对比

下面的流程对比了论文的法定人数写入和 DynamoDB 的托管式写入。 形态相似;责任从你的代码移到了 AWS。

论文:N、R、W 由你调DynamoDB:固定 3 AZ 副本put(key, value)将键哈希到环上写入 N 个副本收到 W 个确认?读取时用向量时钟合并主副本串行化写入,法定人数隐藏

在论文里,你掌管法定人数的运算和合并;在 DynamoDB 里,整个下半部分 都是托管的,你只需为每个请求选择 ConsistentRead

这段血脉在哪里渗入你的代码

最终一致性的默认设置就是论文的透出。一个全局二级 索引是异步复制的,因此一个刚写入的项目可能会有片刻从索引中 缺失 —— 同样是那笔“之后再调和”的交易,只是发生在索引 层。参见 GSI 与 LSI,了解那种滞后何时会有影响。

你可以用两种方式买回强一致性。在基表读取上使用 ConsistentRead: true (它会路由到主副本),或者用一个 ConditionExpression 守卫一次写入, 使它只有在项目当前状态匹配时才落地。在 DynamoDB 表达式构建器里草拟一个 —— 例如 attribute_not_exists(PK),把一次 PutItem 变成只做插入的操作,即 论文中冲突检测的现代替身。

唯一要记住的一件事

论文为“永不对写入说不”而优化。DynamoDB 继承了那种 偏向,这就是为什么它的默认设置偏爱可用性,以及为什么强一致读取成本 更高。为单分区 Query 建模你的键,就像 单表设计里那样,并 只在你确实必须时才动用 Scan —— 那个环 让一次全表遍历如它听起来那般昂贵。

试用 DynoTable,浏览你的表及其 GSI,然后在 SQL Workbench 里对你自己的数据运行 JOIN 和 GROUP BY。

更新于