# Genarrative 作品列表 K6 压测 本目录用于对“作品列表/公开广场”读接口做本地压测。数据源来自私有 SpacetimeDB migration,但提取脚本只输出作品 profile 白名单表,并对用户、作者、作品号、asset id 等标识做稳定映射。 ## 文件 - `extract-works-list-data.mjs`:从 migration JSON 提取作品列表压测数据;本地输出也会脱敏路由 ID,因此默认用于列表接口压测,详情接口需先把同一份脱敏数据导入目标环境。 - `k6-works-list.js`:K6 压测脚本。 - `data/spacetime-migration-7.local.json`:本地私有原始数据副本,已被 `.gitignore` 忽略,不要提交。 - `data/works-list.local.json`:本地脱敏压测数据,已被 `.gitignore` 忽略,不要提交。 - `data/works-list.sample.json`:可提交的少量脱敏样例。 ## 数据边界 允许导入的表: - `puzzle_work_profile` - `custom_world_profile` - `match3d_work_profile` - `square_hole_work_profile` - `big_fish_work_profile` - `visual_novel_work_profile` 明确不导入: - 账号/认证:`user_account`、`auth_identity`、`refresh_session`、`auth_store_snapshot` - 钱包/邀请:`profile_wallet_ledger`、`profile_redeem_*`、`profile_invite_*` - 游玩历史/埋点/存档:`public_work_play_daily_stat`、`profile_played_world`、`puzzle_runtime_run`、`profile_save_archive`、`runtime_snapshot` - AI 任务过程:`ai_task`、`ai_task_stage`、`ai_text_chunk` - asset 二进制:`asset_object`、`asset_entity_binding` 提取脚本会移除 `source_session_id` / `source_agent_session_id` 等会话派生字段;这些字段不属于作品列表卡片压测必要字段。 ## 重新提取数据 从仓库根目录执行: ```bash npm run loadtest:extract-works -- \ --input scripts/loadtest/data/spacetime-migration-7.local.json \ --output scripts/loadtest/data/works-list.local.json \ --sample-output scripts/loadtest/data/works-list.sample.json ``` 也可以直接执行: ```bash node scripts/loadtest/extract-works-list-data.mjs \ --input scripts/loadtest/data/spacetime-migration-7.local.json \ --output scripts/loadtest/data/works-list.local.json \ --sample-output scripts/loadtest/data/works-list.sample.json ``` 当前 local 全量提取结果: - `puzzle_work_profile`: 80 - `custom_world_profile`: 1 - `match3d_work_profile`: 0 - `normalizedWorks`: 81 当前可提交 sample 结果: - `puzzle_work_profile`: 3 - `custom_world_profile`: 1 - `match3d_work_profile`: 0 - `normalizedWorks`: 4 ## 真实接口 已从 `server-rs/crates/api-server/src/app.rs` 确认的读接口: 公开接口,无需 Bearer token: - `GET /api/runtime/puzzle/gallery` - `GET /api/runtime/puzzle/gallery/{profile_id}` - `GET /api/runtime/custom-world-gallery` - `GET /api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}` - `GET /api/runtime/custom-world-gallery/by-code/{code}` 需要 Bearer token 的个人作品列表接口: - `GET /api/runtime/puzzle/works` - `GET /api/runtime/puzzle/works/{profile_id}` - `GET /api/runtime/custom-world/works` K6 脚本默认只跑公开列表接口;传入 `AUTH_TOKEN` 后会额外跑需要登录态的个人作品列表接口。当前真实列表 handler 未暴露分页/排序 query 参数,因此脚本不追加 `limit/offset`;若后续接口增加分页参数,再在 K6 中补随机分页。 详情接口默认不压测,因为本地数据中的 `profile_id` / `owner_user_id` 已脱敏,直接请求未导入脱敏数据的目标服务会 404。只有在目标环境已导入同一份脱敏数据,或改用真实 ID 本地文件时,才设置 `DETAIL_RATIO` 大于 0;详情请求不把 404 视为成功。 ## 启动服务 按项目约定启动本地 dev 栈: ```bash npm run dev ``` 注意端口可能漂移。以启动日志中的实际 api-server 端口为准,然后传给 K6。 注意:K6 的 `open()` 会按 `k6-works-list.js` 所在目录解析相对路径,因此 `WORKS_DATA` 应写成 `data/works-list.local.json`,不要写成 `scripts/loadtest/data/works-list.local.json`。 Bash / Git Bash: ```bash BASE_URL=http://127.0.0.1: WORKS_DATA=data/works-list.local.json npm run loadtest:k6:works -- --summary-trend-stats="avg,min,med,p(90),p(95),p(99),max" ``` PowerShell: ```powershell $env:BASE_URL="http://127.0.0.1:" $env:WORKS_DATA="data/works-list.local.json" npm run loadtest:k6:works -- --summary-trend-stats="avg,min,med,p(90),p(95),p(99),max" ``` ## 50 HTTP req/s 口径 `k6-works-list.js` 默认一次 iteration 会依次请求两个公开列表接口:`/api/runtime/puzzle/gallery` 和 `/api/runtime/custom-world-gallery`。因此目标约 50 HTTP req/s 时,`ramping-arrival-rate` 的 `PEAK_RPS` 应设置为 `25`。如果传入 `AUTH_TOKEN` 或把 `DETAIL_RATIO` 设为大于 0,每次 iteration 的请求数会增加,需要重新折算。 验收目标: - `http_req_failed < 1%` - `http_req_duration p95 < 2000ms` - `dropped_iterations = 0` - 压测窗口内 Nginx 无新增 502 ## Smoke ```bash BASE_URL=http://127.0.0.1:8787 \ WORKS_DATA=data/works-list.local.json \ SCENARIO=smoke \ DETAIL_RATIO=0 \ npm run loadtest:k6:works ``` 默认:1 VU / 30s。 ## Baseline ```bash BASE_URL=http://127.0.0.1:8787 \ WORKS_DATA=data/works-list.local.json \ SCENARIO=baseline \ VUS=10 \ DURATION=3m \ DETAIL_RATIO=0 \ npm run loadtest:k6:works ``` 默认阈值: - `http_req_failed < 1%` - `http_req_duration p95 < 800ms` - `http_req_duration p99 < 1500ms` - `works_list_shape_error_rate < 1%` ## Spike ```bash BASE_URL=http://127.0.0.1:8787 \ WORKS_DATA=data/works-list.local.json \ SCENARIO=spike \ START_RPS=5 \ PEAK_RPS=25 \ HOLD=60s \ DETAIL_RATIO=0 \ npm run loadtest:k6:works ``` 默认阈值: - `http_req_failed < 1%` - `http_req_duration p95 < 2000ms` - `dropped_iterations = 0` - `works_list_shape_error_rate < 1%` PowerShell: ```powershell $env:BASE_URL="https://genarrative.world" $env:WORKS_DATA="data/works-list.local.json" $env:SCENARIO="spike" $env:START_RPS="5" $env:PEAK_RPS="25" $env:HOLD="60s" $env:END_RPS="5" $env:DETAIL_RATIO="0" npm run loadtest:k6:works -- --summary-trend-stats="avg,min,med,p(90),p(95),p(99),max" ``` 线上 release 回归可使用同一组环境变量: ```bash SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works ``` ## 带登录态压测个人作品列表 先通过本地登录或接口获取 access token,然后传入: ```bash BASE_URL=http://127.0.0.1:8787 \ AUTH_TOKEN='' \ SCENARIO=smoke \ DETAIL_RATIO=0 \ npm run loadtest:k6:works ``` 不要把 token 写入仓库文件、README 或 shell history 中可共享的位置。 ## 详情接口压测 仅当目标环境存在 `WORKS_DATA` 中的同一批 `profileId/ownerUserId` 时启用: ```bash BASE_URL=http://127.0.0.1:8787 \ WORKS_DATA=data/works-list.local.json \ SCENARIO=smoke \ DETAIL_RATIO=0.35 \ npm run loadtest:k6:works ``` 如果详情请求返回 404,说明压测数据 ID 未导入目标环境或目标服务数据不一致,应先修正数据源,不要把 404 当成功。 ## 排障 - 如果公开 gallery 返回 `creation_entry_disabled` 或 503,检查本地 creation entry 配置是否禁用了对应入口。 - 如果高压下返回 429,优先确认目标环境是否设置了 `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS` 以及 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS`、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS`、`GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS`。429 表示 Nginx 或 api-server 背压已生效,不等同于业务错误;继续看内存、p95、`http_req_failed` 和 OTLP / Nginx timing 判断阈值是否偏低。 - 如果直连 `api-server` 压测出现 `connection refused` 或 status 0,说明压力已经打到 TCP 监听 / accept 层;此时同时检查 `GENARRATIVE_API_LISTEN_BACKLOG`、Nginx upstream keepalive 和是否需要在 Nginx 前置限流,不能只靠应用层背压解释。 - 如果个人作品列表返回 401,确认 `AUTH_TOKEN` 是当前 api-server 可识别的 access token。 - 如果详情全部 404,确认是否已向目标环境导入与 `WORKS_DATA` 一致的数据。 ## 压测窗口采集 Nginx upstream timing: ```bash sudo tail -f /var/log/nginx/genarrative.access.log sudo tail -f /var/log/nginx/genarrative.error.log ``` api-server 与 SpacetimeDB 日志: ```bash sudo journalctl -u genarrative-api.service -f sudo journalctl -u spacetimedb.service -f ``` api-server 的 OpenTelemetry 默认关闭。需要验证 OTLP traces / metrics / logs 时,先在服务器本机启动只监听 `127.0.0.1` 的 `otelcol-contrib` debug exporter: ```bash npm run otel:debug ``` 如果要把本机数据转发给 Rider OpenTelemetry 面板,先在 Rider 的 OpenTelemetry 设置中启用固定 OTLP server port,例如 `17011`,再运行: ```bash RIDER_OTLP_GRPC_ENDPOINT=127.0.0.1:17011 npm run otel:rider ``` 脚本会在 `.codex-temp/otelcol/` 生成临时 collector 配置,默认接收 api-server 发到 `http://127.0.0.1:4318` 的 OTLP HTTP 数据。需要改端口时可设置: - `OTELCOL_OTLP_HTTP_ENDPOINT`,默认 `127.0.0.1:4318` - `OTELCOL_OTLP_GRPC_ENDPOINT`,默认 `127.0.0.1:4317` - `RIDER_OTLP_GRPC_ENDPOINT`,默认 `127.0.0.1:17011` - `OTELCOL_BIN`,默认 `otelcol-contrib` 等价的 debug collector 配置如下: ```yaml receivers: otlp: protocols: grpc: endpoint: 127.0.0.1:4317 http: endpoint: 127.0.0.1:4318 exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] exporters: [debug] metrics: receivers: [otlp] exporters: [debug] logs: receivers: [otlp] exporters: [debug] ``` ```bash otelcol-contrib --config /etc/otelcol-contrib/genarrative-debug.yaml ``` 然后在 `/etc/genarrative/api-server.env` 中打开: ```env GENARRATIVE_OTEL_ENABLED=true OTEL_SERVICE_NAME=genarrative-api OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 ``` 注意 `api-server` 当前使用 OTLP HTTP exporter,`OTEL_EXPORTER_OTLP_ENDPOINT` 必须指向 Collector 的 HTTP base endpoint `http://127.0.0.1:4318`。不要把它改成 Collector gRPC 端口 `4317`,也不要直接指向 Rider 的 gRPC 端口;Rider 只由 `npm run otel:rider` 启动的 Collector 通过 `RIDER_OTLP_GRPC_ENDPOINT` 转发。 OTLP logs 是远端观测增量,不替代本地日志;api-server 日志仍看 `journalctl` / `logs/api-server/`,Nginx 日志仍看文件。日志等级继续用 `GENARRATIVE_API_LOG` / `RUST_LOG` 控制,例如 `info,tower_http=info,spacetime_client=info`。 Rider 的 Logs 面板展示的是 OTLP log event 自身字段,不会自动把父 span 的全部 attributes 摊平到每一条日志。请求完成日志会直接携带 `request_id`、`http.request.method`、`http.route`、`url.scheme`、`url.path`、`http.response.status_code`、`status_class`、`latency_ms` 和 `slow_request`;更完整的请求链路仍在 Traces 面板中按同一个 trace/span 关联查看。 压测期间可在 Metrics 面板或 debug exporter 中观察进程内存指标: - `process.memory.usage`:进程常驻内存 / RSS。 - `process.memory.virtual`:进程虚拟内存;Windows 当前按 `PrivateUsage` 上报,Linux 取 `VmSize`。 - `genarrative.process.memory.private`:进程私有内存,Windows 来自 `PrivateUsage`,Linux 近似取 `/proc/self/status` 的 `VmData`。 - `process.cpu.time`:进程 user + system 累计 CPU 秒数。 - `genarrative.process.cpu.usage_percent`:两次指标采集之间的进程 CPU 使用率;100% 约等于占满 1 个 CPU core。 - `process.thread.count`:线程数。 - `process.windows.handle.count`:Windows 句柄数。 - `process.unix.file_descriptor.count`:Linux 文件描述符数。 - `genarrative.http.server.response_bodies.in_flight`:Axum / Hyper 仍持有的响应 body 数;如果内存高但该值很低,说明热点不在业务 handler 生命周期内。 - `genarrative.http.server.request_permits.available`:应用层 HTTP 背压剩余 permit 数,带 `pool=default|gallery|detail|admin`;如果目标 pool 未接近 0,说明没有打满对应 `GENARRATIVE_API_*_MAX_CONCURRENT_REQUESTS`。 - `genarrative.puzzle_gallery.cache.hits` / `genarrative.puzzle_gallery.cache.stale_hits` / `genarrative.puzzle_gallery.cache.misses` / `genarrative.puzzle_gallery.cache.refreshes_started` / `genarrative.puzzle_gallery.cache.refreshes_failed` / `genarrative.puzzle_gallery.cache.rebuilds`:拼图广场响应缓存 fresh 命中、stale 命中、未命中、后台刷新和重建次数。 - `genarrative.puzzle_gallery.cache.rebuild.duration`:拼图广场缓存重建耗时。 - `genarrative.puzzle_gallery.cache.data_json_bytes`:拼图广场缓存内预序列化 data JSON 大小。 - `genarrative.spacetime.read.calls` / `genarrative.spacetime.read.duration_ms`:SpacetimeDB 订阅本地 cache 读次数和耗时;`read=list_puzzle_gallery` 表示当前路径走 view / local cache,不是 procedure。 若 `/api/runtime/puzzle/gallery` 单接口压测出现 GB 级瞬时内存峰值,先区分“持续泄漏”和“请求期分配峰值”:关闭 OTEL 后若峰值仍复现且压测结束后回落,主因通常不是 Collector / exporter。当前拼图广场列表命中缓存时应复用 `PuzzleGalleryCache` 中的预序列化 data JSON,只按请求拼接 envelope meta,不应每个请求重新深拷贝 `PuzzleGalleryResponse` 或构造完整 `serde_json::Value`。 本地 Windows 直连 `api-server` 压测还要单独看 K6 的 VU / 连接模型。已验证在 250 RPS、`PREALLOCATED_VUS=300` 时,哪怕打 `/healthz` 这种小响应,也可能因为本地 300 个 Established 连接触发 `api-server` private memory 瞬时升到约 7GB,压测结束后回落到 100MB 级;同样 250 RPS 改成 `PREALLOCATED_VUS=20 MAX_VUS=40` 后,拼图广场 p95 约 9ms,峰值降到约 600MB。这个现象说明高水位主要来自本机直连连接 / 发送链路,不等价于 SpacetimeDB 或拼图 JSON 缓存泄漏。做本地容量判断时优先让 VU 接近真实并发,避免用过高预分配 VU 把测试变成 Windows 本机连接缓冲压力测试;生产仍以 Nginx upstream keepalive、系统内存和 OTLP 指标一起判断。 线上回归辅助命令: ```bash systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax cat /proc/$(pidof api-server)/limits tr '\0' '\n' < /proc/$(pidof api-server)/environ | grep 'GENARRATIVE_API_.*MAX_CONCURRENT_REQUESTS' ss -ltnp | grep 8082 curl -sS http://127.0.0.1:8082/healthz ``` ## 验证命令 ```bash npx vitest run scripts/loadtest/extract-works-list-data.test.ts npx eslint scripts/loadtest/extract-works-list-data.mjs scripts/loadtest/extract-works-list-data.test.ts scripts/loadtest/k6-works-list.js ```