高级阅读约 3 分钟

DynamoDB 并行扫描

并行扫描把一次 Scan 拆成 N 个独立的 Scan 请求,每个认领表的一个 Segment,从而让多个工作进程同时读取它。它是 Scan API 提供的、唯一一种读取整张表能快过单个分区吞吐量所允许速度的办法。

什么是 DynamoDB 并行扫描?

DynamoDB 并行扫描把一次 Scan 拆成 N 个独立请求,每个通过 SegmentTotalSegments 认领表的一个 Segment,从而让多个工作进程并发读取它。它是 Scan API 提供的、唯一一种读取整张表能快过单个分区吞吐量所允许速度的办法——但它仍然是一次完整读取,所以你要为扫描到的每一个条目付费。

  • 顺序 Scan 一次读取一个分区——它的速度被限制在单个分区的吞吐量上,无论表有多大。
  • Segment + TotalSegments 把读取分片TotalSegments 个工作进程;每个工作进程并行扫描自己那一片。
  • DynamoDB 对做哈希来分配段,所以各片可能一头沉——工作进程更多并不总意味着更快。
  • 它仍然是一次 Scan:你要为读取每一个条目付费,而一次庞大的并行扫描会把表的吞吐量从你实时流量脚下抽干。

为什么顺序 Scan 慢

从 SQL 过来,整表读取感觉像是一次流式操作。在 DynamoDB 里并非如此。表的数据分布在许多物理分区上,但单次 Scan一次一个地走过它们,每页 1 MB。

这意味着一次普通的 Scan 在任一时刻只能从一个分区的吞吐量预算里拉取——即便这张表铺在几十个有空闲容量的分区上。表越大,它爬得越久。(AWS:并行扫描

用 Segment 和 TotalSegments 拆分读取

并行扫描修复了这个瓶颈。你挑一个工作进程数量,把 TotalSegments 设成那个数,再给每个工作进程一个不同的、从零开始的 Segment。每个工作进程发出自己的 Scan;DynamoDB 并发地为它们服务。

Worker 0Scan  Segment=0  TotalSegments=4
Worker 1Scan  Segment=1  TotalSegments=4
Worker 2Scan  Segment=2  TotalSegments=4
Worker 3Scan  Segment=3  TotalSegments=4

每个工作进程仍然用 LastEvaluatedKey 独立分页——它从第一页到最后一页都拥有自己的段。应用把这四条流重新拼接在一起。你现在一次读取四个分区的吞吐量,而不是一个。

一个实算示例:夜间导出

假设你运营一张遥测表 sensor-readings。每个条目是来自某个现场设备的一次读数:

PK = "DEVICE#a83f"          (partition key — the device id)
SK = "TS#2026-06-22T03:14"  (sort key — ISO timestamp)
batteryMv  = 3120
tempC      = 41.8
firmwareTag = "fw-7.2.1"

每晚一个 cron 作业把整张表转储到 S3,供分析数仓使用。对 80 GB 做一次顺序 Scan 要花好几个小时,却几乎没动用你预置的读取容量。于是你把它铺开到八个工作进程上:

Scan  sensor-readings  Segment=0  TotalSegments=8  ConsistentRead=falseScan  sensor-readings  Segment=7  TotalSegments=8  ConsistentRead=false

八个工作进程,八个段,一次整表读取大约快了八倍。如果你只需要最近的读数,加一个 FilterExpression,在行到达线路之前先丢掉旧的时间戳——在表达式构建器里构造并检查那个表达式:

FilterExpression:  begins_with(SK, :today)

DynamoDB 如何把条目分配给段

这里是让人栽跟头的部分。DynamoDB 通过对分区键做哈希把每个条目分配给某个段——不是按行数,也不是按字节数。

所以共享同一个 PK 的每个条目都落在同一个段里。在 sensor-readings 里,DEVICE#a83f 的所有读数都归一个工作进程,无论那个设备有多少个时间戳、或它的有多大。(AWS:并行扫描

sensor-readings对分区键做哈希 0DEVICE#a83fDEVICE#1c20 1DEVICE#9be4 2(空)

各段不均匀。一个工作进程可能拥有三台话痨设备、上百万条读数;另一个可能抽到一片空的。把 TotalSegments 调更高也帮不上忙,如果你的分区键扎堆的话——你只是添了一些闲着的工作进程,干等着那个热的。均匀的键分布才是让这种扇出真正划算的东西。

运行之前先看清读取成本

并行扫描是一次吞吐量事件,不是免费午餐。诚实的问题是"我正要读取这整张表的多大一部分?"——在 DynoTable 运行整表读取之前,它会用一个确认对话框拦住你,显示表的近似大小和条目数量以及一条读取容量警告,然后在读取进行时流式呈现实时的"已扫描条目数"进度,好让那个夜间作业不会给你惊喜。

陷阱,以及何时别费这个劲

  • 吞吐量悬崖。一次高 TotalSegments 的扫描能在几秒内吃光表的全部读取容量,把实时流量饿死。在一张服务用户的表上,用 Limit 参数给每个工作进程限速,或在非高峰时段扫描。(AWS:并行扫描
  • 对某个访问模式来说它仍然是错的工具。并行扫描是用于刻意的整表作业的——导出、回填、迁移。如果你伸手去用它来回答一个反复出现的查询,那是一个建模信号:加一个 GSI,把它变成一次 Query
  • PartiQL 里的 SELECT * 是同一次扫描换了层皮。它会编译成一次顺序 Scan。当你确实需要跨条目分析时——一个 GROUP BY、一个 JOIN、一次聚合——DynoTable 的 SQL Workbench 会在一个有界的结果集上于客户端运行这些,而不是猛捶那张表。
  • 强一致性会让账单翻倍。Scan 默认使用的读取。对于导出,除非每一页都必须反映最新的写入,否则就让 ConsistentRead=false——并且要注意,即便一次强一致的扫描也横跨几分钟,所以它不是一个时间点快照(要那个就用 PITR/Export)。

后续步骤

对键建模,好让日常读取永远不需要扫描——从单表设计Query vs Scan 开始。当一次整表作业确实是正确的选择时,试试 DynoTable,用一个预先的大小与成本警告和实时扫描进度来运行整表读取。

更新于