假设你手里有一张几千万行的 DynamoDB 表,想一次性把三个月前的过期数据全删掉,或者把两张表逐条比对找出差异。直接写脚本扫全表?单线程能跑到天荒地老。开一堆 EC2 自己搞并行?运维成本又让人头大。这正是 Bulk Executor 这个开源工具要解决的问题,而它的作者们把整个构建过程踩过的坑都写了出来,读下来像是一份 AWS Glue + DynamoDB 的实战避雷指南。

先交代背景。Bulk Executor 是个命令行工具,让你能对表里的大量条目批量执行操作,比如按条件批量删除、统计、比对两张表是否一致。它的设计目标很朴素:跑批量命令要像跑本地脚本一样爽——实时输出、能按 Ctrl+C 随时中断;要能扩展到几百个并行 worker 而不用你管基础设施;还要方便加新命令,不能和框架绑死。

选型是第一道坎。作者对比了 SQS + Lambda、Fargate、EMR、Ray 之后,最终选了 AWS Glue。原因很实在:Glue 是 serverless 的,提交一个 job 就能自动把活儿分发给成百上千的 worker 并行跑,而且自带 DynamoDB 连接器,读写数据几乎零成本。代码里几行就能把 DynamoDB 表读成 Spark DataFrame,或者把 DataFrame 写回表。DataFrame 天然支持 where 过滤、orderBy 排序、limit 截断,于是第一个命令 find 几乎没写什么逻辑就做出来了——你只要敲一句 `./bulk find --table orders --where "status = 'undelivered'" --orderby timestamp desc --limit 100`,就能打印出最近 100 条未发货订单。count 和 delete 命令复用同一套查找逻辑,delete 只是找到之后并行发批量删除请求而已。

不过连接器也有坑:它不会完整保留源表的数据类型,比如字符串集合和字符串列表在它眼里没区别。如果你依赖类型精确性做复杂处理,这里得留个心眼。

成本估算是个容易想简单但实际很麻烦的事。作者想在动手前先告诉用户“这活儿大概要花多少钱”。读操作还好办:查表元数据(注意这个数据大约每 6 小时才更新一次,刚灌过数据的表可能被低估),算出全表扫描需要的读请求单位数。按量付费模式下,调 Pricing API 拿到每百万读请求单位的价格,一乘就完事。预置模式就麻烦了,成本取决于预置的吞吐量、自动扩缩容的余量和 job 运行时长。作者的启发式做法是直接按按量付费价格的 2/3 估——理由是预置 100% 利用率时成本约为按量付费的 30%,但现实中很难跑满,50% 利用率时成本约 60%,取 67% 是宁可报高一点。写操作的成本更棘手,delete 命令得先扫描才知道有多少条匹配,所以它分两阶段报价:先估读成本,找到匹配集合后再按集合大小估写成本。fill 命令(按自定义生成器批量灌合成数据)则是先生成几个样本算算条目大小再定价。

有个细节很聪明:作者故意不估 AWS Glue 的 DPU(数据处理单元,就是 Glue 计算资源的计费单位)成本。原因一是算不准,二是他们观察到对于大 job,DPU 费用相比 DynamoDB 费用只是零头——一个每小时几块钱的 Glue worker 能驱动海量数据库操作。小 job 里 DPU 占比高些,但绝对金额很低。工具里留了个 `--XWaitForDPU` 参数,跑完可以看实际 DPU 花费。

中断处理是另一个容易翻车的地方。用户看到估价太高想按 Ctrl+C 取消,如果只是让 Python 客户端脚本退出,AWS Glue job 还会在后台继续跑直到超时,钱照样烧。作者的解法是让 Python 代码捕获中断信号,设置一个 interrupted 标志,一个一直在跑的 watcher 循环(平时监视 Glue 状态变化)发现这个标志后,主动去停掉 Glue job,再干净地退出客户端。这样按一次 Ctrl+C,两边都停。

最让我觉得值得借鉴的是他们对数据安全的执念。delete 这种命令万一 where 条件写错了,可能删掉远超预期的东西。所以他们硬性要求:任何接收变更操作的表必须开启 PITR(时间点恢复,就是能让你把表恢复到过去任意时刻状态的备份机制)。这样即使删错了,也能用配套的 revert-export 命令基于 PITR 增量导出来原地回滚。这是把“操作可撤销”当成了默认前提,而不是事后补救。

日志和输出这块也全是坑。第一版客户端轮询 CloudWatch Logs 拿 Glue worker 的输出,又慢又延迟。换成 CloudWatch Live Tail(实时日志流)后,输出能近实时地流到客户端,但新问题来了:Live Tail 每秒最多显示 500 条事件,超出部分只抽样。结果就是 find 返回大量结果时输出不完整。解决办法是把大输出(find 或 diff 的结果)写到 S3,终端只流式显示前 10 条,附上完整数据集的链接。Live Tail 还有两个意外:事件可能乱序到达,所以他们加了一层重排序;另外 Glue job 可能已经结束但最后几条日志要过几秒才出现在 CloudWatch 里,所以不能 job 一停就停止监听。他们实现了一个“微波炉爆米花监听器”——等日志安静几秒后才真正停止。

错误处理也做了优化。Glue 任务出错时默认把堆栈跟踪写进日志,如果只是单个任务出错,客户端能看到完整堆栈。但如果是几百个任务同时出错,日志会被刷爆。作者的处理是让客户端能快速区分“单点错误”和“大规模失败”,避免在海量日志里捞针。

整篇读下来,最打动我的是作者那种“每个决定背后都有真实代价”的坦诚。选 Glue 是因为它把最难的部分(serverless 并行 + DynamoDB 连接器)免费给了你,但代价是数据类型被简化、日志有延迟、Live Tail 有速率限制。成本估算没有追求精确,而是用启发式给出一个“宁可高估”的数字。强制 PITR 看似增加了用户负担,但换来的是误操作可回滚的安全网。

这套思路对你有什么用?如果你在做任何“对海量数据执行批量操作”的工具或脚本,这里有几个可以直接抄的作业:一是选型时优先考虑“自带连接器的 serverless 并行服务”,能省掉大量基础设施工作;二是任何破坏性操作前先估算成本并强制开启可恢复机制(PITR 或快照),把“手滑”的代价降到最低;三是实时日志流看着美好,但一定要考虑速率限制和乱序问题,大结果集走对象存储 + 流式预览前几条是稳妥做法;四是中断处理要同时覆盖客户端和远端任务,否则用户按了 Ctrl+C 以为停了,远端还在烧钱。

坑都是踩过才知道的,这篇的价值就在于把别人踩过的坑提前标了出来。如果你正准备用 AWS Glue 或 Spark 处理 DynamoDB 数据,花十分钟读读原文,能帮你省下好几个小时的调试时间。

阅读原文 → 返回 AI 技术文档

内容与图片版权归原作者所有 · 原文: https://aws.amazon.com/blogs/architecture/bulk-executor-for-amazon-dynamodb-part-2-lessons-we-learned-the-hard-way/