# K6 作品列表压测计划(使用 spacetime-migration-7.json 作为数据源) ## 目标 使用 K6 对 Genarrative 的“作品列表”相关接口进行压测,并将用户提供的 `spacetime-migration-7.json` 作为压测数据源;数据处理时**只导入作品列表相关数据**,不导入用户、会话、钱包、埋点、运行存档等非作品表,避免把敏感或无关数据带入压测环境。 ## 当前上下文 - 工作区:`/c/proj/Genarrative` - 原始迁移文件:`C:\Users\DSK\AppData\Local\hermes\cache\documents\doc_150e84029b2d_spacetime-migration-7.json` - 已确认原始迁移文件结构: - `schema_version = 1` - `tables = 53` - 作品相关表中当前有数据的重点表: - `puzzle_work_profile`:80 行 - `custom_world_profile`:1 行 - `match3d_work_profile`:0 行 - `big_fish_*`:当前样本中相关表为 0 行 - 原始文件还包含 `user_account`、`auth_identity`、`refresh_session`、`profile_wallet_ledger`、`asset_object`、运行记录等数据,压测导入时必须过滤。 - 当前仓库未发现现成 K6 脚本或 `k6` 相关文件,需要新增压测脚本与数据提取脚本。 - `package.json` 当前有 `dev/dev:rust/test/check` 等脚本,未发现 K6 npm script。 ## 范围约束 ### 本次只导入/使用 1. 作品列表表: - `puzzle_work_profile` - `custom_world_profile` - 后续若接口覆盖其他玩法,可扩展: - `match3d_work_profile` - `square_hole_work_profile`(以实际 SpacetimeDB 表名为准) - `big_fish_work_profile`(以实际 SpacetimeDB 表名为准) - `visual_novel_work_profile`(以实际 SpacetimeDB 表名为准) 2. 为作品列表卡片展示所需的最小字段: - 稳定 ID:`profile_id`、`work_id` 或 `public_work_code` - 标题:`work_title` / `level_name` / `world_name` - 描述:`work_description` / `summary` / `summary_text` / `subtitle` - 作者:`owner_user_id`、`author_display_name`、`author_public_user_code` - 封面:`cover_image_src`、`cover_asset_id`(如果接口只返回 asset id,则压测阶段不额外导入二进制 asset) - 状态与计数:`publication_status`、`published_at`、`play_count`、`like_count`、`remix_count` - 作品内容摘要:`levels_json`、`profile_payload_json`、`theme_tags_json` 等列表渲染或进入作品详情可能需要的 JSON 字段 ### 本次不导入/不使用 - 认证与账号:`user_account`、`auth_identity`、`refresh_session`、`auth_store_snapshot` - 用户资产与钱包:`profile_wallet_ledger`、`profile_dashboard_state`、`profile_redeem_*`、`profile_invite_*` - 游玩历史/存档/运行态:`profile_played_world`、`public_work_play_daily_stat`、`puzzle_runtime_run`、`profile_save_archive`、`runtime_snapshot` 等 - AI 任务过程:`ai_task`、`ai_task_stage`、`ai_text_chunk` - asset 二进制与绑定:`asset_object`、`asset_entity_binding`,除非后续确认作品列表接口强依赖它们;即便需要,也只导入作品列表封面所需的最小 metadata,不导入原始大对象。 ## 推荐目录与文件 建议新增: ```text .hermes/plans/2026-05-11_195214-k6-works-list-load-test-plan.md # 本计划 scripts/loadtest/extract-works-list-data.mjs # 从迁移文件提取作品列表数据 scripts/loadtest/k6-works-list.js # K6 压测脚本 scripts/loadtest/data/works-list.sample.json # 过滤后的样例数据(不要提交敏感原始迁移全量) scripts/loadtest/README.md # 执行说明与指标阈值 ``` 可选新增 npm scripts: ```json { "loadtest:extract-works": "node scripts/loadtest/extract-works-list-data.mjs", "loadtest:k6:works": "k6 run scripts/loadtest/k6-works-list.js" } ``` ## 数据提取方案 ### 输入 默认读取: ```bash node scripts/loadtest/extract-works-list-data.mjs \ --input "C:/Users/DSK/AppData/Local/hermes/cache/documents/doc_150e84029b2d_spacetime-migration-7.json" \ --output scripts/loadtest/data/works-list.local.json ``` ### 输出结构 建议输出为 K6 直接可读的 JSON: ```json { "source": "spacetime-migration-7.json", "generatedAt": "", "tables": { "puzzle_work_profile": [ { "profile_id": "...", "work_id": "...", "owner_user_id": "...", "work_title": "...", "work_description": "...", "publication_status": "Published", "published_at": { "__timestamp_micros_since_unix_epoch__": 0 }, "play_count": 0, "like_count": 0, "levels_json": "..." } ], "custom_world_profile": [] }, "workIds": { "puzzle": [""], "customWorld": [""] } } ``` ### 过滤原则 1. 按 `tables[].name` 白名单过滤,只保留作品 profile 表。 2. 对每个 row 再按字段白名单过滤,避免误带账号、手机号、token、钱包流水等字段。 3. 对特别大的字段进行处理: - `cover_image_src` 如果是 `data:image/...base64`,默认替换为占位符或截断,避免压测数据文件过大。 - `levels_json`、`profile_payload_json` 保留原文,但可以记录大小;如果过大,再提供 `--compact` 选项只保留摘要。 4. 输出 `.local.json` 默认加入 `.gitignore`;如果要提交样例数据,只提交脱敏/裁剪后的 `works-list.sample.json`。 ## K6 压测接口矩阵 需要先确认本地 api-server 实际端口。默认以 `http://127.0.0.1:8787` 为例,实际运行时通过环境变量覆盖: ```bash BASE_URL=http://127.0.0.1: k6 run scripts/loadtest/k6-works-list.js ``` 初版建议覆盖以下“作品列表”读接口,具体路径以仓库服务端路由为准,实施时需要通过搜索 api-server 路由确认: | 场景 | 目的 | 候选路径 | | --- | --- | --- | | 拼图作品列表 | 作品列表主场景之一,当前数据量最多 | `/api/creation/puzzle/works` 或实际 puzzle works list route | | RPG/自定义世界作品列表 | 使用 `custom_world_profile` 数据 | `/api/creation/custom-world/works` 或实际 custom world works route | | 作品详情/启动前读取 | 模拟用户从列表点进作品 | `/api/creation/*/works/:profileId` 或 `/api/runtime/*/works/:profileId` | | 公开作品库 | 如果首页/发现页依赖 | `/api/runtime/*/works` 或 gallery/list route | > 注意:不要凭空固定 endpoint。实施阶段先用 `search_files` / 路由源码确认真实路径,再写入 K6 脚本。 ## K6 场景设计 ### 阶段 1:基线 smoke 目的:确认脚本、数据和目标服务可用。 ```js export const options = { scenarios: { smoke: { executor: 'constant-vus', vus: 1, duration: '30s' } }, thresholds: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<800'] } }; ``` ### 阶段 2:常规读压 目的:模拟日常列表浏览。 - `constant-vus`: 10/25/50 三档 - 每个 VU 随机选择作品类型和列表分页参数 - `sleep(0.5~2s)` 模拟用户停留 - 阈值建议: - `http_req_failed < 1%` - `p95 < 800ms` - `p99 < 1500ms` ### 阶段 3:峰值/突刺 目的:模拟首页入口或活动导致的作品列表突增。 - `ramping-arrival-rate` - 从 5 RPS 增长到 100 RPS,维持 2~5 分钟,再降回 - 单独输出 `checks`:列表接口状态码、响应 JSON shape、items 数量 ### 阶段 4:容量探索 目的:找瓶颈,不作为每次回归必跑。 - 每轮提升 RPS 或 VU - 观察:api-server CPU/内存、SpacetimeDB 日志、错误率、p95/p99 - 一旦 `http_req_failed >= 5%` 或 p95 持续超过 2s,停止继续加压并记录容量点。 ## K6 脚本设计要点 1. 使用 `SharedArray` 加载 `works-list.local.json`,避免每个 VU 重复解析大 JSON。 2. 基于数据源里的 `profile_id` / `work_id` 随机抽样,保证请求覆盖真实作品 ID。 3. 对列表接口添加分页/排序 query,例如: - `?limit=20&offset=0` - `?pageSize=20&cursor=...`(以真实 API 为准) 4. 使用 `check()` 验证: - HTTP 200 - 响应体是 JSON - `items` 或 `works` 是数组 - 列表项包含 `profileId/profile_id`、标题字段、状态字段 5. 使用 `Trend` / `Rate` 细分指标: - `works_list_duration` - `works_detail_duration` - `works_list_shape_error_rate` 6. 支持环境变量: ```bash BASE_URL=http://127.0.0.1:8787 \ WORKS_DATA=scripts/loadtest/data/works-list.local.json \ SCENARIO=baseline \ k6 run scripts/loadtest/k6-works-list.js ``` ## 实施步骤 1. **确认路由** - 搜索 api-server / BFF 的作品列表路由。 - 明确各玩法对应 endpoint、鉴权要求、分页参数、返回字段。 2. **实现数据提取脚本** - 新增 `scripts/loadtest/extract-works-list-data.mjs`。 - 只按表白名单读取作品列表 profile 表。 - 对字段做白名单与脱敏/截断。 - 输出 `works-list.local.json`。 3. **生成本地压测数据** - 用用户提供的迁移文件生成 `scripts/loadtest/data/works-list.local.json`。 - 验证输出只包含作品表和作品字段。 4. **实现 K6 脚本** - 新增 `scripts/loadtest/k6-works-list.js`。 - 支持 `BASE_URL`、`WORKS_DATA`、`SCENARIO`。 - 覆盖列表接口,必要时增加详情/启动前读取接口。 5. **新增执行说明** - 在 `scripts/loadtest/README.md` 写明:安装 K6、启动本地 dev 栈、提取数据、运行 smoke/baseline/spike、查看结果。 6. **本地验证** - 启动 Genarrative dev 栈;注意端口可能漂移,使用实际 api-server 端口。 - 跑 smoke:`SCENARIO=smoke`。 - 确认失败率、p95、响应 shape。 7. **可选集成 npm scripts** - 如果团队希望标准化入口,再加入 `package.json` scripts。 8. **记录结果** - 将 smoke/baseline/spike 的结果摘要追加到 `scripts/loadtest/README.md` 或单独保存到 `.hermes/plans/` 的结果文档中。 ## 启动与运行建议 本地服务启动按当前 Genarrative dev 栈约定: ```bash npm run dev ``` 如果 SpacetimeDB/API/Vite 端口被占用,项目脚本会寻找可用端口;压测时必须从启动日志中读取实际 api-server 地址,并传给 K6: ```bash BASE_URL=http://127.0.0.1: \ WORKS_DATA=scripts/loadtest/data/works-list.local.json \ SCENARIO=smoke \ k6 run scripts/loadtest/k6-works-list.js ``` ## 验证标准 ### 数据源验证 - `works-list.local.json` 中只出现作品 profile 表。 - 不出现以下字段或内容: - `password_hash` - `refresh_token_hash` - `phone_number_e164` - `phone_number_masked` - `wallet_ledger_id` - `auth_identity` - `user_account` - `puzzle_work_profile` 行数应接近原始文件中的 80 行。 - `custom_world_profile` 行数应接近原始文件中的 1 行。 ### K6 smoke 验证 - 所有目标接口返回 2xx。 - `http_req_failed < 1%`。 - 响应 JSON shape 与 shared contracts 对齐:`items` 或 `works` 数组。 - K6 输出中能区分不同 endpoint 的耗时。 ### 性能阈值初稿 - Smoke:`p95 < 800ms`,失败率 `< 1%` - Baseline:`p95 < 1000ms`,`p99 < 2000ms`,失败率 `< 1%` - Spike:允许短暂 p95 抖动,但 1 分钟内应恢复;失败率 `< 5%` 阈值后续需要结合本地机器性能、SpacetimeDB 本地模式和正式部署规格调整。 ## 风险与注意事项 1. **原始迁移文件包含敏感数据。** 必须只提取作品列表白名单字段,禁止把原始 JSON 全量提交到仓库。 2. **base64 封面可能导致压测数据膨胀。** 默认截断或替换为占位符,除非本次明确要测封面 payload 对响应体积的影响。 3. **本地 SpacetimeDB 与 api-server 端口会漂移。** 不要硬编码端口,运行时通过 `BASE_URL` 注入。 4. **列表接口可能需要鉴权。** 若实际接口要求登录,不要导入真实 refresh session;应使用本地测试账号或专门的压测 token 生成流程。 5. **作品表名/接口路径可能与候选名称不完全一致。** 实施前必须以源码路由为准。 6. **本计划仅保存压测方案,不执行实际压测。** 后续执行时再创建/修改脚本、导出过滤数据、跑 K6 并记录结果。 ## 开放问题 1. 压测目标是本地 dev 栈、测试环境,还是预发/生产只读接口?不同环境阈值和安全边界不同。 2. “作品列表”是否只包含拼图和自定义世界,还是要覆盖 match3d、square-hole、big-fish、visual-novel 的统一列表入口? 3. 是否允许使用专门压测账号/token?如果接口无鉴权则无需处理。 4. 是否需要测封面/asset 加载,还是只测作品列表 JSON API?