6.9 KiB
远端作品列表压测排查报告
时间: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 spike:
http_req_failed/works_list_shape_error_rate约 21.99%。 - 100 RPS spike:
http_req_failed/works_list_shape_error_rate约 25.47%。 - 从 k6 check 看,失败主要集中在
puzzle_gallery_list,custom_world_gallery_list基本正常。
已完成排查
1. 服务器进程与资源
远端服务监听:
- Rust api-server:
127.0.0.1:8082,systemd 服务genarrative-api.service。 - SpacetimeDB:
127.0.0.1:3101,systemd 服务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:5432PostgreSQL 被拒绝。 - PM2 restart 次数已超过 33 万。
- 该进程不是当前公网
/api/*使用的 Rust api-server,但会制造额外 CPU/内存/日志抖动。
2. 压测窗口服务端日志
子任务聚合了 2026-05-12 04:50-05:05 的 nginx 与 api-server 日志。
nginx access:
/api/runtime/puzzle/gallery:4661 次,全部 200。/api/runtime/custom-world-gallery:4659 次,全部 200。
api-server journal:
/api/runtime/puzzle/gallery:
- completed:4661
- status:200 全部
- slow_request:0
- latency_ms:min 13 / p50 30 / p90 43 / p95 50 / p99 62 / max 88
/api/runtime/custom-world-gallery:
- completed:4659
- status:200 全部
- slow_request:0
- latency_ms:min 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 顺序请求两个接口:
GET /api/runtime/puzzle/galleryGET /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:1192server-rs/crates/api-server/src/puzzle.rs:1385-1409server-rs/crates/spacetime-client/src/puzzle.rs:367-381server-rs/crates/spacetime-module/src/puzzle.rs:430-443server-rs/crates/spacetime-module/src/puzzle.rs:1393-1404
关键实现:
list_puzzle_gallery_tx对puzzle_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-248by_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 更容易受作品表规模和统计表规模影响。
当前判断
本次排查有两个层面的结论:
-
生产服务端日志没有证明
puzzle/gallery在 04:50-05:05 窗口真的 30s 慢或 5xx。- api-server 记录的 p95 只有 50ms。
- nginx 看到两个接口都是 200。
- 所以 k6 侧的 30s timeout 需要进一步从客户端网络、连接池、Windows/k6 执行环境、summary 混合统计角度验证。
-
代码层确实存在可修的性能隐患。
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. 修复代码性能隐患
优先级建议:
count_recent_public_work_plays改为使用by_public_work_play_daily_stat_work_day复合索引,或至少改成批量统计,避免 N 次全表扫描。list_puzzle_gallery_tx使用by_puzzle_work_publication_status索引查询 Published,或参考 custom-world 建立puzzle_gallery_entry公开读模型。- gallery 列表页不要实时逐条扫描统计表,可维护读模型或批量聚合
recent_play_count_7d。
D. 调整 k6 脚本输出
建议 k6 summary 按 endpoint tag 输出或新增单接口模式,否则 overall 指标会把 puzzle/custom-world 混在一起。
建议增加:
ENDPOINT=puzzle_gallery_listENDPOINT=custom_world_gallery_list
让脚本只跑一个 endpoint,避免诊断时混淆。