先看条件
脱离测量条件的延迟数字没有意义。以下是一次完整压测的全部前置条件;后面每一张表都建立在这些条件之上。 性能实测时间为 2026-09-25。
浏览器之外的真实 HTTPS API 请求,经 Nginx 网关并验证测试 CA;计时含 HTTPS + 读取 JSON + 语义校验。 也就是说,这里的每一个毫秒都包含 TLS、网关转发与响应体解析,不是只统计服务端函数耗时。
| 项 | 值 |
|---|---|
| 数据规模 | 合成租户 perf-capacity-v2:50,000 产品 / 50,000 报价 / 1,005,950 明细行;
另加项目现有 CMM Excel 数据与规则、20 个合成产品方案、50,000 条区域价目 |
| 行规模分布 | 20 行占 49,985 份;50 / 200 / 1,000 行各 5 份 |
| 宿主机 | MacBookPro16,2,Intel x86_64,8 逻辑 CPU,16 GiB,macOS 14.8.3 |
| 运行时 | Docker Linux,Linux 6.12.5-linuxkit,Python 3.12.14、PostgreSQL 17.11、Nginx 1.30.5;Docker 分配约 8 GiB |
| 进程配置 | 2 个 API 容器 × 2 Uvicorn 进程;业务池 8 / 身份池 4;单进程请求并发上限 32;1 Worker + 1 Scheduler(4/2 池) |
| 用户 | 300 个独立账号(1 管理员 + 299 销售),独立连接池,已登录会话 |
| 负载曲线 | 前 1,800 秒平均思考 10 秒,后 1,800 秒 5 秒 |
| 测量口径 | 浏览器之外的真实 HTTPS API 请求,经 Nginx 网关并验证测试 CA;计时含 HTTPS + 读取 JSON + 语义校验 |
一小时持续压测
3,600.24 秒连续运行,300 个活跃用户执行列表、详情、初始化、首页统计、产品分页、选配、保存与重算, 全程共发出 170,327 次真实 HTTPS 请求。
| 阶段 | 请求数 | 错误 | P50 | P95 | P99 |
|---|---|---|---|---|---|
| 前 30 分钟(思考 10 s) | 57,594 | 0 | 36.12 ms | 166.82 ms | 505.49 ms |
| 后 30 分钟(思考 5 s) | 112,733 | 4 | 43.71 ms | 274.23 ms | 1,540.02 ms |
300 是带思考时间的活跃用户数,不等于每秒 300 请求或 300 并发。 实际稳态吞吐是平均 47.31 req/s。把活跃用户数当作并发数读取,会高估本系统近一个数量级。
4 次 503 集中在第 37 分钟(初始化 1、详情 2、列表 1),均返回明确"服务繁忙" + Retry-After;
之后 23 分钟无新增错误。过载保护按预期动作并给出可重试信号,而不是让请求挂死或静默失败。
分操作延迟
同一小时内的请求按操作拆分。"服务端 P95"只计应用内部耗时,与端到端 P95 的差值即为网关、TLS 与网络序列化开销。
| 操作 | 请求数 | P50 | P95 | P99 | 服务端 P95 |
|---|---|---|---|---|---|
| 报价列表 | 55,637 | 36.17 ms | 189.54 ms | 1,138.87 ms | 159.98 ms |
| 报价详情 | 43,166 | 31.24 ms | 176.85 ms | 892.87 ms | 142.50 ms |
| 初始化 bootstrap | 15,872 | 69.36 ms | 335.78 ms | 2,125.88 ms | 300.84 ms |
| 首页统计 | 15,781 | 38.24 ms | 199.70 ms | 1,138.75 ms | 169.86 ms |
| 产品分页 | 16,010 | 31.06 ms | 166.96 ms | 938.51 ms | 133.90 ms |
| 选配联动 最重的操作 | 12,619 | 96.57 ms | 388.01 ms | 1,629.78 ms | 355.27 ms |
| 保存报价 | 8,040 | 72.53 ms | 443.52 ms | 1,706.18 ms | 396.15 ms |
| 报价重算 | 3,202 | 28.27 ms | 155.30 ms | 713.85 ms | 115.07 ms |
大报价:按明细行拆分
真正拉开差距的不是报价"张数",而是单张报价的明细行数。下表把保存与重算按 20 / 50 / 200 / 1,000 行分别统计。
| 明细行 | 保存 P95 / P99 | 重算 P95 / P99 |
|---|---|---|
| 20 | 325.76 / 1,415.04 ms | 130.85 / 713.85 ms |
| 50 | 354.69 / 1,187.00 ms | 70.06 / 153.98 ms |
| 200 | 799.09 / 3,232.91 ms | 147.10 / 306.39 ms |
| 1,000 | 1,758.08 / 6,975.73 ms | 306.76 / 391.17 ms |
22 / 22 passed 验收阈值
docs/performance/acceptance-summary.json 中 22 项阈值全部 passed。
阈值是事前设定的判定线,不是事后挑出来的好看数字。
阈值口径
- 选配:P95 ≤1,000 ms、P99 ≤3,000 ms
- 各规模保存:P95 ≤2,000 ms
- 重算:20 / 50 / 200 行 P95 ≤3,000 ms,1,000 行 P95 ≤10,000 ms
- PDF:20 / 50 / 200 行 P95 ≤10 s,1,000 行 P95 ≤60 s
- 四类搜索:P95 ≤1,000 ms
文档生成
以下 PDF 生成与 300 用户在线负载同时进行,计时含入队 → 排队 → 轮询 → 下载的完整端到端耗时, 而不是只统计渲染函数本身。
| 报价行 | PDF 页数 | 样本 | P50 | P95 | 最大端到端 |
|---|---|---|---|---|---|
| 20 | 2 | 20 | 3.133 s | 5.899 s | 6.840 s |
| 50 | 3 | 10 | 3.796 s | 4.320 s | 4.954 s |
| 200 | 10 | 10 | 4.034 s | 8.923 s | 10.817 s |
| 1,000 | 44 | 5 | 5.910 s | 11.668 s | 13.009 s |
超大 XLSX
50,000 行 XLSX(含入队 / 等待 / 下载)共 27.191 秒,909,204 字节。
高峰段追加样本
高峰段追加 80 次 20 行 PDF:P50 3.502 s / P95 4.251 s / 最大 4.482 s。
样本处理
两段合计 125 次 PDF,所有样本保留,不挑选较快样本,也不剔除离群值。
本轮为单个 Worker 顺序生成,未测 300 人同时批量生成 PDF。多人同时触发大批量文档时, 排队时间会随队列长度线性累加,不能用上表的端到端数字外推。
全量关键词搜索
在高峰期间以管理员身份执行全目录 + 全报价范围搜索,每类 50 个不同关键词,四类合计 200 次请求。 这是最坏的搜索范围:不做租户或权限裁剪,直接扫全量数据。
| 搜索 | P50 | P95 | P99 | 最大值 |
|---|---|---|---|---|
| 全目录 SKU | 208.62 ms | 877.06 ms | 1,500.85 ms | 1,968.90 ms |
| 全目录产品名称 | 186.75 ms | 814.20 ms | 1,836.36 ms | 1,915.15 ms |
| 全报价编号 | 249.74 ms | 796.74 ms | 1,955.89 ms | 2,800.47 ms |
| 全报价客户名称 | 233.88 ms | 911.44 ms | 1,242.72 ms | 1,314.94 ms |
资源与稳定性
基于 159 个有效样本的宿主机与容器资源采样,以及压测全程的数据库连接与事务观测。
宿主 CPU
平均 50.42%,最高采样 86.3%。宿主为 8 逻辑 CPU,全程未出现持续打满或请求堆积。
宿主内存
最低可用内存 5,525 MiB。宿主总内存 16 GiB,其中 Docker 分配约 8 GiB。
数据库侧
应用连接最多观测 52 个;最老事务 0.876 秒;死锁 0;无容器重启或 OOM。
容器内存
| 容器 | 最高采样内存 | 最后采样内存 |
|---|---|---|
| API 1(两进程) | 674.10 MiB | 658.00 MiB |
| API 2(两进程) | 669.20 MiB | 644.70 MiB |
| Worker | 118.80 MiB | 30.19 MiB |
| 调度器 | 39.16 MiB | 36.43 MiB |
| 网关 | 15.83 MiB | 14.33 MiB |
| PostgreSQL | 621.40 MiB | 603.40 MiB |
API 内存从初期 ~300 MiB 增至末期 ~650 MiB, 一小时结果不能证明长期无内存增长。若要支撑长时间连续运行,需要做更长时间的驻留观测与泄漏排查, 本轮数据不足以支撑"长期稳定"的结论。
其他可用性证据
除了延迟与错误率,我们还观测了报表、冷启动、容器替换、数据一致性与大目录打开这几类常见"上线后才暴露"的问题。
报表
50,000 行 CSV 入队后 6.658 秒完成;汇总 10.310 秒,金额与数据库一致。
冷启动
在线负载期间新建双进程 API 容器,首个进程就绪 6.871 s,双进程全就绪 7.039 s。
容器替换
API 1 移除重建、IP 变化后,网关容器不变、登录会话持续有效,新实例 0.876 秒后被观测到已处理请求。
数据一致性
压测后核对 50,000 报价 + 50,000 摘要索引 + 1,005,950 明细;300 份活跃报价版本 / 金额及 10,760 条明细索引与快照一致。
大目录专项
2026-09-27,520 产品 / 501 行报价用例:目录发布 168 ms、报价创建 286 ms、页面打开 523 ms、CSV 导出 79 ms; 金额未税 ¥50,100 / 税额 ¥6,513 / 含税 ¥56,613。
隔离环境单次观测 不能据此推断生产 P95 或并发上限。
我们做了哪些优化
下面的每一条都有前后对照,但我们不作倍数承诺:这些是在特定数据量、特定硬件与特定规则集下消除的瓶颈, 换一个现场需要重新测量,而不是直接套用。
权限查询
最初的权限查询导致约 12.7% 请求失败、P95 约 12.4 秒。改为索引查询 + 只读价格缓存后消除主要开销。
SQL 与事务次数
六个代表性列表 / 首页 / 详情操作的仓储 SQL 52 → 36 次、事务 26 → 16 次。
连接池与过载保护
原 4/2 池 + 16 处理名额时 3.22% 请求被过载保护拒绝;调到 8/4 与 32 后,三分钟高峰复测 11,387 次请求零错误。
PDF 子进程
20 行 PDF 曾 P95 10.301 秒(超标)。定位为任务子进程重复编译 Python 源码,改为镜像构建期 compileall 后达标。
测试与质量保障
2026-09-27 全量回归结果。测试在系统临时目录建库,读取 data/source/ 原始客户样本,不改写用户运行数据。
| 范围 | 结果 | 耗时 |
|---|---|---|
| 后端业务与接口 | 895 / 895 通过 | 271.499 s |
| 前端逻辑 | 180 / 180 通过 | 1,649.9 ms |
| 浏览器回归(真实接口,非 mock) | 35 / 35 通过 | 2.8 分钟 |
| 构建与格式 | Prettier + Vite 构建通过;8765 / 8766 只读健康检查 HTTP 200 | — |
后端行覆盖率 94%
docs/full-coverage.txt:2,203 stmts / 130 miss。
app.py 90%、catalog.py 96%、engine.py 98%、
documents.py 99%、config_tables.py 100%、security.py 93%。
SaaS 新增 18 模块
行覆盖 89%,分支覆盖 83%(1,906 stmts / 210 miss)。
CLI / MCP 新增包
覆盖率 90%。
测试资产
tests/*.py 109 个 + tests/*.test.js 45 个 + Playwright spec 19 个。
覆盖的 correctness 场景
288 个组合逐项匹配、1/2/4 倍 BOM、Decimal 尾差分摊、不可折扣项、导入原子性。
覆盖的流程与输出场景
审批、版本冲突、订单锁定、文档格式。
诚实声明
性能页最容易越界的地方,是把一次本机测量说成一份对外承诺。以下是本页数字不能被解读成的东西。
上述全部结果来自本机 Docker 单机上的合成负载实测,用于说明系统在给定条件下的行为与容量量级, 不是生产服务等级承诺。P95 231.84 ms 是本次测量条件下的观测值,不能作为对外 SLA 引用。
未测:中国办公网络、同时登录突发、SSO、跨地域数据库、真实外围系统耗时、300 人同时批量生成 PDF。
部署形态 单主机参考部署
- Compose 是单主机参考部署,不宣称主机级高可用
- 租户隔离由应用层实施,未使用数据库 RLS
- 未实现自助开通与 SaaS 计费
- RPO 24 小时 / RTO 4 小时仍是部署演练目标,不是已达成的 SLA
容量边界 报告导出
- 报告导出的容量边界:500,000 / 200,000 / 5,000 行
- 该边界为既定容量口径,本轮实测未覆盖该量级(本轮最大实测为 50,000 行 XLSX)
- 超出边界的导出需按实际数据量与并发数另行验证,不能由本页数字外推