mcpField
A
amazon-dynamodb

amazon-dynamodb

github.com/aws数据与数据库3.3k5.2k 次安装今天更新
代码仓库

关于 amazon-dynamodb

这项跨主机技能用于设计和验证 DynamoDB 数据层,面向 Agent 和开发者。它将建模原则与可选的成本估算、AWS 部署、基准测试、报告和资源清理流程结合起来。

主要功能

  1. 01设计访问模式与架构
  2. 02根据数据模型估算成本
  3. 03部署临时 AWS 资源
  4. 04执行基准测试并生成报告
  5. 05生成待审核的清理脚本

适用场景

  1. 01规划 DynamoDB 应用数据层
  2. 02估算建模工作负载成本
  3. 03通过 AWS 基准测试验证设计

MCP 相关

该技能建议使用 AWS MCP 服务进行 AWS 交互,但并非必需;脚本也可通过环境凭证使用 boto3 和 AWS CLI。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/aws/agent-toolkit-for-aws --skill amazon-dynamodb

安装后在 Claude Code 中输入/amazon-dynamodb确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
cost-model-schema.md
loop-state-schema.md
performance-model-schema.md
performance-report-format.md
reference-architecture.md
vector-search.md
scripts/
benchmark_lambda.py
benchmark_model.py
calculate_costs.py
deploy_model.py
find_skill_dir.sh
generate_perf_report.py
generate_teardown.py
iterate_design.py

详细介绍

DynamoDB 公理

本文档是一组面向 DynamoDB 应用的设计公理。它旨在供不了解应用其他背景信息的 Agent 阅读,并用于制定有理有据的数据层设计。

护栏 — 此技能自身文件的位置(MCP 与本地安装)

此技能可以通过两种方式加载,而它们会从不同位置解析技能自带的文件。读取参考资料或运行脚本之前,请先确定技能的加载方式:

  • 通过 AWS MCP retrieve_skill 工具加载: 此技能未安装在本地文件系统中。你必须通过带有 file 参数的 retrieve_skill 获取每个参考资料或脚本(例如 file="references/vector-search.md" 或 file="scripts/calculate_costs.py"),并 运行返回内容中的脚本。不要在本地使用 file_read 读取这些路径——它们在磁盘上不存在。
  • 本地安装(例如 .kiro/skills/amazon-dynamodb/ 或 ~/.claude/skills/amazon-dynamodb/):使用相对路径从本地技能目录读取和运行文件。

此区别仅适用于技能自身打包的文件。用户数据和会话产物始终从用户的工作目录读取,并写入该目录。切勿通过 retrieve_skill 获取或写入客户数据。

无论这些文件位于何处:此技能在验证设计、部署临时表或执行基准测试时,都会调用真实的 AWS API。在可用时,建议使用 AWS MCP 服务进行这些 AWS 交互——它能在不同主机之间提供一致的凭证处理和区域解析。但这不是必需的:此处的每个脚本都使用 boto3 和 AWS CLI,通过当前环境的凭证链访问 AWS,因此即使不存在 MCP 服务,此技能也能照常使用。

下文中的 ${SKILL_DIR} 解析适用于本地安装的情况:通过 MCP 加载时,不存在可供解析的目录,而且此技能提供的脚本无法从返回的字符串中执行——因此,估算成本或进行实时验证需要采用本地安装形式。

解析技能自身的路径

此技能与宿主无关,可在 Claude Code、Kiro、Codex、Cursor、普通终端或 CI 中运行。它在磁盘上的位置取决于宿主(~/.claude/skills/…、~/.kiro/…、~/.codex/…、~/.cursor/…、仓库检出目录或其他位置)。Agent 的工作目录是用户的项目,而不是技能包,因此像 scripts/calculate_costs.py 这样的相对路径无法解析。在本文档中,${SKILL_DIR} 表示包含此 SKILL.md 文件的目录的绝对路径(技能根目录,其中包含 scripts/ 和 references/)。

每个会话解析一次 ${SKILL_DIR},然后重复使用。 选择适用于你的宿主的第一个可行方法:

  1. 你已经知道路径。 你是通过某个路径加载 SKILL.md 的,${SKILL_DIR} 就是该文件所在的目录。这是最可靠的来源,应优先使用。

  2. 环境变量。 如果设置了 $DDB_SKILL_DIR,则信任它。

  3. 捆绑的解析器(与宿主无关,不依赖任何宿主假设)。它会搜索常见的安装根目录,并通过哨兵文件验证匹配结果,因此绝不会悄无声息地返回错误目录:

    # If you already know the path to the script, just run it directly:
    #   SKILL_DIR="$(sh /path/to/amazon-dynamodb/scripts/find_skill_dir.sh)"
    # If you don't, this host-neutral one-liner searches the common roots
    # (~/.claude, ~/.kiro, ~/.codex, ~/.cursor, ~/.config, ~/.local/share, $PWD):
    SKILL_DIR="$(find "$HOME" "$PWD" -maxdepth 7 -type f -name SKILL.md -path '*amazon-dynamodb*' 2>/dev/null \
                 | head -1 | xargs -I{} dirname {})"
    # Verify it before trusting it (sentinel check), then hand off to the resolver
    # for its loud-on-failure diagnostics:
    SKILL_DIR="$(sh "$SKILL_DIR/scripts/find_skill_dir.sh" 2>/dev/null || echo "$SKILL_DIR")"
    

解析器会打印经过验证的技能根目录并以状态码 0 退出;或者不打印任何内容并以非零状态码退出,同时给出修复提示。因此,成功时可以放心使用 SKILL_DIR="$(sh …/find_skill_dir.sh)"。它是纯 POSIX sh,在不同宿主上的行为完全一致。

解析完成后,导出该变量,让后续命令都能直接使用,并让脚本也能读取它:

export DDB_SKILL_DIR="$SKILL_DIR"
python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.md

脚本会在内部相对于自身定位同级文件(其他脚本、scripts/benchmark_lambda.py),因此你只需要根目录路径,无需指定每个脚本的单独路径。

规则:

  • 始终使用绝对路径调用脚本($DDB_SKILL_DIR/… 形式)。不要 cd 到技能目录——用户的工作目录必须保持不变,以便他们的产物(dynamodb_data_model.json、cost_report.md 等)生成在预期位置。
  • 如果以上三种方法都无法解析目录,请停止并询问用户技能的安装位置,不要猜测。错误的 ${SKILL_DIR} 会导致后续出现令人困惑的“找不到文件”错误;问一个澄清问题代价更低。

流水线一览

每个阶段对应一个工具。默认流程不会访问 AWS:大部分工作都在阶段 1 中完成(你可以通过对话讨论并完善设计)。 阶段 2(成本)会在用户请求时或设计即将定稿时运行,而不是每轮都自动运行。阶段 3–6 是一个明确选择加入、开销较大的分支,会创建真实的 AWS 资源并产生实际费用;只有在用户明确同意后才能进入。每个阶段的详细约定见最后一列所列的章节。

#阶段命令(执行 export DDB_SKILL_DIR=… 后)读取写入是否访问 AWS?章节
1设计(无脚本——你生成访问模式列表和架构)—(回复中的产物)否要生成的产物
2成本python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.mddynamodb_data_model.jsoncost_report.md否成本估算
3部署python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest-out created_resources.json --yes-deploy模型 + 配置created_resources.json是实时验证
4基准测试python3 "$DDB_SKILL_DIR/scripts/benchmark_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest created_resources.json --raw-out perf_raw.jsonl --summary-out perf_summary.json模型 + 配置 + 清单perf_raw.jsonl、perf_summary.json是实时验证
5报告python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model dynamodb_data_model.json --summary perf_summary.json --output performance_report.md模型 + 摘要performance_report.md、design_findings.json否实时验证
6拆除python3 "$DDB_SKILL_DIR/scripts/generate_teardown.py" --manifest created_resources.json --out teardown.sh → 审核 → bash teardown.sh --confirm清单teardown.sh是(使用 --confirm 时)实时验证第 6 步
—迭代python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" …(将 3→4→5→成本整合为一轮由人驱动的流程)模型 + 配置 + 循环状态 + 清单loop_state.json + 上述产物是(需通过门控)迭代设计循环

谁读取什么。 你(Agent) 读取精简的产物:cost_report.md、design_findings.json、loop_state.json。用户 读取 performance_report.md。切勿读取 perf_raw.jsonl(体积较大)——它仅供阶段 5 使用。

授权门控。 阶段 3 及之后需要 --yes-deploy;如果费用将超过 cost_guardrail_usd,基准测试会拒绝继续,除非指定 --allow-spend;运行 bash teardown.sh --confirm 前,必须先由用户确认已审核,并明确表达执行意图。详情见实时验证。

AWS 访问权限(推荐使用 MCP,但并非必需)。 阶段 3–6 会与 AWS 交互(创建表、Lambda 和 IAM 角色,然后执行基准测试并拆除资源)。为了获得最佳的 AWS API 调用体验,推荐使用 AWS MCP 服务,但并非必需——这里的每个脚本都直接使用 boto3,可在普通 Shell 中搭配标准 AWS 凭证(配置文件、SSO 或环境凭证)运行,因此无论是否使用 MCP 服务,这项技能的运行方式都相同。此技能不依赖任何 MCP 专用工具。

如何使用这些公理

  1. 当任务是设计、审查或评估完整应用的数据层时(多实体架构、多表布局、与流、搜索、通知的端到端组合),请先阅读参考架构。 ${SKILL_DIR}/references/reference-architecture.md 展示了一个完整的多租户看板任务管理 SaaS(“TaskBoard”),部署在 AWS 上并由 DynamoDB 提供支持,涵盖并论证了所有相关组件(Cognito、CloudFront、HTTP API、Lambdas、Streams、OpenSearch、AppSync Events、SQS/EventBridge、级联操作、幂等中间件)。公理告诉你必须满足什么;参考架构则展示了这些组件在实践中如何配合。对于多表设计,如果不阅读它,你会错过参考架构中的一些模式,而仅凭公理很难推导出来——幂等中间件、幻影 upsert 防护、AppSync 通道授权、将通知作为 EventBridge 而非表的决策,以及通过分块 BatchWriteItem 实现级联删除。只有小范围问题才可跳过这一步——单表问题、查询成本计算、明确的调试问题。
  2. 在应用以下任何公理之前,先生成访问模式列表(见下一节)。每条建模公理都假设该列表已存在;如果没有它,就无法应用询问“此模式是否频繁?”或“此查询会返回什么?”的公理。
  3. 生成 要产出的成果 中列出的产物。这些是设计的输出,而不是中间笔记。公理用于塑造产物;产物才是 Agent 要交付的内容。
  4. 将 模式 一节与公理一并应用。模式不是公理,而是参考架构具体呈现、且设计需要用到的关键实现细节,即使没有任何公理明确要求它们。
  5. 当两条公理指向相反方向时,按照冲突解决顺序处理。正确性优先于运维必要性,运维必要性优先于成本,成本优先于风格。
  6. 遇到术语歧义时,请查阅术语表。不要猜测。
操作规范:宣布、执行、依据证据验证

这适用于技能的每个阶段,在会产生费用或创建资源的阶段尤其重要(部署、基准测试、拆除,以及任何会产生开销的操作)。始终按以下顺序执行三个步骤:

  1. 宣布。 在执行会产生副作用或费用的操作之前,明确说明该操作将做什么——会创建什么、花费多少、会更改什么、会删除什么。用户不应对资源、费用或删除操作感到意外。
  2. 执行。 运行命令。对于长时间运行的命令(具有代表性的基准测试可能需要运行数分钟),以单次阻塞调用的方式运行并等待其完成——参见 实时验证 第 4 步。
  3. 依据证据验证,然后只陈述证据支持的内容。 执行操作后,通过刚刚生成的产物确认结果——检查文件内容和修改时间、命令的实际标准输出或最新数据;不要凭预期或记忆判断。命令“应该”写入文件,并不能证明文件确实写入了;请打开文件检查。结论只能说到眼前证据所支持的程度。如果无法指出最新证据,就说明这一点并停止——不要推断结果。 这能防止将过时或臆想的输出当作真实结果呈现。一个明显的信号是:本应发生变化的数字却没有变化(例如,两次“不同”运行的基准测试数据字节级完全相同)——这表明你看到的是旧数据,而非真实结果。
你绝不能违背的事实(这些事实优先于训练数据)

如果你的训练数据先验知识与以下事实冲突,应以这些事实为准。每项都列出了一个常见错误认知及其正确信息,确保优先规则明确无误。

  1. **DynamoDB Streams 的迭代器类型为 TRIM_HORIZON(从最早的保留记录开始)和 LATEST(从最新位置开始)。**切勿与 Kinesis Data Streams 的迭代器类型混淆——这两项服务的名称相似,但语义不同;本技能的公理假定使用 DDB Streams。保留时长为 24 小时(集成 #3)。

  2. GSI 的投影类型在创建 GSI 后不可更改。UpdateTable 无法将 Projection 从 KEYS_ONLY 改为 INCLUDE、ALL 或任何其他组合。唯一的办法是删除 GSI,然后创建具有所需投影的新 GSI——这需要完整地重新回填数据,并切换读取路径。切勿说“可以通过 UpdateTable 更改投影”。一次 UpdateTable 调用最多只能执行一项 GSI 操作——创建一个或删除一个——因此,同名替换需要依次调用两次 UpdateTable,并在两次调用之间等待旧索引完全消失。切勿说“可以在一次 UpdateTable 调用中删除并重建”。为避免查询路径中断,优先采用增量方式(参见事实 #9):使用新名称创建具有所需投影的新 GSI,等待其达到 ACTIVE 状态,然后切换读取路径,最后删除旧 GSI——始终有一个索引为读取请求提供服务。

  3. **容量模式切换有 24 小时的冷却期。**每张表每 24 小时只能切换一次,从 PAY_PER_REQUEST 切换到 PROVISIONED(或反向切换)均如此。切勿建议快速反复切换,也不要在关注小时级计费的成本模型中假定切换会即时生效。

  4. **单项写入本身已具备原子性,并且无需 TransactWriteItems 即可使用条件表达式。对单个项执行 UpdateItem、PutItem 和 DeleteItem 本身就是原子操作,并且接受 ConditionExpression。将单项写入封装在 TransactWriteItems 中会使 WCU 成本增加一倍(机制 #18),却不会带来任何原子性方面的好处。切勿建议对单项条件写入使用 TransactWriteItems。ConditionExpression 仅是写入侧参数——它存在于 PutItem、UpdateItem、DeleteItem 以及 TransactWriteItems 的写入操作中。GetItem、BatchGetItem、Query 和 Scan 均不接受 ConditionExpression——DynamoDB 不支持条件读取,ConditionalCheckFailedException 也只会出现在写入操作中。**切勿将 GetItem 描述为“仅在条件满足时返回项”,或说它会抛出 ConditionalCheckFailedException——这种行为并不存在。任何提供了键的调用者都能读取该项;读取侧唯一的过滤方式是 FilterExpression(仅适用于 Query/Scan——在读取并计费后才应用,绝不适用于 GetItem),但它也不负责授权,只会缩小调用者已经付费读取的结果范围。要阻止调用者读取其他租户的项,正确做法是让他们无法定位该数据——使用授权标识符作为表的分区键(数据建模 #14),这样其他租户的键就不属于调用者可访问的分区——而不是在此基础上附加一个“条件 GetItem”。

  5. **项大小上限为 400 KB,且不可突破。**1 MB 是 Query/Scan 响应页的上限,而不是项大小上限。接近 400 KB 的项每次写入成本也更高(WRU = 每 1 KB 计 1 个,向上取整;机制 #18),因此即使尚未触及上限,大项的成本也很高。

  6. **BatchGetItem 和 BatchWriteItem 均不具备原子性。**部分失败是常见情况,会通过 UnprocessedKeys(BatchGetItem)或 UnprocessedItems(BatchWriteItem)返回。客户端必须对未处理的部分采用指数退避策略并重试。切勿将批量操作描述为原子操作或全有全无的操作——需要跨多个项实现原子性时,应使用 TransactWriteItems(须遵守机制 #14 的限制)。

  7. **预留容量仅适用于预置容量,不适用于按需容量(PAY_PER_REQUEST)。**不要为按需表推荐预留容量——这种产品并不存在。按需模式的节省来自基于用量的折扣或表类选择(Standard 与 Standard-IA),而不是预留容量。

  8. ConditionExpression 失败仍会消耗写入容量。ConditionalCheckFailedException 收取的 WCU 与相同形态的成功写入相同。不要声称条件写入失败是免费的,也不要声称条件检查发生在评估写入成本“之前”。成本模型应考虑预期失败率(Mechanics #18 正是因此使用 conditional_fail_rate)。

  9. GSI 创建后,其键架构(分区键 / 排序键)不可更改。UpdateTable 可以添加新的 GSI 或删除现有 GSI,但不能更改现有 GSI 的 KeySchema。因此,为索引重新设置键——包括通过添加哈希后缀对热点 GSI 分区键进行写分片——属于增量迁移,而不是只改代码:创建具有新键的新 GSI → 等待其完成数据填充 → 切换读取 → 删除旧 GSI。只有当新索引必须覆盖已写入且不会再被触碰的项目时,才需要进行历史数据回填;稀疏或较小的在途索引(例如仅存放活动订单的索引)只需通过持续写入即可完成数据填充,无需回填。不要将 GSI 键变更描述为“只需改代码”或“无需架构迁移”。

  10. DynamoDB 原生支持向量搜索。语义相似性搜索不需要第二个数据存储。****向量索引是继 GSI 和 LSI 之外的第三种索引类型:使用 CreateTable 的 VectorIndexes 参数或 UpdateTable 的 VectorIndexUpdates 参数声明向量索引,将嵌入向量作为数字列表存储在项目中,并使用 SearchVectors API 查询(近似最近邻)。不要声称 DynamoDB 无法进行向量搜索或相似性搜索,也不要默认将语义搜索需求转给 OpenSearch、pgvector 或 S3 Vectors——向量与业务数据存储在同一项目中,无需复制流水线(Integration #8 负责路由不同类型的工作负载)。Query、Scan、PartiQL 和 DAX 均不适用于向量索引;对向量索引使用 Query 会被拒绝,并返回 ValidationException: Query operation not supported on this index type.。向量索引不是 GSI,也不通过 GSI 配置——不存在 VectorIndexConfig,二级索引中也没有向量配置块。向量索引要求使用按需容量(Mechanics #19)。

    **DynamoDB 不会重新计算嵌入向量,索引也无法修复过时的向量。**向量是普通项目数据:DynamoDB 只会对你写入的数字建立索引。编辑源文本后,已存储的嵌入向量仍然描述旧文本,因此它会一直匹配旧含义——不会报错,也不会在任何地方发出过时信号。当嵌入向量背后的内容发生变化时,请明确说明这一点并给出修复方法——检测内容变更,并使用同一模型重新生成嵌入向量,然后将新向量写回(通常的做法是通过 DynamoDB Streams 将变更发送给重新生成嵌入向量的使用者;也可以在写入路径中同步重新生成)。切勿暗示索引会自行刷新向量。

    Dimensions、DistanceFunction、Projection 和 SearchSchema 全部不可变——每一项都在 CreateTable 时固定,而不仅仅是投影。更改其中任何一项都意味着创建替代索引并进行迁移;无法通过 UpdateTable 就地更改这些属性(Mechanics #7)。 SDK 版本过旧不代表功能不存在。 这些操作已在 botocore/boto3 ≥ 1.43.64 和 AWS CLI v2 ≥ 2.36.16 中提供。低于这些版本时,客户端中完全没有这些操作——aws dynamodb search-vectors 会报错 Found invalid choice,而 hasattr(client, "search_vectors") 的结果是 False。检查 hasattr 比较版本字符串更好:它会直接检查是否具备该功能,也不会过时。如果该功能不存在,请告知用户升级;切勿据此断定该功能不存在。完整的 API 接口、注意事项和故障排查说明:${SKILL_DIR}/references/vector-search.md。

    引用的服务限制是设计边界,不是可以争辩的事实。 本技能中提及的每表索引数量、最大维度、TopK 范围和内联筛选器数量都可能变化,这类配额通常会提高。设计时应遵守这些限制,但在告知用户其设计超出限制前,请先通过 AWS Service Quotas 或 DynamoDB 开发者指南确认当前值——如果配额已经提高,而本技能尚未更新,就不应因此错误地否决设计。

这十条事实并非完整的公理集——它们只是大模型最容易根据先验知识误判的部分。当用户的问题涉及其中任何一条时,请明确陈述正确事实,然后继续;不要用“我认为”或“通常”之类的措辞模棱两可。

访问模式列表

在修改架构前,列出应用必须支持的所有模式。针对每种模式,记录:

  • 一句话描述调用方要执行的操作。
  • 预期 RPS(按照机制 #2,将“未知”视为有待解决的设计缺口)。
  • 每次调用返回的项目数和近似项目大小(KB)。
  • 一致性要求(强一致、最终一致或事务性)。
  • 授权范围——调用获准前必须验证的标识符(参见数据建模 #14)。

列表必须是编号排序表格。本文其余部分均假定该列表已存在。任何无法追溯到该列表中某一条目的建模决策,都缺乏依据。

各实体的运维配置输入

**在提出任何表边界之前,必须先进行此项访谈。**先产出完整的多表设计,再补充“以下是我做出的假设”,这违反了工作流程,并非捷径。以下针对各实体的问题会通过 Data modeling #3 影响表拆分决策;如果答案是 Agent 假设的,而非用户陈述的,就会错误触发该信号,导致设计过度拆分(或者,如果 Agent 为了简化而猜测“没有差异”,则会拆分不足)。先询问,再设计。

在将实体分组到表之前,向用户逐个实体(或逐个逻辑聚合——父实体及其紧密绑定的子实体可以共用一组答案)收集运维配置要求。不要默默假设这些默认值,因为 Data modeling #3 会将运维配置差异作为拆分表的信号——如果这种差异是 Agent 假设的,而非 用户陈述的,就会错误触发该信号,导致设计过度拆分。

对于每个实体,询问:

  • **备份和恢复粒度。**此实体是否需要 PITR?如果需要,保留期是多久(默认 35 天,也可以更短)?此实体是否需要独立于其他实体恢复,还是始终与它们一起恢复?(按 Data modeling #5,独立恢复要求必须分表。)
  • **Streams 消费方。**是否有下游系统需要此实体的变更事件,例如搜索索引、分析导出、通知、审计或 CDC?需要哪种流视图类型(按 Integration #3,默认值为 NEW_AND_OLD_IMAGES)?这里回答“否”也是一个有效答案:没有 Streams 消费方意味着可以保持 Streams 禁用,从而降低成本并简化设计。
  • **容量模式。**此工作负载的特征是否适合预置模式(数月持续且可预测的流量,依据 Mechanics #19),还是仍以按需模式为默认?“未知”表示按需模式。
  • **TTL。**应用是否会设置逐项过期属性?如果会,该属性使用 Unix 纪元秒数(依据 Patterns #3)。如果不会,则关闭 TTL,项目会一直保留,直到被删除。
  • **加密和 IAM 范围。**是否有任何非默认要求,例如客户管理的 KMS 密钥、特定的 IAM 边界或跨账户资源策略?默认使用 AWS 拥有的 KMS 密钥和标准 IAM;任何差异都必须明确说明。

将这些视为与 RPS 同等重要的设计输入。缺少答案意味着仍有待解决的问题,而不是可以自行猜测的值。如果用户说“所有实体都相同”,记录这一点,不要将实体视为在运维方面存在差异——这样就不会阻碍依据 Data modeling #1 进行共置。如果用户明确说明存在实际差异,则 Data modeling #3 会因实际差异而触发,表也会拆分。

逐实体属性检查(影响项目大小)

在 RPS 之后,项目大小是成本估算的第二大影响因素,也是估算最容易在不知不觉中出现偏差的地方。一个声明为 20 items × 1,536 B 的 Query,如果实际返回的是 20 × 512 B,建模成本就是实际成本的 3 倍。Mechanics #2 指出,未知的 RPS 是设计缺口;对项目大小也应采取同样的标准——对 estimated_item_size_bytes 作出无依据的猜测是设计缺口,而不是安全的默认值。

对于每个实体,在确定数值之前,与用户逐项检查属性列表。优先先询问;根据推断出的属性继续则是备选方案。无论采取哪种方式,用户都必须看到并确认逐属性明细,之后才能将其作为成本估算的输入——默默补全正是导致项目大小偏差达到 2–10 倍的原因。

  1. 根据领域提出属性列表。对于 Waypoint,包括 waypoint_id、courier_id、lat、lng、recorded_at。对于 Contract,包括 firm_id、contract_id、title、status、body、created_by、created_at、updated_at。
  2. 估算每个属性占用的字节数,可从以下数值开始:
    • ID 和短字符串(ULID、UUID、slug、枚举值):每个约 40 B。cost-model-schema.md 中通用的 S=100 启发式值对于免费层存储路径较为保守;估算单项大小时,请使用符合实际的数值。
    • 标题、显示名称、简短描述:100–300 B。
    • 长篇内容(合同正文、消息正文、序列化 JSON 聚合数据):明确询问用户。未经确认,不要猜测为 4 KB 或 50 KB。
    • 数值属性:约 8 B。
    • ISO 字符串格式的时间戳:约 25 B。纪元数字格式的时间戳:约 8 B。(机制 #11。)
    • 布尔值:约 1 B。Map/List:每个实例默认粗略估算为约 200 B,但如果用户要在 Map 中存储大型数据块,请询问。
  3. 询问用户知道而你不知道的修正信息:“该项是否会按机制 #10 携带任何反规范化的父级数据?”“是否有长度变化很大的自由文本字段?”“你存储的是完整文档还是摘要?”根据回答更新估算值。
  4. 将每个属性的估算值相加,得出实体的 estimated_item_size_bytes。对于只投影部分属性的 Query(INCLUDE / KEYS_ONLY,或应用端投影),请为访问模式使用更小的 estimated_item_size_bytes——DynamoDB 按实际读取的投影视图字节数计费,而不是按完整项计费。
  5. 如果用户对某个具体属性不确定,请在产物中将该属性标记为假设(遵循与未知 RPS 相同的规范)。不要默默选定一个数值。
  6. **无论是交互式对话还是一次性提示,都必须在回复中展示完整列表。**为每个实体输出一个简洁的 Markdown 表格,列为 attribute | type | bytes | source (user or guess)。这是回复格式要求,不是对话门槛。即使一次性场景中不会有后续轮次,也要在回复中放入表格,让用户确切看到你的假设——这样他们才能在正文大小被高估 3 倍并影响后续所有成本数字之前及时指出。明确将每个不确定的估算标记为“guess”;不要把猜测伪装成用户提供的数值。明确邀请用户纠正:“以下标注处是我的猜测——请纠正任何错误之处。”即使用户说“直接选取合理数值继续”,也要输出表格。

校准:对于 cost-model-schema.md 中的 Contracts 应用参考示例,Contract 大约为 2 KB(不是 50 KB——50 KB 是正文的最坏情况大小,而不是典型值),Clause 大约为 512 B。如果声明的 estimated_item_size_bytes 超过列出的属性总和的 2 倍,而用户没有解释差值,那么这就是猜测——请重新检查。

跳过此流程可能导致单个模式的估算偏差 2–10 倍。实时验证步骤(如下)会暴露这种偏差,但为了让成本估算达到正确的数量级,你不应该依赖实时验证。

要产出的工件

请按下方列出的顺序产出工件。 Schema 和逐模式计划是主要输出;成本估算(第 7 项)和实时验证(第 8 项)应在设计完成后进行,而不是取代设计。如果回复一上来就分析成本,把 Schema 藏在附录里,那就颠倒了依赖关系——用户要的是设计,而成本是设计的一个属性。访问模式列表(第 1 项)、Schema(第 2 项)和逐模式计划(第 3 项)必须在回复中先展示并进行讨论,然后才能给出任何成本数字。第 1–6 项是无需 AWS 的设计本身,也是默认交付内容;第 7 项(成本)应在用户提出要求或最终定稿时产出(参见成本估算);第 8 项(实时验证)是可选的 AWS 分支。仅把这些工件放在 dynamodb_data_model.json 中并不符合要求——用户看的是你的文字回复,而不是 JSON。❌ 错误的回复形式(实际失败案例):回复以“## 给 CFO 的摘要 — $1,019/月”开头,Schema 只存在于磁盘上的 dynamodb_data_model.json,而回复中唯一的设计内容则是结尾处的“已生成工件”文件列表。✅ 正确做法:先在回复中呈现访问模式列表、每张表的 Schema、逐模式计划和逐实体字节数表(参见 逐实体属性解析 第 6 步),然后给出成本摘要,最后说明“已写入 cost_report.md”。

一份完整的设计应包含:

  1. 访问模式列表,如上所述。

  2. 每张表的 Schema:主键(按 Data modeling #7 命名)、GSI 及其键属性和投影类型,以及操作配置(Streams、PITR、TTL、容量模式、Global Tables 复制、加密、IAM 范围——均按 Data modeling #3)。

  3. 逐模式计划:针对列表中的每种访问模式,写明确切的 API 调用(按 Mechanics #16 使用 GetItem、Query 或 BatchGetItem)、目标表或 GSI、键条件、筛选表达式(如有),以及按 Mechanics #18 中的公式计算的预估成本。

  4. 扇出拓扑:对于启用了 Streams 的每张表,列出消费者(Lambda、EventBridge Pipe、Kinesis shim)、源端筛选器(Integration #4),以及失败时的目标位置和重试上限(Integration #5)。

  5. 偏离项及其理由:列出未采用的任何公理或模式,并说明理由。

  6. 幂等性和条件写入保护:哪些路由使用幂等键中间件(Patterns #1),哪些 UpdateItem 带有用于防止幽灵式 upsert 的 attribute_exists 保护条件(Patterns #2),以及哪些 PutItem 带有用于防止重复创建的 attribute_not_exists 保护条件。

  7. 月度成本估算——应在用户提出要求或最终定稿时产出,而不是在每轮设计讨论中都例行计算。用户仍在探索或完善模型时,应继续进行设计讨论,不要每轮都运行计算器。用户询问成本,或设计即将定稿(例如用户表示准备采用、提交评审或需要具体数字)时,使用 ${SKILL_DIR}/scripts/calculate_costs.py 产出估算——参见下文的成本估算。对于范围过窄、无法形成完整设计的问题(例如单次查询的容量估算、调试讨论或具体的机制问题),跳过此项。

  8. 实时验证(可选,作为最后一步提出):成本估算完成后,询问用户是否希望将此 Schema 部署到其指定的 AWS 账户,并测量实时 DynamoDB 中每种操作的实际容量、延迟和 GSI 放大率。如果用户同意,请遵循下文的实时验证。对于范围过窄的问题、跳过成本估算的情况,或用户没有沙盒账户的情况,应跳过此项。与成本估算不同,此步骤会创建实际资源并产生实际费用,因此必须明确提出,并获得明确同意。

冲突解决优先级

当公理指向相反方向时,按以下优先级处理:

  1. 正确性 — 授权边界对齐(数据建模 #14)、一致性要求、事务原子性、幂等性。如果设计导致数据泄露到其他租户,或在要求强一致性的场景下提供过期数据,那么无论它在其他方面有何优点,都是错误的。
  2. 运维必要性 — PITR、Streams、容量或复制配置存在差异(数据建模 #3)、恢复粒度(数据建模 #5)、每个分区的吞吐量上限(机制 #3)、事务限制(机制 #14)。这些属于物理或服务约束,不能因偏好而忽视。
  3. 成本和性能 — 按访问模式共同存储(数据建模 #1)、专用 GSI(数据建模 #6)、投影选择(机制 #7)、成本公式(机制 #18)、容量模式(机制 #19)。
  4. 风格和惯例 — 命名(数据建模 #7);在没有其他依据时,采用单表还是多表的设计思路(数据建模 #11)。当与更高优先级的公理冲突时,这类原则最容易被推翻。

两个具体示例:

  • 数据建模 #1(按共享访问模式共同存储)与数据建模 #14(按授权边界分区):优先采用 #14。如果自然访问键与授权键不同,则使用授权标识符作为键,并通过 GSI 提供替代访问方式。
  • 数据建模 #1(共同存储)与数据建模 #3(因运维配置差异而拆分):优先采用 #3。即使两个实体共享读取模式,只要它们需要不同的 PITR 保留期或 Streams 消费者,就应放在不同的表中。

术语表

  • 访问模式 — 应用对数据层发起的请求,根据其键条件、返回的项目、频率和一致性要求进行描述。它是 DynamoDB 设计的原子单元。
  • 聚合 — 一组一起读取或写入的实体。单个项目、项目集合,或不同键下的一组项目都可以构成聚合;如何选择是机制 #1 讨论的主题。
  • 项目集合 — 共享同一个分区键值的一组项目。针对项目集合的查询只涉及一个分区,成本低;跨分区读取则不然。
  • 标识关系 — 一种数据模型,其中子实体的键由其父实体的标识符和子实体自身的标识符组成。子实体不能脱离父实体独立存在。
  • 复用键 — 值中编码了类型前缀(例如 USER#42、ORDER#42)的分区键或排序键,因此一个物理键可以存放多种逻辑实体类型。
  • 稀疏 GSI — 一种 GSI,其索引属性只存在于部分基表项目中,因此索引仅投影这些项目。当某种访问模式在读取时本来会筛除大部分项目时,这种索引很有用。相同语义也适用于向量索引,也是静默取消索引背后的机制。
  • 向量索引 — 除 GSI 和 LSI 之外的第三种索引类型,用于存储来自具名项目属性的向量嵌入,并通过 SearchVectors 而非 Query/Scan 读取。通过 CreateTable 上的 VectorIndexes 或 UpdateTable 上的 VectorIndexUpdates 声明。仅支持按需容量;每张表最多 5 个。
  • ANN(近似最近邻)— 向量索引使用的搜索策略。它以精确性换取速度,因此在数据相同的情况下,不同运行或不同 Region 的结果及其排序可能略有差异。
  • SearchSchema — 向量索引的可选架构,最多列出一个 HASH 元素(向量索引分区键,用于限定范围并扩展每次搜索,且每次调用都必须提供)和最多 18 个 INLINE_FILTER 元素(在存储层应用的可选相等性筛选条件)。创建后不可更改。每个 SearchSchema 属性也必须出现在表的 AttributeDefinitions 中。
  • 距离函数 — COSINE、EUCLIDEAN 或 DOT_PRODUCT,在创建索引时选择且不可更改。它决定 Score 和排序顺序:COSINE/EUCLIDEAN 的值越低越相似,DOT_PRODUCT 则越高越相似,且其分数可能为负数。
  • 静默取消索引 — 写入的项目如果没有向量索引的 SearchSchema HASH 属性,就会在基表中被接受且不会报错,但永远不会复制到索引中,因此会从 SearchVectors 结果中消失,而 GetItem 仍会完整返回该项目。这是预期行为,并非缺陷;请参阅 ${SKILL_DIR}/references/vector-search.md。
  • VS / VWR — 向量搜索和向量写入请求单位。向量索引容量按字节计量,以 VectorSearchRequestBytes 和 VectorWriteRequestBytes 报告,与基表 RCU/WCU 分开计算;每个请求的最低计量值为 1 KB。
  • GSI 写入放大 — 对基表项目的每次写入,只要项目包含投影到 GSI 的属性,就会针对每个匹配的 GSI 产生一次写入,并分别按 WCU 计费。
  • LWW(最后写入者胜出)— 标准 Global Tables 使用的冲突解决策略:时间戳最新的写入胜出;较早的写入会被静默丢弃。
  • MRSC(多 Region 强一致性)— 一种可选启用的 Global Tables 模式,通过共识机制在副本之间提供强一致性,但写入延迟和成本更高。
  • 热点分区 — 接收的流量超过每分区吞吐量上限(机制 #3)的分区,即使表级容量充足,也会导致请求被限流。
  • 毒丸记录 — 流使用者无法成功处理的记录。它会阻止其分片继续向前处理,直到该记录被丢弃、重试至耗尽次数,或路由到故障时目标位置。
  • RCU / WCU — 读容量单位和写容量单位;这是预置容量模式下每次操作吞吐量单位的称法。机制 #18 中的公式以 RCU/WCU 编写,对按需模式同样适用。
  • RRU / WRU — 读请求单位和写请求单位:这是按需(PAY_PER_REQUEST)模式下相同每次操作单位的称法,按请求计费,而非按预置容量秒计费。一个 RRU 等于一个 RCU 的工作量,一个 WRU 等于一个 WCU 的工作量——机制 #18 中的用量计算完全相同,只有计费维度不同。成本参考(cost-model-schema.md)使用 RRU/WRU,因为计算器按需定价;公理和性能报告使用 RCU/WCU。它们表示相同的数量——不要把模型中的 RRU/WRU 数值和报告中的 RCU/WCU 数值视为不同的量。

数据建模

  1. 按共同的访问模式,而不是按领域,将数据放在一起。只有当应用请求会同时获取或写入两个实体时,它们才属于同一张表。业务领域相同——例如“用户数据”“账单数据”——不足以构成理由。如果没有共同查询就将数据放在一起,只会引入耦合,不会带来性能收益。

  2. 将表的数量视为设计结果,而不是目标。不要为了只用一张表进行优化,也不要按每个实体一张表来设计。正确的表数量取决于访问模式的分析结果。如果分析得出三张表,就部署三张表。

  3. 将表级配置同时视为建模输入和接口声明。流、时间点恢复、TTL、容量模式、关联的 Kinesis 流、Global Tables 副本、加密和 IAM 范围都在表级别生效——它们声明了该表如何参与更广泛的系统。如果两个实体需要不同的运维设置——例如不同的 PITR 保留期限、不同的流使用者、不同的复制区域或不同的容量模式——这种差异就是它们应放在不同表中的主要信号,而不是以后再协调处理的次要问题。

  4. DynamoDB Streams 支持两个并发使用者,没有原生的按实体过滤功能,保留期限为 24 小时。这些特性对下游架构的约束,与键架构一样严格。确定扇出拓扑后,再敲定表布局。

  5. 按恢复粒度进行设计。时间点恢复在表级别运行;不支持部分恢复。如果两个实体永远不会一起恢复,就不应放在同一张表中。事件的处理成本会随着恢复操作必须一并恢复的无关数据量增加。

  6. 优先增加 GSI,而不是过度复用键。过去促使人们过度复用索引的约束——每张表的 GSI 数量上限较低、每个索引都需要单独预置容量,以及不支持按需模式——通常不适用于现代 DynamoDB。现代 DynamoDB 支持每张表创建多个 GSI,且共享容量并按需计费(具体的每表 GSI 上限请查阅 AWS 当前的服务配额文档)。除非有明确且经过测量的理由,否则应为每种访问模式选择专用 GSI。

    向量索引较为稀缺,并且与 GSI 共用索引操作配额。 一张表最多支持 5 个向量索引,而 GSI 配额要高得多,因此应有计划地分配向量索引,不要针对每种查询形式都添加一个。更重要的是,每张表一次只能运行 一个 在线索引操作——创建或删除——并且 GSI 和向量索引共用此限制。在一次 UpdateTable 中执行两个 Create 操作会失败,并出现 LimitExceededException: Subscriber limit exceeded: Only 1 online index can be created or deleted simultaneously per table。索引操作应逐个进行。还要注意,这会使任何向量索引迁移(机制 #7)都很慢,因为每个新索引都必须完成回填后,下一个才能开始。

  7. 键名应反映其含义。使用 customer_id、order_created_at 和 OrdersByCustomer,而不是 PK、SK 和 GSI1。自描述键可缩短新成员熟悉系统所需的时间,让代码评审不必查阅架构文档,并使工具能够内省模型。只有在确实存在多态层级结构时,才使用通用的过度复用键。

  8. 只预先连接会在单个请求中一起读取的实体。项目集合用于满足一次性查询,例如“获取父项及其所有子项”。如果没有任何访问模式会同时读取两个实体,就不要将它们存储在一起。没有对应读取需求的预先连接只会引入耦合,没有任何收益。

  9. 默认不要跨服务边界共享表。多个服务共享一张表,会迫使团队协调 GSI 分配、键约定和架构变更。随着表不断增长,这种协调成本也会叠加。服务所有权应意味着表所有权。

  10. 为模型的变化做好准备。访问模式会不断演变。如果某种表布局需要重构多个表才能适应新的访问模式,那就是一种负担。优先选择这样的设计:添加或修改访问模式只需局部更改单个表,而不必改动整个数据访问层。

  11. 将单表设计作为一种策略,而不是默认做法。对于紧密关联、联合访问模式可预测的父子层级,单表设计是合适的,例如订单和明细项、租户和子资源。但不应将其作为全局规则。这种模式的价值只体现在局部;全局应用会引发生产复盘记录中提到的运维问题。

  12. 将分析型工作负载从操作表中移出。DynamoDB 是 OLTP 存储。类似 SQL 的查询、全表聚合和临时 BI 应放在下游系统中,并通过导出或零 ETL 集成向其填充数据。不要为了满足分析需求而重塑操作表。

  13. 采用标准全局表时,围绕最后写入者胜出进行设计;如果业务领域无法接受这种机制,则选择多区域强一致性(MRSC)。标准全局表提供具有最终一致性和最后写入者胜出冲突解决机制的多区域主动-主动复制——它们不是 CRDT。无法接受 LWW 的领域——库存、余额、计数器——有两种选择:在应用层避免冲突(写入固定在特定区域、分片分区、条件更新),或选择 MRSC。MRSC 是一种可选模式,通过共识协议在指定副本集之间提供强一致性读写。MRSC 要求采用三地区域拓扑(两个活动副本加一个见证节点),写入延迟和成本更高,并且按表配置。两种模式都支持跨账户副本,并且遵循相同的一致性语义——账户边界涉及 IAM 和所有权,而非一致性。选择标准全局表还是 MRSC 是一种建模决策:它决定了该表可以承载哪些业务领域。

    一种特殊情况:单个热点项受到争用——例如限时抢购时,数千名买家同时争抢同一 SKU 的库存扣减。这里的问题不仅是强一致性,还有吞吐量:对单个项的写入会在内部串行化,因此在达到分区的原始容量前,就会遇到延迟升高和每分区 WCU 上限(机制 #3);而且每次条件扣减失败仍会消耗 WCU(事实 #8)。将单个项的写入固定到某个区域并无帮助——这只会让争用集中在一个区域,并不会消除争用。解决方法是使用分片计数器:将数量拆分到 N 个同级项(以 sku#shard0..sku#shardN 作为分区键,使其分散到多个分区),在随机分片上执行带有 > 0 条件的扣减,失败时重试其他分片,并分散读取所有分片后求和——容量设置应宁可少卖,也不要超卖(每个分片都会单独执行下限检查)。注意,这确实是对分区键进行写入分片(机制 #3),应用于同级计数器项;它是解决争用的方法,并不与之矛盾。将写入固定到某个区域并按项避免冲突,仍适用于基数高、单项争用低的数据(例如每个账户的余额,每个用户都有自己的分区键,争用率约为 1),但不适用于单个受到争用的行。

  14. 让分区键与授权或租户边界保持一致。你在允许读写之前验证的标识符——tenant_id、account_id、board_id,或任何用于限定调用方访问范围的标识符——都应作为所有存储该范围内数据的表的分区键。当授权和查找共用同一分区时,针对调用方无法证明自己有权访问的分区执行查询,只会返回空结果;如果两者不一致,调用方就可以传入他人的子项标识符,跨越授权边界访问数据(IDOR)。确实需要跨租户的数据应放在单独的表中,并采用自己的访问规则,而不应以此为由折中主表的设计。 在任何设计评审中——尤其是当用户询问安全问题是什么时——评估分区键是否与授权/租户边界一致,是首要检查项,也是必须首先说明的结论,优先于热分区、投影或成本方面的发现(依据冲突解决顺序,正确性优先于成本)。典型的漏洞:分区键被设置为客户端提供的标识符(来自 URL 的 review_id/order_id),而非服务端派生的授权标识符(user_id、tenant_id、account_id)——这就是 IDOR。如果设计已经对齐,说明这一点然后继续;如果存在偏差,要首先指出,并且不要将其降级为“添加一个 ConditionExpression”——每次调用检查是一种纪律,而非结构性修复。当你解释为什么 ConditionExpression 不是修复方案时,要把机制讲对:写入侧条件是选择加入的(一条被遗忘的调用路径 = IDOR),而且它完全不覆盖读取——但它不覆盖读取的原因是 GetItem/Query 本身就不接受 ConditionExpression(事实 #4),而不是“条件式 GetItem 无论如何都会返回该项 / 抛出 ConditionalCheckFailedException”。不要发明一种读取侧条件机制来反对它。结构性修复是键模式(外键不可寻址),而读取保持安全是因为调用方无法命名一个不属于自己的分区——就这样表述。

集成

  1. 确保 Streams 消费者按事件标识符实现幂等或去重。DynamoDB Streams 提供至少一次投递——处理器在 Lambda 超时、批量重试或事件期间重放后,会看到同一条记录两次,并且无论被调用一次还是多次,都必须产生相同的结果。

    有两种可行策略:

    • 幂等处理器:将操作设计为重复执行即为无操作。条件写入、基于集合的变更(对集合使用 ADD)以及下游 API 上的幂等令牌都符合条件。
    • 显式去重:将流记录中的 eventID 记录到去重存储中(通常选择带 TTL 的 DynamoDB 表),并跳过已存在的记录。

    示例(按 eventID 去重,TTL 限定在重投递窗口内):

    def handler(event, context):
        for record in event["Records"]:
            try:
                dedupe_table.put_item(
                    Item={"event_id": record["eventID"], "ttl": int(time.time()) + 86400},
                    ConditionExpression="attribute_not_exists(event_id)",
                )
            except ClientError as e:
                if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
                    continue  # already processed
                raise
            process(record)
    

    按 eventID 去重,而不是按 order_id 等应用标识符——同一个聚合会合法地产生许多事件,按业务标识符作为键会丢弃有效的变更记录。

  2. 使用事务性发件箱模式实现可靠的事件发布。在单次 TransactWriteItems 调用中写入状态变更和一条发件箱记录;让下游消费者通过 Streams 读取发件箱并发布事件。这消除了“已更新但未发出”的故障模式,且无需诉诸两阶段提交。

  3. 将流视图默认设为 NEW_AND_OLD_IMAGES。与 OpenSearch 的零 ETL 集成、大多数变更数据捕获消费者,以及任何依赖差异的逻辑,都需要两个镜像。

  4. 在源头过滤事件,而不是在处理器中过滤。源侧过滤有两种机制——在诊断或提出修复方案时,两者都要提及:

    • Lambda 事件源映射 FilterCriteria——在 Lambda 调用之前过滤流记录。一个原本每天处理 1000 万事件但只关心其中 50 万的处理器,变为只运行 50 万次的处理器,按 50 万次调用计费而非 1000 万次。过滤形状(针对 DynamoDB 流记录的 JSON 模式匹配):{"dynamodb": {"NewImage": {"status": {"S": ["shipped", "delivered"]}}}}。
    • EventBridge Pipes 过滤器——当 Pipe 作为消费者时,应用相同的源侧丢弃。当扇出跨越多个下游目标(一个事件 → Lambda + SQS + EventBridge 总线)时,使用 Pipes,因为 Streams 上 2 个并发消费者的限制(数据建模 #4)使得 Pipes 成为超过 2 个消费者拓扑的常见路径。 这两种机制都能消除调用成本,以及处理程序原本用于筛选的工作。在处理程序中内联筛选逻辑,每丢弃一个事件就要承担完整的调用成本——如果筛选器丢弃 95% 的流量,就意味着 95% 的调用纯属浪费。如果问题涉及源端筛选,回答应同时指出这两种机制,并根据使用者拓扑推荐合适的一种。
  5. 将事件源映射配置视为可靠性层面。BatchSize、MaximumBatchingWindowInSeconds、ParallelizationFactor、MaximumRetryAttempts、MaximumRecordAgeInSeconds、BisectBatchOnFunctionError 和失败目标共同定义了交付语义。有几个默认值会带来切实风险:MaximumRetryAttempts 和 MaximumRecordAgeInSeconds 默认均为 -1,这意味着,一条无法处理的记录会阻塞其分片长达完整的 24 小时流保留期,之后又悄无声息地消失。应限制重试次数和记录存留时长,并配置失败目标,以便隔离毒性记录,而不是让它们阻塞分片或消失。将其中任何一项保留为默认值,都是一种有意的选择,而非中立选项。

  6. 将启用流视为一项持久的接口承诺。禁用流后再重新启用,会生成新的流 ARN。绑定到旧 ARN 的任何 Lambda 事件源映射或 EventBridge Pipe 都会变成孤立配置。流的生命周期属于表的公开契约,应据此管理。

  7. 区分由 TTL 触发的删除和用户触发的删除。DynamoDB 会通过 Streams 交付 TTL 删除事件,并将 userIdentity.principalId 设为 dynamodb.amazonaws.com。使用者应根据此属性进行分流,将 TTL 事件单独路由——通常送往归档或审计工作流——而不要将其视为普通的用户删除事件。

  8. 应根据分析或搜索工作负载选择合适的目标;不要为了适配这两种用途而重塑操作型表。DynamoDB 是 OLTP 存储;对于分析工作负载,受支持的只读优化系统路径是托管集成,而不是 Scan,也不是专门构建的 GSI。语义相似性是例外,应留在 DynamoDB 中(事实 #10)。根据工作负载类型进行路由:

    • 语义相似性——“查找相似项”、“查找更多类似项”、RAG 检索、推荐、AI Agent 记忆、基于嵌入的异常检测 → 在 DynamoDB 表自身上建立向量索引,并使用 SearchVectors 查询。无需第二个数据存储,无需复制管道,也不需要 PITR 前提条件。对于任何以嵌入相似度表达的需求,这都是默认选择。请参阅 ${SKILL_DIR}/references/vector-search.md。
    • 全文、关键字、容错拼写或模糊匹配、分面、聚合、地理空间/半径搜索 → 采用零 ETL 方式集成至 Amazon OpenSearch Service。通过 PITR 快照进行初始加载,通过 DynamoDB Streams 持续捕获更改,近实时更新(秒级)。要求在源端启用 PITR 和 Streams,并将流视图类型设为 NEW_AND_OLD_IMAGES。
    • SQL 分析、BI、物化视图、数据共享 → 采用零 ETL 方式集成至 Amazon Redshift。通过 PITR 导出进行初始加载,每隔 15–30 分钟通过增量导出持续捕获更改。要求在源端启用 PITR,并将 KMS 配置为使用 AWS 拥有的密钥或客户管理的密钥(不支持 AWS 管理的 KMS)。
    • 开放格式数据湖、Apache Iceberg、多引擎分析(Athena、EMR、Spark、Redshift)或 ML 特征存储 → 采用零 ETL 方式集成至 Amazon SageMaker Lakehouse。由 Glue 编排初始导出,并通过增量导出将 Iceberg 表写入 S3 或 S3 Tables,数据通常可在 15–30 分钟内更新。要求启用 PITR,并配置基于资源的策略,授予 Glue 导出操作权限。
    • 带有自定义下游处理且不要求数据时效性的时间点批量转储 → 将 DynamoDB 导出到 S3(不称为零 ETL,但在没有合适的零 ETL 目标时,这是正确的回退方案)。不消耗 RCU。 在上述每条零 ETL 路径中,源表启用 PITR 都是硬性前提——这不适用于向量索引路径,因为它不属于集成。不要用 begins_with 查询和专门的 GSI 来近似搜索。不要扫描运行表进行分析。这些集成之所以存在,是因为这些工作负载可以与列存储、搜索引擎或 Iceberg 表组合使用,却无法与 NoSQL 键值存储组合使用。

    拆分打包在一起的需求;不要把所有需求都发往同一个地方。 对“搜索”的需求通常包含多种类型。语义相似度交给向量索引;词法、模糊和地理搜索交给 OpenSearch;SQL 交给 Redshift。确实同时需要语义搜索和全文搜索的工作负载,应同时使用向量索引和 OpenSearch。两种做法都不对:习惯性地把语义相似度转交给 OpenSearch,或把容错拼写的关键词搜索或地理半径搜索交给向量索引。基于嵌入向量的 ANN 不支持子串、前缀或编辑距离匹配。

    当一个运行表同时为 Redshift(SQL/BI)和 SageMaker Lakehouse(Iceberg)路径提供数据时,优先使用单次 DynamoDB→Lakehouse Iceberg 导出,并让 Redshift 读取相同的 Iceberg 表(Redshift Spectrum / 原生 Iceberg 支持),而不是运行两个并行的零 ETL 集成——这样无需牺牲 SQL/联接能力,就能避免第二次增量导出和重复的每 GB 费用。只有当使用者确实需要不同的数据新鲜度或隔离时,才使用两个独立的集成。

机制

  1. 通过权衡实体被共同读取的频率与实体被独立写入的频率,选择聚合的紧密程度。三种选项——将子项嵌入单个项目中、在共享分区键下将它们归为一个项目集合,或将它们存储为独立聚合——构成了父项与子项绑定紧密程度的一个范围。选择时没有单一的阈值。较高的读取相关性意味着应将数据放在一起,但写入密集型工作负载和较大的项目会推动选择相反方向,因为每次更新都要重写整个项目。项目大小以及子项数量是否有上限,与访问频率同样重要。经验法则:如果订单行项目大多数时候都与订单一起获取,且行项目数量有上限,可以考虑将其嵌入,或以订单为父项使用项目集合;如果单个行项目经常被单独更新,则应将其分开存储,以免每次写入都重写整个父项。选错层级是大多数“单表设计出了问题”案例背后的根本错误。

  2. 记录每种访问模式的 RPS。没有请求速率,就无法确定分区容量、选择按需容量还是预置容量,也无法论证 GSI 的必要性。基于业务背景的估算就足够;完全没有速率则不行。将“未知”视为有待弥补的设计缺口。

  3. 遵守每个分区的吞吐量上限。单个分区每秒最多支持 1,000 个写入容量单位和 3,000 个读取容量单位。超过这些限制的工作负载必须对分区键进行分片——通常对写入密集型流量添加哈希后缀,或对顺序键使用时间分桶。

  4. 基表的键架构必须恰好包含一个 HASH,且最多包含一个 RANGE。当基表需要复合键时,将其编码为带有稳定分隔符的拼接字符串,例如 tenant_id#user_id。GSI 支持原生多属性键——分区键最多包含四个属性,排序键也最多包含四个属性——DynamoDB 会将 PK 属性组合后进行哈希,以实现数据分布。优先使用原生多属性 GSI 键,而不是合成拼接键:写入的项使用领域模型中的自然属性,客户端代码无需拼接或解析,并且向现有表添加新的多属性 GSI 无需回填合成属性。仅当组件数量超过四个,或旧表已采用这种模式时,才使用合成拼接键。

  5. 多属性 GSI 键有严格的查询规则。对于分区键一侧,每个 PK 属性都必须使用等值条件约束——PK 为(tenant_id、region)的 GSI 不能仅按 tenant_id 查询,且任何 PK 属性都不允许使用不等式运算符。对于排序键一侧,必须按照定义顺序从左到右约束属性;不能跳过中间属性。等值条件必须位于任何不等式条件之前,且只能有一个不等式条件——它必须是键条件表达式中的最后一个条件。BETWEEN、>、<、>=、<= 和 begins_with 都属于不等式。违反这些规则是 GSI 无法满足预期访问模式的最常见原因。

  6. 如果有 50% 或以上的项会被筛除,请考虑使用稀疏 GSI。仅为含有某个属性的项编制索引,明显比为所有项编制索引后在读取时筛选更省成本。稀疏索引还能减少写放大,因为只有带有被索引属性的项才会投影到索引中。

  7. 仅投影访问模式会读取的属性。ALL 投影会让每次基表更新的存储成本和写放大大致翻倍。如果读取延迟很重要,或者应用的读负载远高于写负载,请使用 ALL。如果项较大或表的写负载较高,请使用 INCLUDE;如果延迟要求允许,也可以使用 KEYS_ONLY 并反向查询表。

    对于向量索引,投影是严格的读取边界,且不可变。 SearchVectors 无法返回未投影到索引中的属性,请求这类属性会被静默忽略——不会报错,只是该属性不会出现在结果中。创建后无法更改投影,因此投影属性不足意味着必须创建替代索引并迁移(增量迁移,参见事实 #9),或者在搜索后使用 GetItem/BatchGetItem 从基表补全缺失属性。**而且不可变的不只是投影:Dimensions、DistanceFunction 和 SearchSchema 也同样不可变。**因此,“将此索引切换为 COSINE”或“向搜索架构添加分区键”也需要迁移到新索引,绝不能使用 UpdateTable——当某个调试讨论定位到距离函数错误时,应明确指出这一点,而不应只描述投影的情况。还请注意,即使使用 ALL,向量属性默认也会从结果中排除,因为向量数据体积较大;只有确实需要返回该属性时,才应使用 ProjectionExpression 显式请求。

  8. 避免使用可变属性作为 GSI 键。DynamoDB 会将键属性变更实现为先从索引中删除、再插入,因此该次更新的写入成本会翻倍。频繁变化的字段不应作为 GSI 分区键或排序键。

  9. 当子项访问主要发生在父项上下文中时,使用标识关系。如果子实体无法脱离父实体独立存在,并且获取或更新子项时通常会携带父标识符,则在基表上建模该关系,将父标识符作为分区键,将子标识符作为排序键。这样可以省去“按父项列出子项”模式所需的 GSI,并通常通过消除相关索引带来的写入放大效应,将写入成本减半。例外情况是某个高频路径会在不提供父项信息的情况下,仅按子项自身的 ID 更新单个子项,例如作用于某个明细项的 webhook。UpdateItem 要求提供完整主键,因此,如果调用方经常只有 order_item_id,标识关系架构就会迫使系统在每次写入时通过 GSI 查询 order_id,抵消节省的成本。在这种情况下,应反向设计:以子项自身的 ID 作为键,并通过 GSI 携带父项信息。正确的结构取决于高频访问模式,而非风格偏好。

  10. 仅对很少变化的属性进行反规范化。短路复制——例如将用户显示名称存到订单上、将产品 SKU 存到明细项上——是安全的,也能省去额外读取。复制可变属性会让每次更新都变成涉及多个项的扇出操作,其成本很快就会超过读取收益。

  11. 根据所需的排序行为选择时间编码方式。字符串按字典序排序;"10" 排在 "2" 前面。如果需要自然的字符串排序且重视可读性,请使用 ISO 8601 时间戳。如果重视紧凑性、算术运算或精度,请使用 Unix 纪元时间。不要在同一张表中混用不同编码。**当键属性(表或 GSI 的分区键/排序键)为数字——例如纪元时间戳、数字 ID——或二进制数据时,必须在数据模型中声明其类型,以便线上部署时正确创建。**DynamoDB 只会为键属性指定类型,并会在写入时强制检查该类型:未声明的键会以字符串(S)部署,之后首次写入真正的整数/二进制数据时就会失败并报 ValidationException——这会发生在生产环境中,而非基准测试中(种子数据可能通过,但线上写入会失败)。请在 entities[].attributes[] 中声明类型,如 {"name": "order_date", "type": "N"};也可以在表级别的 attribute_definitions 中声明,如 {"attribute_name": "order_date", "attribute_type": "N"}(部署和基准测试均可读取这两种写法;发生冲突时,以 attribute_definitions 条目为准)。参见 ${SKILL_DIR}/references/cost-model-schema.md。

  12. 不要依赖 TTL 实现对时间敏感的过期处理。TTL 删除是最终一致的——后台清理器按尽力而为的计划运行,过期项在被清理器移除前可能仍可见数小时;实际中,最多可能在 TTL 时间戳过后约 48 小时才被移除。TTL 适用于回收存储空间和清理数据;不适用于会话、令牌、权益或实时事件触发器等安全敏感的过期处理。

    由此带来两个后果:

    • **读取时进行筛选。**由于项在清理器运行前可能在 TTL 过期后继续存在,因此每条关注过期状态的读取路径都必须检查 TTL 属性本身——使用 FilterExpression 中的 #ttl > :now,或在应用端进行检查。不要假设 DynamoDB 返回的项在逻辑上仍然有效。
    • **需要精准计时时,与 EventBridge Scheduler 配合使用。**如果工作负载需要在过期时执行精确操作——例如撤销会话、发送提醒或释放锁——请在写入时为确切时间戳创建一次性的 EventBridge Scheduler 调用,并由该调用执行操作。TTL 负责最终清理;调度器负责时间敏感的触发。随后,Streams 可以将实际发生的 TTL 删除传播到归档或审计流程(参见 Integration #7),但 Streams 不是计时机制。

    TTL 属性必须是以秒为单位的 Unix 纪元时间值,而非毫秒。

  13. 通过事务而非应用逻辑强制保证唯一性。创建一个同级查找项,例如 UNIQUE#email#[email protected],并在一次 TransactWriteItems 调用中将其与实体一并写入。在应用代码中先检查再写入会产生竞争窗口;事务则不会。

  14. 在事务限制范围内操作。TransactWriteItems 受 三项硬性限制——如果问题涉及事务限制,始终说明全部三项,不要只提一项或两项。 这些限制彼此独立;超出任何一项都会导致事务被拒绝:

    • 每个事务最多 100 个项——数组中的项总数。
    • 总负载 4 MB——事务中所有项的合并大小。不要省略此限制,也不要把它并入 100 项的条目;包含 10 个各为 500 KB 的项的事务虽然符合 100 项限制,却会超过 4 MB 限制。负载上限恰好是 4 MB——不是 16 MB(那是 BatchGetItem 的上限),不是 1 MB(那是 Query/Scan 的分页上限),也不是 400 KB(那是项的最大大小)。
    • 单个区域——事务只能在一个区域内执行;它不会跨越 Global Tables 副本。如果不同区域的调用方需要参与同一事务,那么标准 Global Tables 就不适合该工作负载的所需一致性模型(请使用 MRSC,或调整设计,使每个事务都限定在单一区域内)。

    经常超出这些限制的操作,表明聚合边界划分得过粗,而非数据库存在需要绕过的限制。如果导入或批量操作超过 100 项,应将其拆分为多个事务,并明确处理部分失败恢复(使用作业 ID、检查点进度,并对每个分块进行幂等重试)。

    计算 100 项限制时,统计的是项,而非记录。 当一条逻辑记录需要多个 DynamoDB 项时——例如,根据机制 #13,为一个联系人创建一个唯一性哨兵项,再加上一次计数器递增——TransactWriteItems 数组中的每个项都要计数。每个联系人占 3 个项,因此每个事务只能容纳约 30–33 个联系人,而不是 100 个。4 MB 负载限制还会独立构成第二个上限;对于每条记录接近 30 KB 的情况,4 MB 上限会先于 100 项上限触发。应根据这两项限制规划分块大小,并选择较小的上限。

    超过 100 项的批量导入应作为后台作业处理,而非同步 API 调用。 使用循环执行 TransactWriteItems,分页处理数万条记录,会超出任何合理的请求超时时间(API Gateway 为 30 秒,Lambda 上限为 15 分钟,浏览器请求也有超时限制)。应将此操作建模为持久化作业:接受请求、将作业记录入队,再由工作器(Step Functions、SQS + Lambda、批处理作业)驱动分块执行,并通过检查点记录进度,以便重启后从中断处继续,同时异步报告完成情况。不要把“分块事务”当作一个可组合成单次同步操作的整体。

  15. 默认采用最终一致性读取。强一致性读取会消耗双倍 RCU。只在需要读己之写语义的边界使用,不要将其设为全局默认值。

    当调用方更新某个项后,立即需要获取最新值时,优先在 UpdateItem 上使用 ReturnValues=ALL_NEW(或根据需要使用 ALL_OLD / UPDATED_NEW / UPDATED_OLD 变体),而不是再执行一次读取。写入操作会在响应中返回更新后的项,不额外消耗 RCU,也没有一致性方面的顾虑——写入返回的负载天然就是权威数据。在 UpdateItem 后进行强一致性 GetItem,需要消耗 1 RCU,还要多一次往返,只为获取原本就可免费获得的信息。这是在 DynamoDB 上实现读己之写最省成本的方式。

  16. 将每种访问模式映射到 GetItem、Query 或 BatchGetItem。Scan 适用于管理操作,而非应用读取路径。生产环境的访问模式如果需要使用扫描,就说明存在建模缺口,而不是一种可接受的查询选择。

  17. 限制每个查询。指定排序键范围或筛选条件,计算预期页面大小,并使用 LastEvaluatedKey 分页。限制可以来自键结构本身——包含数量有上限的子项的父分区(例如某个订单下的明细项、某个团队的成员)本身就已受限,而“返回此父项下的所有内容”是有效的访问模式。不能算作访问模式的是:随着数据集增长,结果大小也会无限增长的查询,例如多年间“某个客户的所有订单”或“日志中的所有事件”,却没有范围、目标页面大小或终止条件。对持续增长的集合返回无界结果集,是设计上的缺漏,而不是查询选择。

  18. 计算每种访问模式的成本。读取定价取决于 API:

    • GetItem / BatchGetItem:按返回的项目计费,按 4 KB 向上取整。最终一致性(默认)= 每个项目每 4 KB 收取 0.5 RCU;强一致性 = 每 4 KB 收取 1 RCU;事务性 = 每 4 KB 收取 2 RCU。BatchGetItem 不提供单次请求折扣——每个项目分别计费。
    • Query:按符合键条件的项目总大小计费,按 4 KB 向上取整,费率与 GetItem 的每 4 KB 费率相同。FilterExpression 在计费之后应用——即使筛选条件丢弃了 90% 的匹配项,成本也不会因此降低 90%。
    • Scan:按检查的项目总大小计费,即实际读取的表或 GSI 部分;同样是在应用任何筛选条件之前计费。这正是 Scan 不适合用于高频路径的根本原因。

    写入定价:WCU = frequency × ⌈item_size / 1 KB⌉ × copies_updated。UpdateItem 按更新前后项目大小中的较大值计费,而非按差值或仅按更新后的大小计费——即使更新使项目变小,仍要按较大的更新前大小向上取整后付费。事务性写入的成本为 2 倍。ConditionalCheckFailedException 消耗的 WCU 与同类成功写入相同;估算时应将 conditional_fail_rate 纳入考量。

    如果无法为某种访问模式写出这些公式,就说明该模式的定义不够精确,无法建模。

  19. 默认使用按需容量;有证据后再切换。按需模式消除了某类与容量相关的故障,是新工作负载或负载变化较大时的正确默认选择。对于持续高吞吐量且模式可预测的工作负载,预置容量配合自动扩缩容可降低单位成本。应根据实测流量触发切换,而不是提前预判。

    对于包含向量索引的表,按需模式不是默认选项,而是强制要求。 预置容量的表完全不支持向量索引。创建索引前,必须将预置容量表切换为 PAY_PER_REQUEST;而此切换受 24 小时容量模式冷却时间(事实 #3)限制,因此需要纳入排期,而不是视为可即时完成的步骤。这也意味着,任何包含语义搜索的设计都无法选择预置容量,因此之后不要以降低单位成本为由,提议将此类表切换为预置容量。

模式

以下是与上述公理并列的具体实现模式。它们并非“正确性/运维/成本/风格”意义上的公理,而是完整设计所需的关键细节,即使没有任何公理明确要求,也不可或缺。参考架构中对每种模式都有完整说明;本节是简要版本,便于 Agent 判断何时适用。

  1. 使用 IdempotencyKeys 表保护会修改数据的 API 路由。 对于客户端可能重试的任何 POST、PUT、PATCH 或 DELETE(移动网络故障、负载均衡器重试、用户重复轻点),都必须使用幂等中间件。结构如下:专用表,PK = <user_id>:<METHOD>:<path>:<client-supplied-uuid>,属性为 status、response_status、response_body、expiration(Unix 纪元秒数)。处理程序运行前,中间件会使用 attribute_not_exists(id) 执行条件 PutItem;发生冲突时,则读取缓存的响应并返回。TTL 为 24 小时;关闭 PITR(该表是缓存)。这种机制可以避免重试“创建订单”导致重复向客户收费或重复扣减库存——TransactWriteItems 在单次调用中提供这种保障,但无法保障跨多次重试。

  2. 使用条件表达式保护会修改数据的写入。两部分——每当提到这种模式时,两部分都必须作为指导内容。

    A 部分——除非明确希望执行 upsert,否则在 UpdateItem 上使用 attribute_exists(<pk>)。 UpdateItem 默认会在缺失时创建。如果 PATCH /orders/:id 使用了猜测得到或过期的 order_id,就会悄无声息地生成一条虚假的订单记录——没有错误,没有日志,也无法恢复。对于调用方断言该记录存在的每个 UpdateItem,都添加 ConditionExpression: attribute_exists(<partition_key>);捕获 ConditionalCheckFailedException,并将其转换为 404。

    B 部分——当你希望“已存在时失败”时,在 PutItem 上使用 attribute_not_exists(<pk>)。 PutItem 默认会覆盖已有记录。本应创建新订单(或新评价、新配送员分配记录、新唯一性哨兵)的 POST /orders,如果没有保护条件,就会悄无声息地覆盖具有相同键的现有记录。对于已存在该键时应失败的每个 PutItem,都添加 ConditionExpression: attribute_not_exists(<partition_key>);捕获 ConditionalCheckFailedException,并将其转换为 409 Conflict。

    数据层的不变量由条件写入保障。建议其中一部分时,也要指出另一部分——设计评审经常会发现:应用多年前添加了 UpdateItem 保护条件,却漏掉了 PutItem 保护条件。

  3. TTL 属性使用 Unix 纪元秒数。诊断“TTL 没有删除任何内容”问题时,必须列出以下三种常见格式错误,因为它们都会悄无声息地导致过期机制失效。 DynamoDB TTL 会将属性读取为数字,并将其解释为自 Unix 纪元起的秒数。以下任何一种情况都会导致 TTL 悄无声息地永不触发(没有错误,没有日志——清理器只会跳过该行):

    1. 使用毫秒而不是秒。 JavaScript 中的 Date.now() 返回毫秒;如果原样写入,时间戳会大约大 1000 倍,解析后将是距今约 5 万年的时间,因此永远不会成为过去。修正方式:JS 中使用 Math.floor(Date.now() / 1000),Python 中使用 int(time.time())。
    2. 使用 ISO 8601 字符串而不是数字。 Python 中的 datetime.datetime.now().isoformat() 或 JS 中的 new Date().toISOString() 会生成类似 "2026-05-13T09:45:00Z" 的字符串。DynamoDB TTL 会将字符串视为非数字,并直接跳过该行。
    3. 属性名称错误。 TTL 在每张表中使用特定属性名称配置(ttl、expiration、expires_at 等)。如果应用写入的是 expires_at,而 TTL 配置的是 ttl,或者拼写错误导致属性名称不一致,TTL 就会扫描一个不存在的属性并跳过该行。请确认表的 TTL 规范中配置的属性名称与应用写入的名称完全一致。

    在写入时进行校验:在 TTL 值写入表之前,将任何大于 current_time + 50 years 的值标记为可能存在毫秒与秒混用的错误。在整个代码库中统一使用同一属性名称。这与机制 #12 相辅相成:仍须遵循“读取时过滤”的准则,因为即使 TTL 配置正确,删除也不是即时发生的。

  4. 不要为临时推送、电子邮件或短信持久化 Notifications 表。 每个领域名词都建一张表的本能在这里是个陷阱。如果通知发送后即被遗忘——推送到 APNS/FCM,通过 SES 发送事务性电子邮件——正确的架构是 EventBridge → SQS → NotificationLambda → delivery service。DynamoDB 仅用于需要日后查询的状态。只有在确实存在针对它的读取访问模式时,才创建 Notifications 表——例如每位用户的应用内收件箱、投递状态仪表板或合规审计。否则,你付出 DynamoDB 写入成本,却永远不会读取结果。若需要每位用户的收件箱:PK = user_id,SK = <createdAt>#<notificationId>,并为较早的条目设置 TTL。

  5. 将级联删除规划为分批执行的 BatchWriteItem,而不是同步 API 调用。 在分层架构中删除父实体(看板、订单、租户,或 GDPR 下的用户)时,需要删除其下所有键对应的数据。使用 BatchWriteItem(每批 25 项),并通过 Query(..., Limit=25, LastEvaluatedKey=...) 分页处理。这是一个后台任务,而不是同步 API 响应——在较大规模下,遍历所需时间足以超过典型的 Lambda 超时时间,而且部分失败必须能够重试。如果 GDPR 或法规要求删除数据,就应将此任务明确纳入设计;不要假设它会自动从架构中免费实现。

成本估算

设计和讨论数据模型是默认的无 AWS 路径,用户通常是在探索选项——键、GSI、单表与多表、投影取舍——但还不想知道具体金额。不要每次讨论设计时都习惯性地运行计算器。 满足以下任一条件时,才生成月度成本估算:(a)用户询问成本(或索要成本报告);或(b)设计即将定稿——用户表示准备采用该设计、提交评审,或以其他方式希望确定具体数字。在此之前,继续讨论设计。

如果没有完整的设计,就完全跳过估算——例如仅为单个查询估算规模、排查热分区,或解答具体机制问题。在这些情况下,没有可供估算的内容。

生成估算时,以下三步缺一不可:(a)编写 dynamodb_data_model.json;(b)调用 ${SKILL_DIR}/scripts/calculate_costs.py;(c)在摘要中引用生成的 cost_report.md。不运行计算器、直接在正文中计算数字,绝不能作为替代方案——即使只是给出“粗略”或“实际”的数字也不行;即使是为了将醒目显示的数字与给定的日流量进行核对也不行(这种情况下,应设置 avg_rps,让计算器生成预期流量场景——见下文)。计算器会使用与实时账单校准的定价模块;手动计算会导致数字悄然偏离,最终用户可能会采用一份无法自圆其说的报价。

计算器位于 ${SKILL_DIR}/scripts/calculate_costs.py。它读取的 JSON 架构记录在 ${SKILL_DIR}/references/cost-model-schema.md 中——首次生成成本估算时,请先阅读该文件。

工作流程:

  1. 将设计序列化到 dynamodb_data_model.json。 计算器不会重新设计数据模型,而是读取你已经生成的设计。你构建的访问模式列表(依据 Mechanics #2、#18)几乎可以原样转换为 JSON 中的 access_patterns 数组;你构建的架构(依据 Data modeling #3、#7、Mechanics #4、#7)可以转换为 tables、gsis,并在设计中包含向量索引时转换为 vector_indexes。如果计算器需要的字段缺失(最常见的是某个模式缺少 peak_rps),这说明设计本身存在缺口——请向用户确认或做出合理且有依据的假设并明确标注,而不是填写 0。 向量索引作为 vector_indexes 条目加入模型,其搜索则作为带有 top_k 的 SearchVectors 访问模式加入。 计算器支持这两者(完整结构见 ${SKILL_DIR}/references/cost-model-schema.md)。请注意它与服务 API 之间的不对称:你通过 VectorIndexes CreateTable 参数创建索引(事实 #10),但通过 vector_indexes 模型字段计算其成本。了解 API 结构并不等于了解模型结构;混淆两者就会导致向量索引的估算成本为零。

    如果你发现自己得出“计算器不支持向量搜索”的结论,那是因为模型中缺少向量索引——请修正模型,而不是退回手动计算。 计算器会计算向量索引存储和向量写入容量的费用,并将两者计入总额;它会有意不计算向量搜索的费用,而是打印 not priced,并附上说明和实测驱动因素。这两部分都已处理,因此手动计算其中任何一项,恰恰就是上述规则禁止的内联算术。这可不是个小疏漏:对于一个覆盖约 3 KB 条目、维度为 1024、投影类型为 ALL 的索引,向量写入通常是估算中单项最大的费用——在 200 rps 下测得为每月 $1,942,而基表写入本身为每月 $972——因为每次写入时,ALL 都会将整个条目复制到索引中。凭直觉估算这个数字,可能会让你低估实际的最高成本驱动因素,幅度超过一个数量级。

    当用户说明平均流量或每日数据量时,请在模式上设置 avg_rps——不要手动计算一个“现实”的数值。 总额按持续峰值计算(每种模式 24/7 均按 peak_rps 运行);如果 15K 笔订单/日的工作负载对应的持续峰值月度费用很高,用户还需要了解预期费用。设置 avg_rps(例如,15,000 笔订单/日 ÷ 86,400 秒 ≈ 平均 0.17 rps,而黑色星期五峰值为 600 rps),计算器就会按该平均速率再输出一个预期月度成本总额。这是生成现实流量成本的唯一获准方式——用心算将峰值总额换算成每日流量成本,恰恰就是上述规则禁止的内联算术。

  2. 运行计算器。 将 JSON 写入你选择的工作区路径,然后调用:

    python3 ${SKILL_DIR}/scripts/calculate_costs.py --model /path/to/dynamodb_data_model.json --output /path/to/cost_report.md
    

    JSON 是中间产物。除非用户提出要求,否则无需向其展示。

  3. 展示成本报告。 返回 cost_report.md——内容较短时可直接展示,否则提供文件链接。突出报告列出的主要驱动因素(按降序排列的“成本模式”表),并说明你采用的假设(估算的 RPS 数值、假设的保留期、一致性选择)。报告顶部的免责声明是产物的一部分——不要删去。

  4. 利用成本报告检视设计,而不只是汇报数字。 成本估算具有诊断作用。如果某种模式占据账单的大头——典型情况是 GPS ping 这类高频写入——这就是建模信号,而不只是一项费用:重新审视聚合选择(机制 #1)、投影结构(机制 #7)、转换时使用稀疏 GSI 是否能减少放大效应(机制 #6),或该模式是否根本不适合放在 DynamoDB 中(集成 #8)。提出替代方案,并在修订后的模型上重新运行计算器。这正是将计算器打包在一起的主要价值:从“这是设计”→“这是账单”→“这是更便宜的设计”的反馈循环只需一两个来回,而不必另行开展一项工作。 此计算器仅模拟请求和存储成本,不模拟 Streams 读写成本、PITR、备份、DAX 或数据传输。如果这些费用项可能对工作负载有实质影响,请在估算中一并指出,让用户了解该数字不包含哪些内容。存储按完整公开价格计费——不假设享有 25 GB 免费额度(该额度适用于整个账户,而且往往已经用完),因此绝不要告诉用户“存储免费”。将写入模式的 retention_days 设为相应值,以确定其存储数据量;计算器会按此计算。存储数值是按该 retention_days 计算的有限保留期快照——对于没有 TTL/“永久保留”数据的表,实际存储量会在快照之后持续增长,因此不要把稳态存储数值重新表述为“ N 年后”的数值:应说明该数值假设的保留窗口,或者指出没有 TTL 的表会无限累积数据。

**请在最终回复中点名这些产物。不要让工具调用消失在行文中。**用户看到的是你的自然语言输出,而不是你的 shell 历史记录。运行 calculate_costs.py 后,你的回复必须点明:(a)你写入的 JSON 文件(dynamodb_data_model.json);(b)你调用的脚本(${SKILL_DIR}/scripts/calculate_costs.py);(c)该脚本生成的报告文件(cost_report.md)。只展示成本数字却不提这些产物文件,会让人觉得你是手动算出的——工作流证据必须写在正文中,而不能只留在工具调用记录里。

实时验证

完成成本估算后,主动提出进行实时验证——在用户指定的 AWS 账户中部署表和 GSI,并对其施加缩减规模的流量,以测量实际的每操作容量和往返延迟。用一行询问,且只有在用户明确同意后才运行。即使在这一行的提议中,也要说明用户需了解的两项限制,以便负责任地选择是否同意:只能在沙盒/测试账户中运行(绝不能在生产环境或包含真实用户数据的账户中运行),并且清理由用户负责(该技能会提供 teardown.sh,且绝不会自动删除)。在任何部署之前,仍须先完整披露以下四项事实——这只是为了避免一行提议过于轻描淡写。对于范围较窄的问题——单条查询的容量评估、针对性调试、具体机制问题——如果没有完整设计,也就没有可供基准测试的内容,则跳过此步骤。

运行之前,务必用自己的措辞明确说明安全预期(deploy_model.py 脚本也会打印警告横幅,但这只是额外保障——明确安全预期是 Agent 的责任)。请在回复中逐字说明以下四项事实,不要将其改述成更简短的摘要:

  1. 实时验证会在用户指定的 AWS 账户中创建真实的 DynamoDB 表、Lambda 函数和 IAM 角色。
  2. 实时验证会产生真实的(小额)AWS 费用——实际通常为个位数美分,但确实会产生账单。
  3. 该账户必须是测试账户或沙盒账户——绝不能是生产账户,也绝不能是包含真实用户数据的账户。
  4. 该技能不会自动清理——结束时,用户会收到 teardown.sh 脚本以供审核;只有在用户明确审核并表示要执行后,该技能才会代其执行(见下方第 6 步)。

说明上述四项事实后,请用户明确提供配置文件、区域,并确认账户用途。只有满足这些条件后,才生成 benchmark_config.json 并调用 deploy_model.py。如果用户尚未指定测试账户,请停止并询问。仅说“这会创建真实资源,请确认配置文件”并不足够——必须逐项列出这四项事实,因为用户往往不知道具体会创建哪些资源,也不知道清理由自己负责。

与成本估算相比,此步骤有两点不同,因此运行方式也必须相应调整:

  1. 真实资源,真实账单。 实际运行缩小版任务通常只需几美分,但使用的是真实账户。绝不静默运行。始终要求明确指定 AWS 配置文件、区域,并确认调用方身份。
  2. 拆除分两阶段进行。 阶段 1:技能生成 teardown.sh 并交给用户。阶段 2:技能可以执行该脚本——但仅限用户明确表示已审阅脚本,且明确指示技能运行脚本之后。技能绝不会主动调用 DeleteTable,也绝不会仅凭一句简单的“开始”或“继续”就运行 teardown.sh。

拒绝启动时,明确列出四项前置条件。 在回复中将它们列为编号项目;不要将它们合并成一段笼统的“我需要更多信息”。指出具体未通过的前置条件,让用户知道是什么阻止了运行:

  1. access_patterns 为空,或任一模式缺少 peak_rps 或其值为 0 → 按机制 #2 的表述拒绝:“未知的 RPS 是设计缺陷,不是基准测试输入。模式 <id> 未声明 RPS——请先提供估算值,再进行基准测试。”
  2. 未安装 boto3 → 原样打印:pip install boto3>=1.34。
  3. AWS 凭证缺失或已过期 → 打印准确的修复方法,例如 SSO 使用 aws sso login --profile <profile>,或使用相应 boto3 异常建议的具体修复方法。
  4. 调用方身份的账户别名或 ARN 包含 prod、production、prd 或 live → 拒绝,并告知用户提供沙盒/测试配置文件。

在写入 benchmark_config.json 或调用 deploy_model.py 之前,检查全部四项。笼统地改述“我需要更多信息”不符合要求——必须明确列出未通过的具体前置条件。

工作流,用户同意后:

  1. 重用成本估算中的 dynamodb_data_model.json。不要重新序列化。

    部署前,确认每个数字/二进制键都声明了类型。 部署过程会在 entities[].attributes[] 和 attribute_definitions 中查找每个键属性(表键及 GSI 分区键/排序键),以此构建每个表的 AttributeDefinitions;找不到的属性会默认设为 S。未声明类型的数字纪元排序键或数字 ID 会以字符串形式部署,随后真实写入时会因 ValidationException 被拒绝。根据数据建模 #11,在此步骤前,确保每个此类键在模型中都带有 "type": "N"(或 "B")——不要因为种子数据干净就认定没有问题,因为故障可能只会在真实写入时出现。若想多一重保障,请在部署后执行 DescribeTable,并确认键的 AttributeType。

    如果设计包含许多模式,请限定运行范围——发出警告、估算时长并征求确认。 基准测试会按顺序驱动各个模式(每个模式依次单独运行预热和测量窗口),因此总耗时会随模式数量增加:约为 table_settle + Σ_patterns (warmup + duration)。采用标准窗口(约 10 秒预热 + 约 60–90 秒测量时长)时,包含 6 个模式的设计需要几分钟,但包含 20 个以上模式的设计需要 25–45 分钟以上,并会拆分为多个连续的 Lambda 调用。在启动包含约 8 个以上模式的设计的运行之前:说明大致耗时,并征求明确确认(“这将对全部 24 个模式进行基准测试,并耗时约 35–45 分钟——继续,还是改为测试一个精简子集?”)。不要在不告知用户的情况下启动耗时 40 分钟的运行。 只对关键模式提供负载测试,并与用户一起选择。 完整的实时测试通常不需要涵盖所有模式——大多数都是低成本的结构化查询,计算器已经能准确估算其单次操作成本。可以提出对精选子集进行基准测试,并与用户共同确定具体模式,优先选择实时测量确实有价值的模式:

    • 吞吐量最高的写入/高流量写入——其 RPS 占账单比重最大或最能考验容量的模式(例如位置 ping 写入)。
    • 事务/多项目核心操作——TransactWriteItems、多表写入、结账路径(事务成本为 2 倍,Mechanics #18,值得进行实时测试)。
    • 新颖或有风险的键结构——任何包含扇出读取(N 个并行 Query)、存在热键风险或涉及不确定 GSI 的模式;跳过常规的按 ID GetItem 模式。
    • 成本占主导的模式——成本报告中标记为主要成本驱动因素的模式。

    若要测试子集,请对模型的精简副本进行基准测试:保留所有 tables(以确保架构能够正确部署和播种),但将 access_patterns 精简为所选模式。成本估算不会受影响——它始终根据完整模型和声明的峰值运行;选择子集只会缩小实时驱动流量的范围,不会影响设计或成本估算。说明要测试和跳过哪些模式(以及原因),让用户了解覆盖范围上的取舍。

  2. 编写 benchmark_config.json——必须设置的字段只有 profile 和 region(如果省略 resource_prefix,部署时会自动生成;其他所有字段都有由模式决定的默认值)。首次运行时请阅读 ${SKILL_DIR}/references/performance-model-schema.md。写入文件之前,先询问用户要运行哪种模式——不要默默选择默认值。 对于一次性实时验证,应在下面两种单位成本模式中选择;第三种模式 "representative" 是迭代设计循环所用的模式(有界规模下的热键倾斜),详见迭代设计循环——只有当用户想探测热分区/限流风险时才提供此模式,不要用于普通成本验证。

    如实说明所驱动的吞吐量,并提供加大负载的选项。 这是实时验证中最重要的一点。默认情况下,quick/standard 只会驱动声明峰值的一小部分(scale_factor 约为 0.01,且不会低于 min_rps_per_pattern)——对于峰值较低的设计,每种模式会下限至约 1 个请求/秒。这属于单位成本样本:用于验证单次操作容量(成本)和无负载延迟,费用低廉(几分钱,每种模式约 1–2 分钟)。它不会施加负载——在约 1 rps 下,零限流在数学上是必然的(按需容量基线约为 2,000 WCU/4,000 RCU),测得的延迟也处于最佳情况。因此,提出测试时要说明测试类型,并提供另一种选择:

    • 单位成本样本(默认)——验证成本和无负载延迟。费用低廉。不测试负载/限流。
    • 按声明峰值或接近峰值驱动——真正的负载测试。在 benchmark_config.json 中设置 scale_factor(声明峰值的比例,例如 1.0 表示完整峰值)或 max_rps_per_pattern;两者都会生效。受单个同区域 Lambda 上限(每种模式约 1,500–2,000 rps)约束,并受 cost_guardrail_usd 和 --allow-spend 控制(避免高成本测试在未提示的情况下启动)。**注意:**新创建的按需表起始基线约为 2,000 WCU/4,000 RCU,随后会延迟一段时间才逐步扩容;因此,部署后立即施加高负载,测到的是冷启动时自适应容量的预热过程,而不是稳态表现——测试已有 ramp_seconds 和 table_settle_seconds,可吸收其中一部分影响;要获得可靠的高 rps 测量值,请使用更长的 duration_seconds。
    • representative 模式——设计循环中用于测试热键/限流风险的模式(有界规模下的 zipf 倾斜);当问题涉及热分区行为时使用(参见迭代设计循环)。 无论 quick / standard 模式还是吞吐量如何,外推的月度费用都相同——每次操作的 CU 是确定的,费用外推始终使用声明的峰值,因此,更高吞吐量的运行能提供负载下的延迟和限流信号,而不会改变费用数值。**运行前,务必告知用户你将采用的每种模式的速率。**按字面原样提出模式选项问题,然后等待用户选择:

    要运行哪种模式?请选择一项:

    • "quick" — 每个模式窗口约 1 分钟,用于冒烟测试,以确认设计能够部署和运行。百分位数稳定性较差。单位成本采样(对于峰值适中的设计,每种模式约以 1 rps 驱动):验证费用和无负载延迟,不验证负载。
    • "standard"(默认)— 使用完整时长的窗口,获得稳定的 p50 / p95 / p99,以及可信的月度费用外推值。默认仍为单位成本采样(每种模式仍约为 1 rps)——费用数值与 quick 相同;你会获得更稳定的延迟百分位数,而不是负载测试。若要进行负载测试,请告诉我以声明的峰值速率或接近该速率进行驱动。 (另有 "representative" 模式——适用于迭代设计循环的热点键 / 限流风险模式。仅当问题涉及热分区行为时才提及;参见“迭代设计循环”。)

    将用户选择的值写入 benchmark_config.json 的 mode 字段。如果用户跳过问题,只说“开始”,则默认使用 "standard",并在回复中说明该默认设置(“你没有指定模式,因此我会运行 standard 模式——如果你想运行 quick,请及时打断。”)。

  3. 部署:python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model … --config … --manifest-out created_resources.json --yes-deploy。打印调用方身份以供确认,遇到生产环境标记时中止,将每个资源的名称添加前缀 ddb-skill-bench-<date>-<uuid8>-(如果你未设置 resource_prefix,则自动生成),并为资源添加运行 ID 标签。在交互式终端中,打印账户信息后会等待几秒,以便通过 Ctrl-C 中止;在 Agent / CI 运行中,会依据 --yes-deploy 授权立即继续。

  4. 基准测试:python3 "$DDB_SKILL_DIR/scripts/benchmark_model.py" --model … --config … --manifest created_resources.json --raw-out perf_raw.jsonl --summary-out perf_summary.json。自动处理冷启动规范(见下文)。

    **执行规范——在前台运行并等待。一次 representative 运行会持续数分钟(每种模式都要经过稳定阶段、预热阶段和测试时长,所有模式累计通常为 15 分钟以上,多模式设计尤甚)。将基准测试作为单个阻塞调用运行并等待其完成——切勿在后台运行后将控制权交还用户。**后台运行可能在当前轮次 / 会话结束时被终止,导致 perf_summary.json 中留存上一次运行的数据——这正是旧摘要被误当作最新摘要读取的原因。如果运行可能超过工具的最长执行时间,**请延长工具超时时间——不要缩短窗口,也不要将任务放到后台运行。**无需为了适应时间预算而手动调整 duration_seconds:繁重的工作会在 Lambda 内部运行(服务端最多 900 秒),而 benchmark_model.py 已会将负载拆分为连续的多次调用(_compute_split)。运行完成后,记下启动时的 UTC 时间;第 5 步将用它与摘要中的 benchmark_completed_at 进行比对。

  5. 报告:python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model … --summary perf_summary.json --output performance_report.md。 在解读摘要前,先确认摘要是最新的,且与您描述的实验一致(应用操作规范)。 perf_summary.json 会记录 benchmark_completed_at(UTC)和运行实际使用的 config。在生成或信任报告前,请确认 (a) benchmark_completed_at 晚于您在步骤 4 中记录的启动时间,且 (b) 摘要中的 config(mode、provisioned_capacity、duration_seconds)与您为 本次 运行设置的值一致。如果任一条件不满足,或者字段缺失,说明基准测试尚未完成,您看到的是过时文件:不要解读,重新运行。 报告的 Deployment 区块会显示 benchmark_completed_at 和运行时间窗口,以便用户看到相同信息。判断数据过时的信号:基准测试数字与您预期应有所不同的先前运行逐字节相同。

    运行您承诺要运行的内容——确认配置匹配,而不只是确认数据是最新的。 如果您告诉用户,本次运行会改变某项设置(提高预置容量上限以隔离偏斜、固定一部分模式、提高 rps 或切换模式),那么在汇报前,请读取最新 perf_summary.json 中的实际 config 区块,并确认其中体现了这一改动。最新运行若仍使用旧配置,就不是您承诺的实验——解读它回答的是另一个问题,而非您设置的实验问题。如果您发现自己描述的实验实际上并未配置,请坦诚说明,并重新运行正确配置;或者,如果无论如何该运行在物理上都无法产生相应信号(例如,约 1 rps 的单位成本/下限运行,无论如何设置容量都无法使任何分区键变热,因此永远无法隔离偏斜——只有代表性模式或以声明峰值或接近峰值运行才能做到;提高下限运行的预置容量上限不会改变任何结果——机制 #3,以及下文的 单位成本/下限运行),就应说明这一点,而不是悄悄以相同配置重新运行。绝不能把一次未更改变量的重新运行说成是在测试该变量。实际中容易遇到的问题是:用户发现两次“不同”的运行产生的数字几乎完全相同——请主动检查配置,避免这种情况。

    ⚠ 当用户要求“清理”“拆除”“移除”或“删除”基准测试资源时,绝不要运行 rm、rm -f、ls、find、aws dynamodb delete-table 或任何其他临时删除命令。 此技能中的“拆除”是指调用 generate_teardown.py 生成 teardown.sh,绝不是自行运行 shell 命令删除本地文件或 AWS 资源。拆除流程见下文步骤 6。本地构件文件(performance_report.md、cost_report.md、dynamodb_data_model.json 等)由用户决定保留或删除——此技能不会清理它们。

  6. 拆除——先生成脚本,仅在用户明确审阅并表示意图后执行。 用户要求拆除、清理或移除基准测试资源时:

    a. 生成脚本:运行 python3 ${SKILL_DIR}/scripts/generate_teardown.py --manifest created_resources.json --out teardown.sh。这会写入 teardown.sh,不会删除任何内容。

    b. 在回复中提供脚本路径、审阅命令和执行命令。 按以下原文格式:

    • 审阅:cat teardown.sh(或用编辑器打开)——该脚本会删除 ddb-skill-bench- 前缀下的 DynamoDB 表、Lambda 函数和 IAM 角色。
    • 执行:bash teardown.sh --confirm——清单中的账户、区域和前缀会在生成脚本时写入脚本;删除前,脚本会重新检查调用者身份和前缀。(将 --confirm 替换为 --dry-run 可预览删除内容;添加 --delete-logs 还会移除 Lambda 的 CloudWatch 日志组——此日志删除不可撤销。) c. 不要把 "teardown" 与 "本地清理" 混为一谈。 绝不要以为任务是在删除本地文件就去运行 rm、rm -f performance_report.md、ls 或 find。只有 teardown.sh 会删除 AWS 资源;本地文件不是该技能需要考虑的事情。

    d. 该技能可以代表用户执行 teardown.sh,但仅在此确切的两部分条件同时满足时才可以:

    • 用户消息必须声明他们已审阅该脚本(例如:"我读了它"、"我审阅了 teardown.sh"、"我检查了内容"、"看过了")。从上下文中推断出已审阅不算数;必须明确声明。
    • 用户消息必须声明执行它的意图(例如:"运行它"、"执行它"、"继续并拆除"、"进行拆除")。

    e. 当用户要求该技能运行 teardown 但未声明已审阅时,该技能不得执行。相反,应给出确切的审阅命令(cat teardown.sh)并要求用户确认他们已审阅。一种必需的回复形式如下:

    在我运行 teardown.sh 之前,请确认你已审阅该脚本。运行 cat <PATH>/teardown.sh —— 它会删除在 ddb-skill-bench- 前缀下创建的 DynamoDB 表、Lambda 函数和 IAM 角色。读完后如果你想让我执行,请回复类似 "我已审阅,继续吧",然后我会运行 bash teardown.sh --confirm。

    f. 当用户要求该技能跳过审阅时("别担心"、"直接运行,不用给我看"、"相信它"),应拒绝。破坏性操作需要用户已声明的审阅。给出拒绝并重新提供审阅命令。

    g. 当 (d) 中的条件满足时,该技能可以运行 bash teardown.sh --confirm,展示完整的 stdout/stderr,并报告删除了哪些资源。

操作按声明执行,绝不被替换。 当设计说 BatchWriteItem 时,基准就以 25 项批次发出 BatchWriteItem。当它说 TransactWriteItems 时,基准就以声明的项集合发出 TransactWriteItems。不使用单项替代(用 PutItem 循环替代 BatchWriteItem,或用单独的 PutItem 调用替代 TransactWriteItems)—— 它们衡量的是不同的设计。事务 2× 写入成本(Mechanics #18)、批次部分失败语义以及事务边界(Mechanics #14)正是实时验证所观察的内容;替换它们就违背了初衷。如果用户询问基准如何演练批次或事务模式,请明确说明声明的操作按声明执行,并指出基准将观察的具体边界。

冷启动纪律。 DynamoDB 的延迟和吞吐量在新表的 t=0 时并不稳定;在这里不遵守纪律收集到的数字会误导性地看起来比稳态更差。在测量可信之前,必须消除三类偏差:

  • 新表容量。 按需表从基线容量(约 2,000 WCU,约 4,000 RCU)开始;在最初几分钟内高于基线的流量可能会被限流,直到自适应容量扩展。在 table_exists 返回后,在进行任何测量调用之前至少等待 table_settle_seconds(默认 30)。
  • 分区预热和客户端预热。 种子写入会预热基准稍后将访问的分区,而新的 boto3 客户端在首次调用时会带来 TLS 握手 + 凭证刷新(数十到数百毫秒)。基准在种子和测量之间短暂暂停,并为每个工作线程发出一次 DescribeTable,因此这些一次性成本不会出现在百分位数中。
  • 每个模式的预热窗口。 每个模式在其测量窗口之前运行一个 warmup_seconds 窗口(默认 10)。预热调用记录为 phase: "warmup",并排除在延迟百分位数和成本外推之外。性能报告单独展示预热 p99 —— 如果它明显高于稳态 p99,这本身就刻画了冷部署行为,值得指出。

测量与不测量的内容:

  • 已测量:通过 ReturnConsumedCapacity 获取每次操作消耗的 RCU/WCU;往返延迟 p50/p95/p99(分别报告预热和稳态数据);限流次数;GSI 写入放大;各模式的均匀分布信号;基于观测数据外推的月度成本。
  • 未测量:TTL 扫描频率、流消费者延迟和成本、持续突发负载下的自动扩缩、跨区域复制延迟、测量窗口之外的长尾流量形态,以及设计中任何非 DDB 服务(Lambda、API Gateway、SQS、EventBridge Pipes、OpenSearch 等)。性能报告会在“已设计,未基准测试”下列出这些内容,让用户清楚了解测得的数据涵盖其架构的哪些部分。

报告格式见 ${SKILL_DIR}/references/performance-report-format.md。它沿用 cost_report.md 免责声明优先、表格驱动的风格,并按模式同时列出观测值和计算器预期值,让用户了解其输入(RPS、项大小、一致性)与实际情况的差异。

反思发现并提供迭代循环。 写完报告后,应将其作为诊断依据,而非最终评分来审阅,并思考:测量结果是否表明存在其他可选设计?针对每项发现,将其归类:

  • 输入准确性发现——观测结果与计算器不符,因为某个声明的输入(RPS、项大小、一致性、conditional_fail_rate)有误。设计本身没有问题;需要更新估算所用的输入。建议更新 JSON,并仅重新运行计算器(成本低,无需重新部署)。
  • 设计发现——无论输入是否准确,观测结果都揭示了一个持续存在的结构性问题。示例及其适用的公理如下:
    • 持续出现的限流随分区键倾斜程度变化 → Mechanics #3(每个分区的上限,对热点键进行写分片)。在将限流归因于热点分区之前,先运行倾斜与资源不足判定门——两项都必须通过:(1)差异测试——热点分区的 p99 必须明显高于冷分区的 p99(报告中的约 1.8× 规则)。如果热点分区的 p99 ≈ 冷分区的 p99,则所有分区都在均匀地受到限流:这是容量不足,而非键倾斜——应明确指出这一点,不要将其归因于分区键。(2)均匀基线测试——使用报告已打印的每次操作观测 CU(不要手动推算项数),检查在当前驱动的 RPS 下,均匀键是否已经会超出预置容量上限;如果会,则此次运行无法单独验证倾斜——建议提高容量并重新运行,使其高于均匀负载需求,这样只有真正的热点分区才会受到限流。只有两项测试都通过后,才能有依据判定“热点分区达到其容量上限”。另请注意:对可变分区键(例如每次状态转换都会变化的状态)进行写分片,虽然能分摊 Mechanics #3 的负载,但该键仍会在每次写入时发生变化,因此 Mechanics #8(可变 GSI 键)的偏差依然存在,必须根据 Artifact #5 继续记录——要消除写放大,需要单独创建一张专用表,而不是添加分片后缀。
    • 某种模式占据账单的大部分 → Mechanics #1(聚合紧密度)、Mechanics #7(投影)、Integration #8(当工作负载属于分析型、高频遥测或全文检索时,迁出 DDB,改用 Timestream/S3/OpenSearch/Redshift/SageMaker Lakehouse)。
    • GSI 写放大高于投影所示 → Mechanics #6(可变键并非始终存在时使用稀疏 GSI)、Mechanics #7(将 ALL 降级为 INCLUDE/KEYS_ONLY)、Mechanics #8(将可变属性用作 GSI 键会使写入量翻倍)。
    • 观测到强一致性读取成本,但实际上并不需要 RYW 语义 → Mechanics #15(默认使用最终一致性),以及使用 ReturnValues=ALL_NEW 避免写入后重新读取的模式。
    • 无界查询反复触及 900 KB 页面上限 → Mechanics #17(约束每个查询;要求指定时间窗口或最大页面数)。
    • 按需模式基线下冷启动 p99 明显升高 → 容量模式或预热决策(Mechanics #19),通常不是架构变更。

当至少有一项设计发现具有实际意义——也就是说,所提变更有望降低 >10% 的成本、消除某个限流来源,或消除某项公理偏差时,告知用户:

  1. 哪些发现指向设计变更,哪些指向输入准确性问题。

  2. 对于每项设计发现,说明其对应的公理,并提供针对 dynamodb_data_model.json 的字面 JSON 差异——不能只用文字描述。将差异放在围栏代码块中,展示具体变更的字段(新增的 GSI 对象、移除的 GSI、更改后的 projection.type、带哈希后缀的分片 PK,或从 access_patterns 数组中完全移除的模式)。仅用文字建议“考虑降低投影等级”并不符合要求——用户必须看到确切变更,才能决定接受或拒绝。如果差异更改了 GSI 的键(常见情况是对分区键进行分片),必须根据 Fact #9 将其标注为新增 GSI 迁移,而不是只需更改代码——应先创建新索引并填充数据,再切换流量,最后删除旧索引。示例格式:

    - { "name": "OrdersByCustomer", "projection": { "type": "ALL" } }
    + { "name": "OrdersByCustomer", "projection": { "type": "INCLUDE", "non_key_attributes": ["status", "total"] } }
    
  3. 说明对主要成本驱动因素和限流来源的预期影响。 口头结论必须以报告为准,绝不能超出报告范围。 向用户提供的聊天摘要不得声称报告未归类的结果。具体来说:

  • 如果报告没有发出 key_skew_patterns 发现,就不要说设计会“限流”或“产生热点分区”——有依据的表述应采用报告原话(“在倾斜负载下未观察到热点分区问题”/“本次运行未验证”)。

  • 重述报告中的保留措辞;不要把结论说得更肯定。“在刻意设置的较低上限下出现了限流”不等于“GSI 分区很热”(参见上文的倾斜与容量不足判定门槛)。

  • 每个数字都必须准确对应它所支持的论断。较低的预置容量上限导致限流,只能证明表的容量不足以应对当前驱动负载,不能证明某个分区键很热。

  • 对比示例——相同数据,两种判定:❌ “已确认:OrdersByStatus GSI 在负载下严重限流。” ✅ “在 50 WCU 上限下,写入 Orders 表时出现了限流——但热点分区 p99 ≈ 冷分区 p99,因此这是均匀的容量不足,而不是键倾斜。本次运行并未隔离出热点分区效应;要测试这一点,需要在容量高于均匀负载需求的情况下重新运行。”

  • 单位成本/基线运行——不要把它们描述成负载测试。 如果本次运行是单位成本采样——采用 quick/standard 模式,或者任何一种每个模式的驱动 bench_rps 都等于 min_rps_per_pattern 基线值的运行(在 perf_summary.json 中显示为统一的 bench_rps,通常约为 1,并且报告免责声明中写有“单位成本采样,非负载测试”)——你不得将“0 次限流”或延迟百分位数作为设计能够承受负载的证据。在每秒不超过几次请求时,零限流在数学上是有保证的(按需基线约为 2,000 WCU/约为 4,000 RCU),而这些延迟是未承载负载时的最佳情况。应说明本次运行实际验证的内容——每次操作的容量(成本)和未承载负载时的延迟——并指出限流/容量上限“未进行测试——见报告中的未验证列表”。如果要向用户说明限流不是风险,应给出结构性原因,而不是零限流次数:例如,高基数分区键(多个用户各自对应不同的 user_id)会分散负载,因此不会有单个分区变热(机制 #3)。要实际测试限流,应将驱动负载提高到已声明峰值附近,或使用代表性模式。

    • 对比示例——约 1 rps 的习惯追踪器运行:❌ “已通过实时 DynamoDB 验证——0 次限流,读取 <8 ms,写入 <50 ms。可以提交审核了。” ✅ “本次运行验证了每次操作的成本(与模型相差不超过 2%)以及未承载负载时的延迟(读取 <8 ms)。运行负载约为 1 rps,因此没有测试限流——不过,这个设计无需担心限流:分区键是 user_id,因此负载会分散到各个用户,不会有单个分区变热。要证明峰值负载下的表现,我们需要重新运行,并驱动到你声明的峰值 rps。”
  • 用户引用任何数字时,都要对照你自己最新的产物进行核实——不要直接附和。 用户在你回复前重述某个数字(“每月 189 美元没问题”“所以那个分区大约是 600 WCU”“那 1,884 次限流”)时,应先对照当前 cost_report.md/perf_summary.json/performance_report.md 中的实际数字——并先按步骤 5 确认该产物是最新的,因为数字与一份过时摘要相符并不算真正核实。如果对不上——即使用户听起来很确定,尤其是附和用户更容易让对话顺畅时——也要明确纠正并引用产物:“更正一下:报告显示预期为每月 835 美元,而不是 189 美元——189 美元是较早的数字。还要继续吗?”默默接受错误数字,导致用户随后将其写进预算演示文稿或设计评审材料,这比造成一时的不愉快更糟。成本、容量计算、限流/延迟次数以及项目大小假设都适用这一要求。用户提供的数字是需要核实的输入,不是需要认可的结论。

  • 不要为本次运行未证实的论断背书。 你口头给出的结论只能陈述眼前产物实际显示的内容。如果你准备给出的数字来自你自己的推算,而非报告(手动外推的预置/预留成本、从 2k-rps 运行跳到“可线性扩展,所以 25k rps 没问题”、基准测试从未隔离验证的每分区数据),就应将其标明为推理,而非测量结果,并向用户说明背后的结构性依据——绝不要把估算说成已验证的结果。“计算器只对按需模式建模;预留容量数字是我根据公开费率手动计算的预置容量方案成本(预留容量仅适用于预置容量——事实 #7),请在制作演示文稿前用 AWS 计算器核实。”这种说法是诚实的。把手动计算结果说成“已验证”则不是。 然后,询问用户是否要继续迭代:应用一项或多项更改并重新评估。提供三个重新评估级别供用户选择。将三个选项作为三个字面项目符号逐条列出——不要合并成一段话,不要漏掉任何一项,也不要调整顺序。用户按名称选择。

  • 仅计算器 — 更新 JSON,重新运行 calculate_costs.py,显示新的 cost_report.md 和并排差异。成本低、不调用 AWS、无需拆除。这是默认选项,应最先提供。

  • 计算器 + 实时验证 — 完成上述操作后,再针对修订后的设计提供第二次实时运行。需要用户重新同意部署到 AWS,并接受第二个拆除脚本;除非更改仅为增量,否则应先运行上一次的拆除脚本。

  • 不做更改 — 用户可能认为这些发现可以接受,认为更改成本高于节省的成本,或认为建议的替代方案不符合此技能不了解的某项约束。根据 Artifact #5,将该决定连同用户说明的理由记录为“偏差”。

不要静默迭代,也不要无限迭代。每轮迭代都必须明确征得用户同意;如果用户拒绝,或报告中没有有意义的发现,就停止并交付最终产物。

迭代设计循环

上述三个重新评估级别作为由 ${SKILL_DIR}/scripts/iterate_design.py 编排的人工驱动循环中的轮次运行。这是实现“部署 → 在具有代表性的规模下测试 → 获取反馈 → 更改设计 → 再次测试”的顺畅流程。

它是什么——以及不是什么。 一次调用 = 一轮:可选地应用用户同意的更改 → 决定复用还是重新部署 →(经授权后)部署或复用 → 在具有代表性的规模下进行基准测试 → 报告 → 呈现发现和建议 → 停止。循环由人工驱动:你运行一轮,呈现发现和具体建议,然后交还给用户。由用户决定要更改什么,并提示下一轮。绝不自动迭代直至收敛;绝不在一次调用中启动第二轮;脚本会强制执行这一点(它没有提出或选择更改的逻辑——只会应用交给它的更改)。

代表性模式(明确说明范围)。 循环以 mode: "representative" 运行(参见 ${SKILL_DIR}/references/performance-model-schema.md):约为声明峰值的 0.10–0.25×,采用 zipf 热键采样和符合实际的项目集合基数。这能够揭示 1% 规模单位成本运行无法发现的问题——热分区限流(Mechanics #3)、负载下限流、流量增长时的 GSI 放大,以及实际基数下的 Query。向用户清楚说明范围:一个区域内的一项 Lambda 每种模式最多可驱动约 1,500–2,000 RPS(32 个驱动线程),因此代表性规模有所限制——它能以有限成本揭示热分区和限流风险,但不能证明设计可以持续承载声明的峰值。每次操作的成本外推仍保持线性且有效(每次操作的 CU 不随规模变化,并根据声明的峰值外推);限流和延迟数值是报告“负载风险信号”部分中的负载风险指标,绝不能线性外推。

超过单个 Lambda 的上限(尚未实现)。 如果设计需要的吞吐量超过单个 Lambda 每种模式约 1,500–2,000 RPS,则可同时调用同一个已部署的基准测试函数(Lambda 可免费水平扩展——无需部署多个函数),每次调用驱动 target_rps / N 的负载,只需播种一次,之后合并结果(调用数、限流数和错误数相加;每次操作的 CU 按调用数加权平均;延迟百分位数根据原始行中已有的每次调用延迟合并后重新计算)。这种负载分片尚未实现——目前上限就是单个 Lambda。不要承诺超过此上限的速率;应提供代表性模式,或说明此上限。 每一轮都必须征得同意。 每次实际部署和实际拆除都必须在每一轮明确征得用户同意。上述四项实时验证事实(真实资源、实际费用、仅限沙盒、不自动拆除)均适用于每一轮。iterate_design.py 若未指定 --yes-deploy,就会拒绝部署;基准测试若要支出超过 cost_guardrail_usd,就会拒绝继续,除非指定 --allow-spend。两阶段认证拆除(实时验证步骤 6)保持不变,仍是唯一的拆除方式——如果某一轮会更改架构,系统会提示用户先运行上一次的 teardown.sh,以免遗留资源。

按需容量与预置容量,以及为什么可能看不到限流。 代表性运行默认按需部署(PAY_PER_REQUEST),与该技能机制 #19 的默认设置一致。在按需模式下,自适应容量会隔离并吸收单个热点键的负载——热点分区通常表现为该分区的尾延迟升高,而非限流,因为表会自动扩展容量来承载负载。因此,key_skew_patterns 信号在出现限流或热点分区 p99 明显高于冷分区 p99(二者均为模式内比较,不依赖基线)时都会触发,这样按需设计也能收到警告。要观察明确的限流,请在 benchmark_config.json(或单表配置)中设置 provisioned_capacity: {"read": N, "write": M}——较低的预置容量上限没有自适应吸收能力,因此热点分区会稳定触发限流;这也是验证计划采用的预置容量的方法。基准测试 Lambda 会禁用客户端重试,以便观察并统计限流,而不是悄悄重试后将其掩盖。请注意,新建的预置容量表会有约 5 min 的突发容量,在短时窗口开始时会掩盖限流——请使用更长的 duration_seconds 或更低的容量,以消耗掉这部分容量。

请遵循以下轮次协议:

  1. 调用 python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" --model dynamodb_data_model.json --config benchmark_config.json --loop-state loop_state.json --manifest created_resources.json [--apply-change change.json] --mode representative [--timestamp <iso>] [--yes-deploy] [--allow-spend]。--timestamp 可选——省略时,本轮会使用当前 UTC 时间戳;只有在需要让循环状态条目使用特定时间(例如用于可复现的测试)时,才传入 ISO 字符串。本轮会运行代表性基准测试,因此适用与实时验证步骤 4 相同的执行要求:必须以单次阻塞式前台调用运行并等待——绝不要在后台运行(被终止的轮次会留下过时产物);如有需要,应提高工具超时时间,而不是缩短窗口。在转述任何结果前,按照步骤 5 验证数据是否为最新(本轮的 loop_state.json 摘要和 design_findings.json 都应基于同一份 perf_summary.json;如果没有刷新,本轮就尚未完成)。
  2. 仅阅读精简产物:design_findings.json、cost_report.md 和 loop_state.json。绝不要读取 perf_raw.jsonl(文件较大)或 performance_report.md(面向用户)。
  3. 转述本轮的精简摘要,以及 loop_state.json 中与上一轮相比的变化(例如:“对 PK 进行分片后,W1 限流次数从 1450 降至 0,p99 降低 70 ms”)。将每项建议的更改以针对 dynamodb_data_model.json 的字面 JSON 差异呈现(与上述迭代提议的差异要求相同)。然后停止,并询问用户要更改什么,或是否接受。
  4. 用户选定更改后,在下一轮通过 --apply-change 传入(一个 merge 对象或一个 ops 列表);在 loop_state.json 的 user_decision 中记录用户对上一轮的决定。 三个重新评估层级如何映射到循环:“仅计算器” → iterate_design.py --calculator-only(应用变更、重新计价,不调用 AWS);“计算器 + 实时验证” → 完整的代表性轮次(通过部署门控或复用);“不做变更” → 按 Artifact #5 将 user_decision 记录为偏差,然后停止。该循环是对原有方案的扩展,而非替代。quick/standard 模式仍用于一次性单次成本验证;representative 是循环使用的模式。循环状态记录在 ${SKILL_DIR}/references/loop-state-schema.md 中。

安全注意事项

安全要求贯穿上述公理(数据建模 #14 中与授权边界对齐的键、模式 #2 中的条件写入保护、机制 #13 中的事务唯一性)。本节汇总了遵循此技能的 Agent 无论采用哪条公理都必须向用户说明的风险,以及针对该技能可执行脚本的专门控制措施。在任何设计评审中,都应说明相关内容;不要假定用户已经了解这些风险。

  1. 授权边界对齐(IDOR)是首要的安全检查。 最常见的 DynamoDB 数据泄露错误,是将分区键设为客户端提供的标识符(例如 URL 中的 review_id/order_id),而不是服务器推导出的授权标识符(user_id、tenant_id、account_id)。两者不一致时,调用方可以传入外部键,访问跨租户边界的数据。应将分区键与授权/租户边界对齐(数据建模 #14),使外部键根本无法寻址;每次调用时使用 ConditionExpression` 是一种规范,而非结构性修复,也无法覆盖读取操作(事实 #4)。在任何设计评审中,都应先于热分区、投影或成本方面的发现,评估并说明这一点。

  2. 静态加密——始终启用;谨慎选择密钥层级。 DynamoDB 会对所有表进行静态加密,且无法关闭。默认使用 AWS 所有者密钥——无需付费、无需管理密钥;未设置 SSESpecification 时即采用此默认状态(实时验证部署正是如此;基准测试清单会记录从 DescribeTable 读回的静态加密状态)。这适用于临时/基准测试表和大多数工作负载。对于受合规制度约束的数据(需要审计密钥使用情况、独立轮换/撤销密钥、跨账户密钥策略,或法规要求持有密钥),建议使用客户管理的 KMS 密钥(CMK)——设置 SSESpecification,并指定 SSEType: KMS 和 KMSMasterKeyId(注意:DynamoDB 唯一的非默认 SSEType 是 KMS;DynamoDB 不支持 AES256 类型),同时说明每次请求产生的 KMS 费用和配置密钥策略的必要性。应在设计中明确说明加密选项(它是每个实体运营配置的输入之一),而不要将其留作隐式设置。传输中加密始终适用于这些脚本发出的所有 AWS API 调用:boto3 默认通过 TLS(HTTPS)与 DynamoDB/Lambda/IAM 通信,不存在明文传输路径;复用此模式的 Agent 应将所有传输中的数据视为已受保护,绝不要禁用 TLS 验证。

  3. 生成的构件包含敏感信息——应妥善处理。 部署会写入 created_resources.json 清单,其中包含 AWS 账户 ID、资源 ARN 和所用配置文件名称。它不是密钥存储(不包含凭证),但可以识别账户和资源,因此不要将其粘贴到共享频道、提交到公开仓库,或原样纳入报告。该技能的脚本绝不会将 AWS 凭证、令牌或连接字符串写入任何构件——请继续保持这一点;如果扩展这些脚本,不要记录凭证信息。

  4. **基准测试资源只能放在沙盒/测试账户中。**实时验证部署会创建实际产生费用的资源。它只能针对用户指定的沙盒/测试账户运行,绝不能针对生产环境或存有真实用户数据的账户运行。部署会拒绝使用带有生产标记的账户别名/ARN(prod/production/prd/live),但 Agent 有责任先说明实时验证的四项事实,并确认账户用途(参见 实时验证)。

  5. **凭证卫生。**绝不能将 AWS 配置文件、访问密钥、会话令牌或 credential_process 数据块持久化到技能生成的任何文件中,也绝不能将它们回显到输出中。脚本使用环境中的 AWS 凭证链进行身份验证(命名配置文件、SSO 或环境凭证);它们既不会读取所有任意环境变量,也不会记录解析出的凭证。写入 benchmark_config.json 时,只保存配置文件名称和区域,不保存其对应的已解析凭证。

  6. **向量搜索不遵循细粒度访问控制,向量索引分区键也不是租户边界。**对于包含语义搜索的任何多租户设计,都必须明确指出以下两种不同风险,并说明相应所需的补偿性侦测控制。

    首先,dynamodb: FGAC 条件键——dynamodb:LeadingKeys、dynamodb:Attributes、dynamodb:Select——不会出现在 SearchVectors 请求上下文中。因此,引用这些条件键的语句中的 Condition 不会匹配,结果是拒绝访问,而非授权。在现有的带 FGAC 条件语句中添加 dynamodb:SearchVectors,会悄无声息地导致向量搜索失效,同时该语句的其他操作仍正常运行;这会表现为索引故障,而非策略错误。应在单独的语句中授予 dynamodb:SearchVectors,将范围限定为索引资源 ARN(arn:aws:dynamodb:<region>:<account-id>:table/<table>/index/<index>),且不附加任何 dynamodb: FGAC 条件。

    其次,将搜索范围限定为某个租户的分区键值,是数据局部性和性能优化手段,不是访问控制机制。任何持有该索引 dynamodb:SearchVectors 权限的主体都能搜索任意分区键值;由于 FGAC 条件键不适用,你无法在 IAM 层限制其可访问的值。如果数据层需要严格的租户隔离,请使用独立的表,或使用具有不同租户专属 IAM 授权的独立索引;由应用层注入调用方的租户标识,且绝不接受客户端提供的该标识,可以作为补充,但不能取代上述措施。这是第 1 项中授权边界检查在向量搜索场景下的体现——但与基表不同,此处键架构不会让外部数据无法寻址,因此结构性修复方案应是分离授权,而非分区。静态加密继承自基表;向量索引没有单独的设置。

    第三,由于分离授权只能限制主体可搜索的索引,无法限制其可搜索的分区键值,因此建议对任何包含多租户向量索引的表启用 CloudTrail 数据事件日志记录,作为补偿性侦测控制。它是记录每个主体搜索了哪个值的唯一手段——这正是 IAM 无法限制的维度——因此,如果主体遭到入侵或应用层注入失效,跨租户搜索就会变得无法观测。针对每个主体的 VectorSearchRequestBytes 设置 CloudWatch 警报,可将分区枚举识别为流量异常。还要注意,嵌入向量本身也很敏感:即使不返回原始文本,相似度结果也会泄露两条记录在语义上接近。因此,默认不在结果中包含向量属性这一安全默认值值得保留;任何记录完整项目的操作都需要像处理表数据一样谨慎。完整说明:${SKILL_DIR}/references/vector-search.md § Security。

最佳实践

这些是推荐的默认设置,而非公理。它们不会影响设计,而是应默认启用的运维防护措施,除非有具体理由不启用。生成设计的 Agent 应启用以下每一项,除非工件中明确说明了禁用理由。

  1. 为每张表启用删除保护。 DynamoDB 的删除保护标志会在服务边界阻止 DeleteTable,直到明确将其禁用。它不产生任何费用,也不会影响性能,还能防止一类事故——IaC 配置错误、复制粘贴的命令,或本应限定范围的堆栈拆除——摧毁生产表。确实需要主动删除时,只需一步即可禁用;除此之外,所有表都应启用保护。

    两个容易被忽略的重要细节:

    • 删除保护默认值为 FALSE。 除非在 create-table 上设置 --deletion-protection-enabled(或在 CloudFormation/CDK 或任何第三方 IaC 工具中设置对应属性),否则新建的表不会启用删除保护。这就是许多较早创建的表,或创建时未经过审慎考虑的表,常常缺少此功能的原因——必须显式启用它。
    • 服务边界保护与 IaC 防护并不重复。 CDK RemovalPolicy.RETAIN、第三方 IaC 工具的 prevent-destroy / retain-on-delete 防护,以及类似机制,只能保护 IaC 操作路径——直接调用 aws dynamodb delete-table CLI、另一个指向同一资源的 IaC 状态文件,或恶意脚本都能绕过这些机制。删除保护直接作用于服务边界上的表,能拦截所有删除路径。应同时使用两者;不要认为仅靠 IaC 防护就足够。
  2. 为每张表启用时间点恢复(PITR)。 PITR 提供连续备份,保留期为 35 天,并支持按秒粒度恢复。其成本只是存储成本的一小部分;而在真正需要它的那一天——误删、迁移出错,或逻辑缺陷写入了损坏的值——它是唯一能恢复数据的手段。仅当临时表中的内容可从其他来源重建时,才禁用 PITR(例如幂等缓存、带 TTL 的去重存储)。根据数据建模 #3,PITR 配置在表之间存在差异,也是一种建模信号。

  3. 确保表可观测——为生产表设置告警并保留审计记录。 没有监控的表发生故障时不会发出任何信号;首次发现限流或成本失控,往往是收到客户投诉或看到月度账单。建议为每张生产表配置:

    • 针对 ThrottledRequests、SystemErrors 和 UserErrors 设置 CloudWatch 告警(对于预置容量表,还应监控已消耗容量与预置容量的对比)。至少应设置一条持续满足 ThrottledRequests > 0 的基础告警——这是热点分区或预置容量不足的最早信号,并且与机制 #3 中的上限直接相关。
    • 对于存放敏感或受监管数据的表,启用 CloudTrail 数据事件日志记录,以便审计条目级读写操作。默认情况下,CloudTrail 会捕获 DynamoDB 的管理事件;条目级(数据)事件需要按表选择启用,而这才是审计或事故调查真正需要的内容。如果审计或运维日志本身包含敏感数据,请使用 AWS KMS 客户管理密钥加密 CloudWatch Logs 日志组(以及 CloudTrail 轨迹的 S3 存储桶),确保静态存储的日志数据受到保护,而不仅仅是源表。

    这些是运维防护措施,并非设计输入;但由于这项技能的指导会复制到许多客户环境中,将它们纳入考量是设计具备充分依据的一部分,而不应事后才补上。

参考架构

将这些公理端到端应用于具体应用的完整示例。当相较于仅依据公理,参考一个可运行的模型更容易回答设计问题时,请查阅这些示例。

  • ${SKILL_DIR}/references/reference-architecture.md — 一个基于 AWS 和 DynamoDB 的多租户看板任务板(“TaskBoard”)SaaS。涵盖访问模式列表、表和 GSI 布局、Streams 向 OpenSearch 和 EventBridge 的扇出、与授权对齐的分区、实时交付,以及每一步所做的权衡。当用户询问完整的 DynamoDB 设计、多租户或数据库周边服务的端到端组合时,请从这里开始。
  • ${SKILL_DIR}/references/vector-search.md — 向量索引 API 表面(VectorIndexes、VectorIndexUpdates、SearchVectors)、SearchSchema 设计、距离函数和分数方向、就绪判定条件、投影和不可变性规则、静默取消索引、IAM(包括精细化访问控制的缺口)、基于字节的成本维度,以及按服务原样错误字符串索引的故障排查表。当设计或调试涉及语义相似性、RAG 检索、推荐或 AI Agent 记忆(事实 #10、集成 #8)时,请阅读此文。否则请跳过。
  • ${SKILL_DIR}/references/cost-model-schema.md — ${SKILL_DIR}/scripts/calculate_costs.py 使用的 JSON 结构。首次生成月度成本估算时请阅读此文(参见上文的成本估算)。非成本问题请跳过。
  • ${SKILL_DIR}/references/performance-model-schema.md — 实时验证脚本使用的 benchmark_config.json 的 JSON 结构。首次运行实时验证时请阅读此文(参见上文的实时验证)。如果暂不考虑实时验证,请跳过。
  • ${SKILL_DIR}/references/performance-report-format.md — ${SKILL_DIR}/scripts/generate_perf_report.py 生成的 performance_report.md 的结构。解读报告或起草设计反思部分前,请阅读此文。
  • ${SKILL_DIR}/references/loop-state-schema.md — loop_state.json 的结构,这是 ${SKILL_DIR}/scripts/iterate_design.py 写入的紧凑型逐轮溯源文件。运行迭代式设计循环时请阅读此文(参见上文的迭代式设计循环)。

评论

支持 Markdown