Files
Genarrative/.hermes/plans/2026-05-12_0616-remote-works-list-loadtest-diagnosis.md
kdletters cf074837a4
Some checks failed
CI / verify (push) Has been cancelled
docs: ignore local load test artifacts
2026-05-12 15:14:11 +08:00

6.9 KiB
Raw Permalink Blame History

远端作品列表压测排查报告

时间2026-05-12 06:16 CST 目标:http://82.157.175.59 SSH远端生产机 root 账号(具体私钥路径仅保留在本机环境,不写入仓库)

背景

远端 k6-works-list.js 压测中:

  • smoke 通过。
  • baseline 10 VU无 HTTP 错误,但 p95/p99 超阈值。
  • 50 RPS spikehttp_req_failed / works_list_shape_error_rate 约 21.99%。
  • 100 RPS spikehttp_req_failed / works_list_shape_error_rate 约 25.47%。
  • 从 k6 check 看,失败主要集中在 puzzle_gallery_listcustom_world_gallery_list 基本正常。

已完成排查

1. 服务器进程与资源

远端服务监听:

  • Rust api-server127.0.0.1:8082systemd 服务 genarrative-api.service
  • SpacetimeDB127.0.0.1:3101systemd 服务 spacetimedb.service
  • Nginx公网 80 反代 /api/*127.0.0.1:8082

服务器规格/状态:

  • 2 vCPU。
  • 内存约 1.9GiB。
  • Swap 约 1.9GiB,已有约 600MiB 使用。
  • / 磁盘约 69%。
  • Rust api-server 当前 CPU 不高。
  • SpacetimeDB 当前 CPU 不高。

发现一个独立异常:

  • PM2 下旧 server-node 进程 genarrative 正在重启风暴。
  • cwd/work/Genarrative/server-node
  • 错误:连接 127.0.0.1:5432 PostgreSQL 被拒绝。
  • PM2 restart 次数已超过 33 万。
  • 该进程不是当前公网 /api/* 使用的 Rust api-server但会制造额外 CPU/内存/日志抖动。

2. 压测窗口服务端日志

子任务聚合了 2026-05-12 04:50-05:05 的 nginx 与 api-server 日志。

nginx access

  • /api/runtime/puzzle/gallery4661 次,全部 200。
  • /api/runtime/custom-world-gallery4659 次,全部 200。

api-server journal

/api/runtime/puzzle/gallery

  • completed4661
  • status200 全部
  • slow_request0
  • latency_msmin 13 / p50 30 / p90 43 / p95 50 / p99 62 / max 88

/api/runtime/custom-world-gallery

  • completed4659
  • status200 全部
  • slow_request0
  • latency_msmin 0 / p50 1 / p90 5 / p95 7 / p99 13 / max 49

结论:

  • 在服务端视角,两个接口在该窗口都没有 5xx也没有慢请求。
  • 这与 k6 客户端侧 30s timeout / failed check 存在明显不一致。
  • 需要进一步区分:客户端侧网络/连接耗尽/本机 k6 执行环境问题,还是 k6 统计混合/响应解析问题。

3. k6 脚本行为

文件:scripts/loadtest/k6-works-list.js

AUTH_TOKEN 时,每轮 iteration 顺序请求两个接口:

  1. GET /api/runtime/puzzle/gallery
  2. GET /api/runtime/custom-world-gallery

DETAIL_RATIO=0 时不会请求详情。

works_list_shape_error_rate 不只代表字段结构错误,只要下面任意 check 失败都会计入:

  • status is 200
  • returns json object
  • has collection
  • list item shape

因此 timeout、非 JSON、非 200、响应结构不符合都会表现为 shape error。

数据文件实际路径:

  • scripts/loadtest/data/works-list.local.json

脚本里 data/works-list.local.json 是相对 k6 脚本文件解析的,因此本身合理。

4. 代码层疑似瓶颈

虽然这次远端服务端日志没有复现慢请求,但代码层仍发现一个真实性能隐患。

/api/runtime/puzzle/gallery 调用链:

  • server-rs/crates/api-server/src/app.rs:1192
  • server-rs/crates/api-server/src/puzzle.rs:1385-1409
  • server-rs/crates/spacetime-client/src/puzzle.rs:367-381
  • server-rs/crates/spacetime-module/src/puzzle.rs:430-443
  • server-rs/crates/spacetime-module/src/puzzle.rs:1393-1404

关键实现:

  • list_puzzle_gallery_txpuzzle_work_profile().iter() 全表扫描。
  • 再过滤 publication_status == Published
  • 对每个公开作品调用 build_puzzle_work_profile_from_row_with_recent_count
  • 该函数调用 count_recent_public_work_plays(ctx, "puzzle", &row.profile_id, now_micros)

count_recent_public_work_plays

  • 文件:server-rs/crates/spacetime-module/src/runtime/profile.rs:1296-1321
  • 当前实现对 public_work_play_daily_stat().iter() 全表扫描过滤。
  • 但表定义已有复合索引:
    • server-rs/crates/spacetime-module/src/runtime/profile.rs:242-248
    • by_public_work_play_daily_stat_work_day(source_type, profile_id, played_day)
  • 当前统计函数未使用该索引。

复杂度风险:

puzzle gallery ~= O(puzzle_work_profile 全表扫描 + Published作品数 * public_work_play_daily_stat 全表扫描)

custom-world-gallery 与 puzzle 的差异:

  • custom-world 使用 CustomWorldGalleryEntry 公开读模型表。
  • puzzle 直接从 puzzle_work_profile 即席拼装。
  • 两者都调用 recent count但 puzzle 更容易受作品表规模和统计表规模影响。

当前判断

本次排查有两个层面的结论:

  1. 生产服务端日志没有证明 puzzle/gallery 在 04:50-05:05 窗口真的 30s 慢或 5xx。

    • api-server 记录的 p95 只有 50ms。
    • nginx 看到两个接口都是 200。
    • 所以 k6 侧的 30s timeout 需要进一步从客户端网络、连接池、Windows/k6 执行环境、summary 混合统计角度验证。
  2. 代码层确实存在可修的性能隐患。

    • count_recent_public_work_plays 未使用已有索引。
    • puzzle gallery 对每个作品重复做 recent count。
    • puzzle gallery 未使用 publication_status 索引或读模型。

建议下一步

A. 先处理服务器 PM2 重启风暴

建议确认旧 Node 服务是否仍需要。

如果不需要,应停止并禁用 PM2 中的旧 server-node

PM2_HOME=/home/ubuntu/.pm2 pm2 stop genarrative
PM2_HOME=/home/ubuntu/.pm2 pm2 delete genarrative
PM2_HOME=/home/ubuntu/.pm2 pm2 save

这是生产侧操作,执行前需要确认。

B. 单接口短压验证客户端/服务端不一致

不要继续用混合脚本大压。

建议新增或临时使用单接口 k6 脚本,分别只测:

  • /api/runtime/puzzle/gallery
  • /api/runtime/custom-world-gallery

并在同一时间窗口并行采集:

  • k6 客户端 summary
  • nginx access 请求数/状态码
  • api-server journal latency
  • 本机到服务器网络错误/timeout

目标是确认 timeout 是不是发生在客户端侧连接/网络,而不是服务端处理慢。

C. 修复代码性能隐患

优先级建议:

  1. count_recent_public_work_plays 改为使用 by_public_work_play_daily_stat_work_day 复合索引,或至少改成批量统计,避免 N 次全表扫描。
  2. list_puzzle_gallery_tx 使用 by_puzzle_work_publication_status 索引查询 Published或参考 custom-world 建立 puzzle_gallery_entry 公开读模型。
  3. gallery 列表页不要实时逐条扫描统计表,可维护读模型或批量聚合 recent_play_count_7d

D. 调整 k6 脚本输出

建议 k6 summary 按 endpoint tag 输出或新增单接口模式,否则 overall 指标会把 puzzle/custom-world 混在一起。

建议增加:

  • ENDPOINT=puzzle_gallery_list
  • ENDPOINT=custom_world_gallery_list

让脚本只跑一个 endpoint避免诊断时混淆。