压测与容量评估:连接数、吞吐与延迟测量
长连接压测和 HTTP 压测最大的区别是:建连只是开始。真正要回答的是「维持 20 万连接之后还能推多少消息、延迟涨到多少」。因此压测必须分三层做,而且压测机自己先成为瓶颈的概率,远高于被测服务先到极限。
三层压测,不要混在一起
| 层次 | 目标 | 工具 | 关键指标 | 常见误判 |
|---|---|---|---|---|
| 建连(Handshake) | 每秒能升级多少连接、fd 与内存上限 | k6、自研 Go 压测器 | handshake/s、失败率、内存/连接 | 压测机端口耗尽被当成服务端拒绝 |
| 保活(Keepalive) | 维持 N 条连接的成本 | k6 ws 长跑、自研长连接压测器 | 内存/连接、心跳丢失率、GC 次数 | 只压 5 分钟,看不到累积上涨 |
| 收发(Throughput) | 消息吞吐与延迟分位 | k6 ws、服务端打点 | msg/s、P50/P99/P999、写队列水位 | 压测客户端 GC 停顿算进服务端延迟 |
wrk 只能压 HTTP 接入层(登录取 token、拉历史消息、健康检查),它不会发送 Upgrade: websocket,用它冒充 WebSocket 压测会得到一个毫无意义的数字。ghz 用于内部推送 RPC(业务到网关、网关之间的 gRPC 投递),关心的是吞吐与 P99。
先用 curl 确认握手本身是对的
curl --http1.1 -i -N -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
'http://127.0.0.1:8080/ws?token=bench'
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
拿到 101 才算握手通过。拿到 200 说明中间层把 Upgrade 吃掉了(按普通 HTTP 处理);400/426 多半是版本或 Sec-WebSocket-Key 不对;502 说明上游没在监听或已过载。压测前先跑通这一条,能省掉一半的排查时间。
k6 压 WebSocket
import ws from 'k6/ws';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 5000 }, // 台阶一:确认基线
{ duration: '5m', target: 50000 }, // 台阶二:稳态观察累积增长,试探上限
{ duration: '2m', target: 0 }, // 恢复段:看资源是否回落
],
thresholds: {
ws_connecting: ['p(95)<500'], // 握手 P95 小于 500ms
checks: ['rate>0.99'],
},
};
export default function () {
const res = ws.connect('ws://10.0.0.12:8080/ws?token=bench', {}, (socket) => {
socket.on('open', () => socket.send(JSON.stringify({ t: 'hello', ts: Date.now() })));
socket.on('message', (m) => socket.send(JSON.stringify({ t: 'ack', ts: Date.now() })));
socket.setTimeout(() => socket.close(), 60000); // 每条连接保活 60 秒
});
check(res, { 'status is 101': (r) => r && r.status === 101 });
}
k6 run --out json=result.json ws-bench.js
✓ status is 101
ws_connecting........: avg=41.2ms med=33.1ms max=842ms p(95)=118.4ms
ws_msgs_received.....: 4821093 24091.9/s
vus_max................: 50000
vus_max 到 50000 而 ws_connecting 的 P95 开始抬头,这个台阶就是拐点所在区间。
ghz 压内部推送 RPC
ghz --insecure -c 50 -n 200000 --proto ./im.proto \
--call im.PushService.Push -d '{"uid":"u-1001","msgId":"m-1"}' 10.0.0.12:9090
Summary: Count: 200000 Total: 41.28s Average: 10.29ms Requests/sec: 4844.55
Distribution: 10% in 3.21ms / 50% 8.42ms / 95% 24.10ms / 99% 41.77ms
平均值 10ms 但 P99 41ms,说明推送链路存在排队;此时应同时看网关的写队列长度与 go_goroutines,而不是继续加压。
压测期间必须同步观测服务端
ss -s ; ss -tan state established | wc -l # 连接总数与已建立连接数
sar -n DEV 1 10 ; sar -u 1 10 # 带宽与 CPU 是否打满
cat /proc/$(pgrep -f im-gateway)/status | grep -E 'Threads|VmRSS'
curl -s 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=1' | head -3
报告里必须把「连接数、msg/s、延迟 P99」画在同一时间轴上。只有同轴对比,才能看出拐点:连接数还在涨但吞吐不再涨、延迟开始陡增的那一刻,就是这套配置的容量上限。
容量估算:先算上限,再留余量
单机容量 = min(
内存 / 每连接内存(无 TLS 约 20~50KB,TLS 再加 30~80KB),
fd 上限(ulimit -n,需预留 20% 给日志、下游连接),
端口与 conntrack(经过 NAT 或作为客户端时才会成为瓶颈),
CPU:心跳与推送的每连接成本 × 连接数
)
示例:128GB 内存、每连接 40KB → 理论 320 万;ulimit -n = 1048576 → 约 100 万;
实测 20 万连接、CPU 70% 时延迟抬头 → 按 20 万/机 定容,预留 2 倍余量。
| 资源 | 参考量级 | 说明 |
|---|---|---|
| 内存 | 20 ~ 50KB / 连接 | 含两个协程栈、读写缓冲、映射条目 |
| 内存 | +30 ~ 80KB / 连接(TLS) | 用会话复用可降低握手期峰值 |
| CPU | 心跳 30s 一次,10 万连接约 3.3k 心跳/s | 推送成本远高于心跳,要分开压 |
| 文件句柄 | 1 连接 1 fd | 进程内还要为日志、DB、下游预留 |
| 带宽 | 消息尺寸 × 条数 × 2(上下行) | 小包多时协议头放大明显 |
| 连接建立速率 | 受 somaxconn、tcp_max_syn_backlog 限制 | 重连风暴时最先撞到这一项 |
常见坑
| 坑 | 现象 | 正确做法 |
|---|---|---|
| 压测机与被测服务同机 | 双方互相抢 CPU,数据无意义 | 压测机独立节点,且与被测在同一网段 |
| 压测机端口/fd 先耗尽 | dial: cannot assign requested address | 加源 IP、开 tcp_tw_reuse、提高 ulimit -n |
| 只压建连不压保活 | 上线三天后内存涨满 | 至少跑 30 分钟以上稳态,观察累积曲线 |
| 压测客户端把心跳挤掉 | 服务端主动断开,误判为服务端不稳 | 压测器必须实现心跳与 pong 响应 |
| 只看平均延迟 | 掩盖长尾,P99 才是用户体感 | 固定输出 P50/P95/P99 与最大值 |
| 没有基线版本对比 | 无法判断优化是否有效 | 每次压测记录版本号、内核参数、压测脚本 hash |
| 压到极限直接结束 | 无法判断资源能否回落 | 必须包含恢复段,验证 fd 与协程回落到基线 |
巡检项
- 每季度重跑一次容量压测,记录版本、内核参数、连接数上限与拐点位置。
- 压测脚本与参数(连接数、消息尺寸、心跳间隔、发送频率)纳入版本库,与线上配置一致。
- 压测报告模板固定七项:连接数、msg/s、P50/P95/P99、内存/连接、CPU、错误率、拐点位置。
- 压测结束必须检查 fd、goroutine、内存是否回到基线,否则先按第 17 章排查泄漏。
小结:容量不是一个数字而是一组约束——先分清建连、保活、收发三层分别压,再用 curl 验证握手、用 k6 压 WebSocket、用 ghz 压内部推送 RPC;压力上来时同步采集 ss -s、sar、pprof 三类数据画在同一时间轴上找拐点,最后按内存、fd、CPU、带宽里的最小值定容并预留两倍余量,同时留出恢复段验证资源能回落。