性能与容量评估
本文档基于实际配置和代码,评估平台在 2C4G 应用服务器 + 2C2G MySQL(阿里云 RDS) 下的承载能力,并给出压测方法与内部部署的推算。
以下数字是基于配置和代码的工程推算,不是实测。每个环境的真实瓶颈取决于业务复杂度、SQL 质量、数据量、并发模式,强烈建议用本文「压测方法」一节的方法实测。推算的逻辑链全部列出,便于你按自己的场景修正。
部署拓扑(单机跑全套)
生产现状是单台 2C4G 应用服务器同时跑全部前端服务 + 后端 + Redis,数据库用独立的 2C2G RDS:
┌─────────────────────────────────────────────────────────┐
│ 2C4G 应用服务器 │
│ │
│ nginx ──┬── admin 域名 → /opt/qzt-admin/(静态) │
│ ├── m 域名 → /opt/qzt-mobile/(静态) │
│ └── 主域名 → 127.0.0.1:3000(CMS SSR) │
│ pm2 qzt-cms (Next.js SSR) ── 127.0.0.1:3000 │
│ qzt-server (Go API) ── 127.0.0.1:9000 │
│ redis-server ── 127.0.0.1:6379 │
└─────────────────────────────────────────────────────────┘
│
▼ TCP 3306
┌─────────────────────────────────────────────────────────┐
│ 2C2G 阿里云 RDS MySQL 8.0 │
└─────────────────────────────────────────────────────────┘
关键约束:4G 内存要同时跑 Go API + Redis + Next.js SSR + nginx + 系统,留给后端的内存预算非常紧张。这是单机拓扑最现实的上限因素。
内存预算分析(4G 怎么分)
单机跑全套时,内存是第一个被吃光的资源,远早于 CPU。
| 进程 | 预估常驻内存 | 说明 |
|---|---|---|
| 系统 + sshd + 监控 agent | ~300 MB | 不可压 |
| nginx | ~50-100 MB | 静态 serve + 反代 |
| Redis | ~150-300 MB | JWT 黑名单、权限缓存、限流计数 |
| qzt-cms(Next.js SSR) | ~600-1000 MB | Node 进程 + SSR 渲染,内存大头 |
| qzt-server(Go API) | ~200-400 MB | Go 运行时 + 连接池 + gin |
| Go 上传缓冲峰值 | +最高 20MB×N | 每个在途上传最多吃 20MB 内存 |
| 安全余量 | ~300 MB | 防 OOM 的 buffer |
| 合计 | ~1.8-2.4 GB | 峰值(高并发上传时上浮) |
4G 单机跑全套,常态占用 2-2.4G,峰值可能逼近 3G。Next.js CMS 是最大的内存消耗者,如果关掉 CMS(或改静态导出),后端能多出 ~800MB 预算,承载力显著提升。
CPU 预算:2 核被 nginx/Go/Node/Redis 共享,Go API 实际能稳定占用约 1.2-1.5 核。
后端单机承载推算
连接数瓶颈(硬约束)
| 资源 | 配置值 | 来源 |
|---|---|---|
MySQL max_open_conns(Go 侧) | 20 | config.yaml mysql.max_open_conns |
MySQL max_idle_conns | 4 | 同上 |
Redis pool_size | 10 | config.yaml redis.pool_size |
nginx upstream keepalive | 32 | deploy/nginx/qzt.conf |
关键瓶颈是 max_open_conns=20。绝大多数请求都要查库,20 个 DB 连接 = 同一时刻最多 20 个请求在执行 SQL,超出的请求排队等连接。
阿里云 RDS 2C2G 的默认 max_connections 约 200-400,Go 侧只占 20,RDS 侧不是瓶颈。
单请求耗时模型(典型 CRM 列表请求)
nginx 反代 ~1 ms
JWT 鉴权 + Redis 查黑名单 ~2-3 ms(Redis 往返)
Casbin 权限检查(Redis 缓存)~1-2 ms
Handler 参数绑定 ~1 ms
Service → Repository → GORM 查询 ~10-40 ms(取决于数据量和索引)
JSON 序列化 + 响应 ~2-5 ms
────────────────────────────────
合计 ~20-50 ms / 请求
取 30ms 作为典型值(带索引、数据量中等的乐观估计)。无索引或全表扫描的慢查询会到 200ms+,把吞吐拉到零头。
吞吐量(QPS)推算
综合连接池约束(理论上限 ~660 QPS)和 2 核 CPU 约束:
| 负载类型 | 估算 QPS | 说明 |
|---|---|---|
| 纯读轻接口(字典、配置) | 500-800 | Redis 缓存命中,几乎不碰 DB |
| 典型业务读(列表/详情) | 150-250 | DB 连接池是瓶颈 |
| 写接口(创建/更新) | 80-150 | 多一次事务 + 审计日志 |
| 复杂导出(Excel 全量) | 5-15 | 单请求耗时长,占连接久 |
| 文件上传(20MB) | 并发受内存限制 | 每个在途上传吃 20MB,并发上限约 10-15 个 |
稳妥可持续 QPS:~150-200(混合读多写少的日常办公负载)。
在线用户 → 并发请求换算
「在线用户」不等于「并发请求」。办公系统的典型行为:一个用户思考、看屏幕的时间占 95%+,假设平均每分钟发 2-4 个请求,100 个在线用户 ≈ 3-7 QPS。
| 指标 | 公式 | 结果 |
|---|---|---|
| 可支撑持续在线用户 | QPS 上限 / 人均 QPS | 200 / 0.05 ≈ 200-400 人在线 |
| 可支撑注册用户(按 10% 同时在线) | 在线数 / 0.1 | 2000-4000 注册用户 |
| 峰值(短时突发,如早高峰打卡) | 连接池排队容忍 | 短时 2-3 倍,即 500-800 并发请求 可排队消化 |
单机承载结论
| 场景 | 可承载用户数 | 触发瓶颈 |
|---|---|---|
| 日常办公负载(CRM/审批/进销存日常操作) | 100-200 活跃在线 / 1000-2000 注册 | 内存(CMS)+ DB 连接池 |
| 中等负载(关掉 CMS 或 CMS 静态化) | 200-400 活跃在线 / 2000-4000 注册 | DB 连接池 + CPU |
| 峰值突发(早高峰、批量导入) | 短时可承受 2-3 倍,响应延迟上升 | 排队,不崩 |
单机 2C4G 跑全套 + 2C2G RDS,建议承载 ≤ 150 活跃在线用户 / ≤ 1500 注册用户。超过这个规模建议参考下文扩容路径。
压测能承载多少用户(直接回答)
| 压测类型 | 可承载并发 | 对应在线用户 | 说明 |
|---|---|---|---|
| 接口压测(单接口纯打) | 200-500 并发请求 | — | 取决于接口类型,字典类 500+,列表类 200,写类 100 |
| 混合场景压测(模拟真实操作) | 30-80 并发虚拟用户 | 150-400 在线用户 | 每个虚拟用户带思考时间(10-30s),更贴近真实 |
| 登录峰值(早高峰集中登录) | 50-80 并发登录 | — | 受登录限流 + Redis 写黑名单影响 |
压测工具的 50 个并发虚拟用户(VU)持续发请求,约等于真实场景 200-400 人在线(因为真人 95% 时间在思考)。
压测方法(怎么测出真实数字)
工具
- wrk(推荐,单接口压测):
brew install wrk - k6(混合场景,JavaScript 脚本):
brew install k6 - vegeta(恒定速率):
brew install vegeta
单接口压测示例(wrk)
# 先登录拿 token
TOKEN=$(curl -s http://localhost:9000/system/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}' | jq -r '.data.access_token')
# 压客户列表(典型读接口)
wrk -t2 -c50 -d60s -H "Authorization: Bearer $TOKEN" \
"http://localhost:9000/crm/customers?page=1&page_size=20"
# -t2 线程数(等于 CPU 核数)-c50 并发连接 -d60s 持续 60 秒
关注三个指标:Requests/sec(QPS)、Latency P99(99 分位延迟)、错误率。P99 < 500ms 且错误率 < 0.1% 是健康线。
混合场景压测示例(k6)
import http from 'k6/http';
import { check, sleep } from 'k6';
const TOKEN = __ENV.TOKEN;
export const options = {
stages: [
{ duration: '2m', target: 30 }, // 2 分钟爬升到 30 VU
{ duration: '5m', target: 30 }, // 稳定 5 分钟
{ duration: '2m', target: 50 }, // 加压到 50 VU
{ duration: '5m', target: 50 }, // 稳定 5 分钟
],
thresholds: { http_req_duration: ['p(99)<1000'], http_req_failed: ['<0.01'] },
};
export default function () {
const headers = { Authorization: `Bearer ${TOKEN}` };
// 模拟真实操作流:看列表 → 翻页 → 看详情 → 休息
http.get('http://localhost:9000/crm/customers?page=1&page_size=20', { headers });
sleep(2);
http.get('http://localhost:9000/crm/opportunities?stage=PROSPECTING', { headers });
sleep(3);
http.get('http://localhost:9000/dashboard/overview', { headers });
sleep(5); // 思考时间
}
跑法:TOKEN=xxx k6 run script.js。逐步加压,观察在哪一档 P99 开始 > 1s 或出现错误,那一档就是真实承载线。
压测时的监控(必须同时看)
# 应用服务器
ssh root@server "vmstat 1" # CPU + 内存(r 列 > 2 表示 CPU 排队)
ssh root@server "free -h" # 内存(看 available)
ssh root@server "top -p $(pidof qzt-server)" # Go 进程 CPU/内存
# Go 内置 pprof(配置 enable_pprof: true)
go tool pprof http://localhost:9000/debug/pprof/heap # 内存火焰图
go tool pprof http://localhost:9000/debug/pprof/profile?seconds=30 # CPU 火焰图
MySQL 监控看 RDS 控制台:活跃连接数(是否打满 20)、慢查询日志、CPU 使用率。
判断瓶颈在哪:
| 现象 | 瓶颈定位 | 处理 |
|---|---|---|
| CPU 满、内存有富余 | CPU 瓶颈 | Go 逻辑或 GC 压力大 |
| DB 连接数打满 20、接口延迟陡增 | 连接池瓶颈 | 调大 max_open_conns |
| 内存 available < 500MB 且持续下降 | 内存瓶颈 | 优先砍 CMS 或升级内存 |
| RDS CPU > 80% | 数据库瓶颈 | 看慢查询、加索引 |
内部部署的承载差异
「内部部署」指企业内网/局域网,用户规模已知且可控,网络条件好。与公网生产相比,承载特点不同。
单机内部部署(同样 2C4G + 2C2G)
| 差异点 | 影响 |
|---|---|
| 内网带宽不是瓶颈 | 公网带宽可能是隐性瓶颈,内网千兆/万兆基本不限 |
| 无 CDN,但本地访问延迟低 | 图片走 OSS 公网反而慢,内网部署推荐 driver: local |
| 无公网 DDoS/爬虫压力 | 流量更「干净」,限流可以放宽 |
| 用户规模可控 | 一般企业内部 50-300 人,不会突然爆发 |
内部单机承载结论:同样配置下,承载 200-400 内部活跃用户比公网更稳。对于 50-200 人的中小企业,这套配置绰绰有余,甚至可以降到 2C2G 应用 + 2C2G DB。
内部部署优化建议(同样硬件跑更多人)
| 优化项 | 效果 | 代价 |
|---|---|---|
| 关掉 CMS(内网不需要企业官网) | 省下 ~800MB 内存,并发翻倍 | 无 |
调大 max_open_conns 到 50-80 | 并发上限提升 2-4 倍 | RDS 连接数压力,2C2G RDS 建议不超过 80 |
CMS 改静态导出(next export) | 省 SSR 内存 | 需重新构建发布流程 |
| 确保关键查询有索引 | 单请求 30ms → 5ms,QPS 翻几倍 | 需 SQL 优化 |
| 热数据加 Redis 缓存 | 减少压库 | 代码改造 |
| 文件用 local 不用 OSS | 省公网带宽、省 OSS 费用 | 需关注磁盘容量 |
内部部署优化后,单机 2C4G 可稳定支撑 400-800 活跃内部用户。
不同内部规模的配置建议
| 内部用户规模 | 推荐配置 | 说明 |
|---|---|---|
| ≤ 50 人 | 2C2G 应用 + 2C2G DB(本地 Redis) | 单机够用,关掉 CMS |
| 50-200 人 | 2C4G 应用 + 2C2G DB(本配置) | 舒适区,可开 CMS |
| 200-500 人 | 2C8G 应用 + 4C8G DB + 调大连接池 | DB 成为瓶颈,升 DB |
| 500-2000 人 | 4C8G×2 应用(LB)+ 8C16G DB + Redis 独立 | 需要负载均衡 |
| > 2000 人 | 拆分:API 集群 + DB 主从 + Redis 集群 + OSS | 超出单机架构范围 |
已知性能敏感点
基于代码审查,以下场景在压测时容易成为瓶颈:
| 点 | 风险 | 缓解 |
|---|---|---|
max_open_conns=20 | 首要瓶颈,高并发排队 | 内部部署调到 50-80 |
| 操作日志异步写库 | 每个写操作 +1 次 INSERT,高写入压 DB | 已异步;可考虑批量写 |
| Casbin 策略加载 | 角色变更触发全量 LoadPolicy,瞬时卡顿 | 已缓存到 Redis(10min TTL) |
| Excel 全量导出 | 单请求占连接久、内存上浮 | 限制单次导出行数(如 1 万行) |
| 文件上传内存缓冲 | 20MB×N 并发占内存 | 大文件改前端直传 STS |
| 列表数据权限过滤 | dept_id 递归 + 角色范围,SQL 变复杂 | 确保 dept_id/owner_id 有索引 |
扩容路径(超过单机上限怎么办)
按瓶颈类型选择:
- 关闭/静态化 CMS → 省 800MB
- 升级应用到 2C8G
- Redis 独立到专门实例
max_open_conns调到 50-80(配合 RDS 升配)- RDS 升到 4C8G
- 加读副本(
enable_read_write_separation: true,代码已支持)
- 应用水平扩展:2 台 2C4G + nginx upstream 加一台
- Redis 必须独立(多实例共享缓存)
- 静态资源全上 CDN
当前架构是「模块化单体」,天然支持从单机演进到集群——JWT 无状态、Redis 已外置、存储可切 OSS。扩容主要是加机器,不需要改代码。
结论速查表
| 配置 | 活跃在线用户 | 注册用户 | 持续 QPS | 峰值 QPS |
|---|---|---|---|---|
| 2C4G 单机跑全套 + 2C2G RDS(本配置,公网生产) | 100-200 | 1000-2000 | 150-200 | 400-600 |
| 2C4G 单机 + 2C2G RDS(内部部署,关 CMS) | 200-400 | 2000-4000 | 250-350 | 600-900 |
| 2C4G 单机 + 2C2G RDS(内部,关 CMS + 调连接池) | 400-800 | 4000-8000 | 350-500 | 900-1500 |
| 2C8G 应用 + 4C8G RDS | 500-1000 | 5000-10000 | 500-800 | 1500-2500 |
一句话结论:当前 2C4G 单机 + 2C2G RDS 的配置,公网生产环境保守承载 150 活跃在线用户 / 1500 注册用户;内部部署优化后可到 400-800 活跃用户,足够绝大多数中小企业使用。超过这个规模,优先升 DB 配置和调大连接池,其次加应用节点。