Amazon DynamoDB 批量执行器(第二部分):我们付出惨痛代价才学到的经验教训

Amazon DynamoDB 批量执行器(第二部分):我们付出惨痛代价才学到的经验教训

💡 原文英文,约3000词,阅读约需11分钟。
📝

内容提要

本文分享了基于AWS Glue构建DynamoDB批量执行工具Bulk Executor的经验,该工具实现无服务器并行,支持find、delete、count等命令。设计取舍包括:用Spark DataFrame简化命令开发,但因连接器不支持删除且丢失数据类型,部分命令改用直接API调用;用CloudWatch Live Tail实时输出,需解决采样、乱序和延迟问题;通过错误累加器避免重复堆栈;强制启用PITR以便回滚;自建分布式限流器;并用AI辅助测试和代码检查。

🔎

延伸解读

成本估算的实用策略

文章详细介绍了Bulk Executor的成本估算方法。对于读取,工具利用表元数据(约6小时更新一次)计算全扫描所需的读请求单位,并按需调用定价API。对于预置模式,由于成本受预置水平、自动扩展余量和作业时长影响,工具采用按需价格的三分之二作为启发式估算,理由是预置模式在100%利用率下成本约为按需的30%,但实际利用率通常较低,取67%可避免低估。删除命令则分两阶段估算:先估算读取成本,再根据匹配项数量估算写入成本。

实时监控的挑战与应对

使用CloudWatch Live Tail实现近实时输出时,遇到了三个问题:每秒最多显示500个事件,超出部分会被采样导致输出不完整;事件可能乱序到达;AWS Glue作业完成后日志仍有几秒延迟。解决方案包括:将大量输出写入S3,终端只显示前10条结果和链接;构建重排序层;实现“微波炉爆米花监控”,在几秒无新事件后才停止监听。这些措施确保了输出的完整性和顺序性。

错误处理的优化模式

为避免大量重复堆栈跟踪淹没终端,工具采用了四种模式:客户端在启动作业前验证参数,提前捕获拼写错误;在Glue内部使用Spark累加器收集异常,仅打印第一个错误;客户端通过模式匹配识别无法自恢复的错误(如AccessDeniedException)并立即停止作业;使用特定异常类型避免简单错误产生堆栈跟踪。最终,每次执行都会明确指示成功或失败,并用颜色编码(红色错误、黄色警告)提高可读性。

直接API调用与连接器的取舍

AWS Glue连接器不支持删除操作,且会丢失数据类型(如不区分字符串集合和字符串列表)。因此,删除命令必须使用直接DynamoDB API调用,通过batch_writer批量删除。fill和load-export也改为直接写入。此外,scancount命令使用分段扫描和Select: COUNT,比基于连接器的count更快,因为减少了数据加载开销。但代价是scancount接受原始过滤表达式而非Spark SQL语法。diff和copy则为了保持数据类型保真度而采用直接读取。

❓

Q&A

Bulk Executor 为什么选择 AWS Glue 而不是其他方案?

Bulk Executor 选择 AWS Glue 是因为它同时提供了无服务器并行能力和内置的 DynamoDB 连接器,让团队无需管理基础设施就能获得分布式计算和数据库读写支持。其他考虑过的方案包括 Amazon SQS 与 Lambda、AWS Fargate、Amazon EMR 和 Ray,但 AWS Glue 让很多复杂工作开箱即用。

Bulk Executor 如何估算 DynamoDB 操作的成本?

对于读操作,Bulk Executor 查看表元数据(约每6小时更新)获取近期大小,计算全扫描所需的读请求单位。按需表直接调用 Pricing API 获取每百万读请求单位的价格相乘;预置模式则采用启发式方法,取按需价格的三分之二(除以1.5),因为预置表在100%利用率下成本约为按需的30%,但实际很少达到,50%利用率时约为60%,取67%是为了报高一点。删除命令分两阶段:先估算读成本,找到匹配项后再根据删除集大小估算写成本。填充命令先生成样本项了解大致大小再定价。不估算 AWS Glue DPU 成本,因为难以简单预测,且对于大作业 DPU 成本通常只占 DynamoDB 成本的一小部分。

Bulk Executor 如何处理用户按 Ctrl+C 终止作业?

Bulk Executor 的 Python 客户端代码会捕获中断信号。中断处理器看到 ^C 后设置一个中断标志,一个始终运行的监视循环会注意到这个标志,并主动指示 AWS Glue 作业停止,然后客户端程序干净退出。这样一次 ^C 就能同时停止客户端和远程作业,避免远程作业继续运行直到超时。

为什么 Bulk Executor 强制要求启用 PITR?

因为像 delete 这样的命令有可能删除超出预期的数据。Bulk Executor 要求任何接受变更命令的表都必须启用时间点恢复(PITR),这样如果 --where 子句没有按预期工作,还可以回滚。这也是构建 revert-export 命令的原因之一,该命令基于 PITR 驱动的增量导出来撤销写入。

Bulk Executor 如何解决 CloudWatch Live Tail 的采样和延迟问题?

Live Tail 每秒最多显示500个事件,超过则只流式传输采样事件,导致大结果集输出不完整。解决方案是将大输出(如 find 或 diff 的结果)写入 Amazon S3,只将前10个结果流式传输到终端,并附上完整数据集的链接。此外,事件可能乱序到达,因此构建了重排序层;AWS Glue 作业可能在最终日志事件出现前完成(有几秒传播延迟),所以实现了“微波爆米花监视器”:只有在几秒静默后才停止监听。

Bulk Executor 如何避免大量重复的错误堆栈跟踪?

Bulk Executor 采用四种模式:1) 客户端在启动 AWS Glue 作业前验证参数,拼写错误在本地捕获;2) 在 AWS Glue 内部创建 error_accumulator(Spark 累加器),每个任务将异常添加到累加器,Spark 驱动只提取第一个错误并打印一条干净消息,然后优雅关闭作业;3) 客户端持续通过模式匹配器检查 Live Tail 事件流,发现无法自行解决的错误模式(如 AccessDeniedException、ModuleNotFoundError、OutOfMemoryError)时立即触发作业停止;4) 使用特殊的应用特定异常类型,让命令在想要给出错误而非堆栈跟踪时抛出,只有开发者类型的错误才生成堆栈跟踪。

Bulk Executor 为什么在某些命令中直接调用 DynamoDB API 而不是使用 Spark 连接器?

因为 AWS Glue 连接器不支持删除操作,所以 delete 命令必须直接调用 DynamoDB API。此外,连接器不能完全保留源表的数据类型(例如不区分字符串集合和字符串列表),因此 diff 和 copy 命令也直接读取以确保完美的数据类型保真度。直接访问还带来了性能优势:scancount 命令使用 DynamoDB 分段扫描和 Select: COUNT 直接计数,比基于连接器的 count 更快,因为减少了开销。

Bulk Executor 如何实现分布式限流?

对于直接调用 API 的命令(如 delete、fill、copy、scancount),连接器不提供内置限流,因此 Bulk Executor 构建了一个分布式限流器,协调所有工作节点。用户可以通过 --XMaxReadRate 和 --XMaxWriteRate 参数控制。如果未指定,工具会检查环境,根据账户和表设置及配额推断合适的速率。最近还添加了功能:如果最大读/写速率无法在 --XTimeout(默认1小时)内完成工作,会发出黄色警告,让用户提前调整。

Bulk Executor 如何利用 AI 辅助开发和测试?

Bulk Executor 使用 AI 工具生成压力测试套件,比较 AWS Glue 5 下新旧 DynamoDB 连接器在不同表大小和读写模式下的表现,AI 运行测试数小时并生成比较报告,帮助团队在一次交互式工作会话中完成了迁移。此外,还实现了 AI linter,通过 markdown 规则文件检查代码中难以用单元测试或端到端测试覆盖的方面,例如确保 README 中的 IAM 权限描述、角色创建逻辑和权限检查逻辑三者一致,以及每个命令都实施限流、大输出必须写入 S3 等规则。

🏷️

标签

➡️

继续阅读