高级后端 · 压测指标与工具图谱

平均数会撒谎, 分位数才是延迟的通用语言 — Little 定并发, 曲棍球杆定容量, wrk/ab 的数字先过三道口径再信

压测链路: 在途请求才是"并发"的真身 -c 400 在途 wrk 炮台 -t8 -c400 -d30s LB listen 队列 App ×4 worker 池 DB pool=50 ← 每个响应带回一个 RT 样本 → 分位数就是给这堆样本排序 Little's Law: L = λ × W → 并发 c = QPS × RT 5000 QPS × 80ms = 400 在途 — 连接数/线程数/池大小/压测 -c 全由它推导 RT = T_net + T_queue + T_svc — 服务 5ms、排队 40ms: 等待才是大头 口径链: nginx QPS ≠ app QPS ≠ DB QPS — 报数字必带层与接口名 瓶颈会迁移: App 顶住了, 先饱和的是 DB pool=50 — 系统容量 = 全链最短板 延迟分布解剖: P99 是第 99% 名的门槛 请求数 ↑ P50 P90 P95 P99 P99.9 μ=45ms 2ms 5ms 10ms 15ms 20ms 30ms 60ms 200ms 1.5s 均值 45ms 是被最慢 1% 拉高的 — 90% 的请求其实 ≤ 20ms P99 = 1 万请求排第 9900 名: 放过最慢 100 个, 其余都得比它快 曲棍球杆曲线: knee 之后吞吐封顶, 时延爆炸 knee ≈ 70% 水位 QPS ↑ / RT ↑ 并发 c → 吞吐量 QPS 时延 RT knee 之后每 +1 并发: QPS 不涨, RT 指数涨 M/M/1 排队: W = S/(1−ρ) — ρ=0.5→2S · 0.8→5S · 0.9→10S · 0.95→20S → 工作点压 70% 输出解剖与事故现场: 先怀疑炮台, 再怀疑系统 wrk 输出五要素 Requests/sec: 41015 Latency Distribution 50% 1.12ms 99% 2.41ms Transfer/sec: 15.18MB errors: connect/read/write/status/timeout = 0 ← 吞吐量 (成功口径!) ← 分位数 P50 与 P99 的倍数 = 拥挤度 ← 带宽口径 (≠ QPS) ← ≠0 先查错误再谈吞吐 ab 陷阱: Time per request 83.2ms 是 RT × c, 不是 RT 真平均在下一行 "across all concurrent requests": 0.416ms = 1000/RPS 事故现场: 压测机端口打光, 误判"系统不行了" wrk: connect() failed: Too many open files (ulimit -n 1024 < -c 2000) TIME_WAIT 28106 → 本地端口 60s 不回收, 单目标 QPS 卡 ≈470/s 工具选型: wrk / ab 单机炮台 · k6 / Locust / JMeter 场景编排 · vegeta / wrk2 恒速 open-loop · tcpcopy / Gor 流量复制

指标五兄弟, 口径定生死

  • • QPS/TPS/RPS 各层各算: nginx/app/DB 差 3 倍很正常, 报数必带层
  • • 吞吐量三口径: #/s、Transfer/sec 带宽、TPS 事务, 别互相冒充
  • • RT = 网络 + 排队 + 服务; 服务 5ms 排队 40ms 是常态
  • • 平均数对长尾毫无抵抗, SLA 只写 P50/90/95/99

两个公式走天下

  • • Little: c = QPS × RT — 并发/连接池/压测 -c 全由它推
  • • M/M/1: W = S/(1−ρ), ρ=0.9 → 10 倍服务时间
  • • 曲棍球杆: knee 之后 QPS 封顶, P99 起飞
  • • 容量 = knee 峰值 × 0.7, 别把极限当常态

压测的诚实性

  • • 压测机三死: wrk 单核 100%、fd 1024、TIME_WAIT 打光
  • • 预热丢弃 + ≥3 轮取中位数 + 方差 >5% 重测
  • • 失败请求不算吞吐, 非 2xx 先看 errors
  • • ab 两行 Time per request, 读错差 200 倍

💡 一句话理解

把系统想成一条高速公路收费站: QPS 是每秒涌来的车流, RT 是过站耗时, 而 P99 回答的是"最堵的那 1% 车要等多久" — 平均过站时间会被堵死的车硬拉高, 所以谈性能只认分位数。车流到 7 成时一切安好, 再往上排队长度非线性爆炸 (曲棍球杆), 这不是玄学, 是排队论 W = S/(1−ρ)。wrk/ab 就是那个发车的调度员: Little's Law 告诉你该发多少车 (c = QPS × RT), 但发车之前先确认调度员自己没累趴 — 压测机端口/fd/CPU 先死, 是压测界的第一玩笑话, 也是第一常见事故。

🧠 必知必会 必考 & 必会

QPS 与 RPS
Requests/Queries per Second — 每秒请求数。但"请求"分层: nginx 记 8000、app 处理 8000、DB 收 22000 (一条请求多条 SQL)。裸报 QPS 是没有主语的句子, 必带层与接口名。
$ wrk -t8 -c400 -d30s --latency http://api.local/list | tail -1
Requests/sec:  41015.32      # 关键: 这是 wrk 视角的 HTTP 请求吞吐
# nginx QPS ≈ app QPS, DB QPS 可能是它的 2.7 倍 — 各层各算
TPS 事务吞吐
Transactions per Second — 每秒业务事务数。一个"下单"事务 = 风控+库存+订单+券 ≈7 个 HTTP 请求: 网关 3500 QPS 只等于 500 TPS。容量评审对齐 TPS (业务), 压测参数对齐 QPS (机器)。
order_tps = 500
gateway_qps = order_tps * 7       # → 3500: 一个事务 7 个请求
db_qps = order_tps * 22           # → 11000: 一个事务 22 条 SQL
# 关键: 三层各说各话, 混用 = 容量评审翻车
吞吐量三口径
Throughput 广义 = 单位时间完成的工作量, 三种口径: 请求数/秒、字节数/秒 (带宽)、事务/秒。wrk 同时给 Requests/sec 和 Transfer/sec — 大响应下先撞墙的是网卡不是 CPU。
Requests/sec:  41015.32      # 口径一: 请求数每秒
Transfer/sec:   15.18MB      # 口径二: 字节每秒 ≈ 121 Mbps
# 关键: 380B 响应没事; 换 10KB 响应 = 3.2 Gbps → 千兆网卡先死
时延分解
Response Time = T_net + T_queue + T_svc。你测到的是响应时间, 服务时间只占一角: SQL 执行 5ms 但等连接池 40ms。排队等待才是优化的第一现场 — 上来就改 SQL 常常白干。
rt = net + queue + svc        # 0.4 + 40 + 5 (ms)
# 关键: svc 只占 11% — 先查"在等谁", 再查"在算什么"
平均数的谎言
均值对长尾毫无抵抗力: 9800 个 10ms + 200 个 2s, 平均 49.8ms"健康", 但投诉来自那 2%。SLA/SLO 必须写分位数 P50/90/95/99/999 — Google SRE 标配里没有"平均延迟"这个 SLO。
lat = [10]*9800 + [2000]*200          # ms, 2% 长尾
print(sum(lat)/len(lat))              # → 49.8  平均"健康"
print(sorted(lat)[9900-1])            # → 2000  P99 才是真相
分位数怎么算
全量排序取第 ⌈p×N⌉ 名 (nearest-rank); 流式用 HdrHistogram / t-digest 近似, 内存只与桶数相关。三条铁律: 分位数不能跨时间窗平均、不能跨实例平均、桶边界必须盖住 SLO。
import math
lat = sorted([8, 12, 9, 200, 11, 1500, 10, 9])   # ms
rank = math.ceil(0.99 * len(lat))                # → 8 (1-based)
print(lat[rank-1])                               # → 1500
# 关键: 样本小时 P99 ≈ max — 样本必须够多, 否则噪声当分位
Little's Law
系统内平均请求数 L = 到达率 λ × 平均停留 W。压测译码: 并发 c = QPS × RT(s) — 想打 5000 QPS、RT 80ms, 就要 400 在途连接。连接池、线程池、wrk -c 全部由它推导, 不拍脑袋。
c = 5000 * 0.080        # → 400 在途 = 压测 -c 与连接池下限
# 关键: RT 翻倍 → 同样 c 下 QPS 减半, 这就是雪崩的数学
并发 ≠ 线程 ≠ 连接
并发 c 是 in-flight (已发出未返回); 线程是载体, 连接是管道。keep-alive 下连接数 ≈ c; 短连接下连接数远超 c (TIME_WAIT 堆积)。"支持 1000 并发"不等于"开 1000 个线程"。
$ ss -ant state established | tail -n +2 | wc -l
412            # → keep-alive 下 ≈ wrk -c 400 的在途请求
# 关键: 看在途 (QPS×RT), 别数线程
M/M/1 与曲棍球杆
排队论最简式: 等待 W = S/(1−ρ), ρ=λ/μ 是利用率。ρ=0.5→2S、0.9→10S、0.95→20S — 利用率逼近 1 时延迟爆炸, 这就是"吞吐平、延迟翘"的曲棍球杆。工作点压在 70%。
for rho in (0.5, 0.8, 0.9, 0.95):
    print(rho, 1/(1-rho))   # → 2.0 / 5.0 / 10.0 / 20.0 倍
# 关键: 最后 10% 的利用率, 买走 10 倍的延迟
think time 与闭环
真实用户操作间有思考时间 (3~10s); 压测不带 think time 等于全员狂点, 画像失真。闭环模型 (收一个发一个) 配 think time: VU 数 ≈ QPS × (RT + think)。open-loop 恒速发压见"分布式韧性"页。
qps, rt, think = 200, 0.1, 5
vu = qps * (rt + think)      # → 1020 个虚拟用户
# 关键: 不加 think time, 1020 VU 会打出 10200 QPS
warmup 预热
JIT 未编译、缓存冷、连接池空、索引未进 buffer pool — 前 2~5 分钟全是假数据。纪律: 同参数预热 5~10 分钟丢弃, 正式测 ≥10 分钟; soak 另算 (小时级抓泄漏)。
$ wrk -t8 -c400 -d5m  URL            # 预热, 结果丢弃
$ wrk -t8 -c400 -d10m --latency URL > run1.txt
# 关键: 预热与正式必须同参数, 换参数 = 重新冷启动
wrk 架构与参数
wrk = 每线程一个 epoll 事件循环: -t 线程数 (≤核数), -c 总连接, -d 时长, --latency 打印分位数, -s 挂 Lua 脚本。单线程 ~几万 RPS 封顶 — 数字上不去先 top 看 wrk 是不是先 100%。
$ wrk -t8 -c400 -d10m --latency --timeout 2s \
      -s order_bench.lua http://api.local/order
# -t8: 8 线程×epoll;  -c400: 400 条长连接
# 超过核数的 -t 只会让 wrk 自己打自己
ab 两个 Time per request
ab 第一行 "Time per request: 83.2ms (mean)" 是 RT×c (一批 c 个的墙钟时间); 第二行 "(mean, across all concurrent requests)" = 1000/RPS 才是平均 RT。读错第一行, 差 200 倍。
Requests per second:    2403.85 [#/sec] (mean)
Time per request:       83.200  [ms] (mean)      # = c/RPS×1000
Time per request:       0.416   [ms] (mean, across all concurrent requests)
# 关键: 83.2 = 200/2403.85×1000 — 是"一批"不是"一个"
黄金四信号
Google SRE 四信号: latency (分位数)、traffic (QPS/带宽)、errors (率+绝对数)、saturation (队列深度/在途/水位)。吞吐+时延只是前两个; 没有后两个, 系统满了你都不知道。
# saturation 的一手来源:
$ curl -s :8080/metrics | grep -E 'inflight|pool_wait'
http_inflight_requests 1840     # 在途逼近容量 → 饱和预警
# 关键: 四件套齐了, 才叫可观测

🏭 生产实战 real world

场景 1 · wrk Lua 压"带登录态的下单"接口

首页压得动, 下单压不了 — POST body、随机 SKU、多用户 token 都要脚本化。wrk 内嵌 Lua: request() 每请求回调, done() 自定义报表。

-- order_bench.lua: 70% 查询 + 30% 下单, 请求级随机 SKU/用户
wrk.headers["Content-Type"] = "application/json"
function request()
    if math.random(10) <= 3 then              -- 30% 下单
        wrk.method = "POST"
        wrk.body = string.format('{"sku_id":%d,"count":1,"uid":%d}',
                                 1000 + math.random(50), math.random(1, 100000))
    else
        wrk.method, wrk.body = "GET", nil     -- 其余走查询
    end
    return wrk.format()
end
function done(summary, latency)
    print(string.format("req=%d err_status=%d timeout=%d", summary.requests,
                        summary.errors.status, summary.errors.timeout))
    for _, p in ipairs({50, 90, 99, 99.9}) do
        print(string.format("p%g = %.1f ms", p, latency:percentile(p)))
    end
end
# $ wrk -t8 -c400 -d10m --latency --timeout 2s -s order_bench.lua http://api.local/order

errors.status 里是非 2xx/3xx 计数 — 吞吐判读前先看它是不是 0。

场景 2 · ab 报告逐行解读: 大促前的容量判定

ab 输出是面试与评审高频题, 每一行都要读得出主语。

$ ab -n 50000 -c 200 -k -H "Accept-Encoding: gzip" http://api.local/list
Concurrency Level:      200
Time taken for tests:   12.345 seconds
Complete requests:      50000      # 全部完成
Failed requests:        0          # 非 0 先停下查, 别看后面了
Requests per second:    4050.26 [#/sec] (mean)          # ← 吞吐
Time per request:       49.380 [ms] (mean)              # ← RT×c, 不是 RT!
Time per request:       0.247 [ms] (mean, across all concurrent requests)
  90%      38                                 # ← 分位段, ab 自带
  99%     102                                 # P99 = 102ms
 100%     245 (longest request)
# 判定: 4050 QPS @ P99 102ms, SLO 200ms → 达标; 余量 = 1 - 3000/4050 ≈ 26%

49.380 = 200/4050.26×1000 — 一批 200 个的墙钟时间; 平均 RT 认准 across 行。

场景 3 · 用 Little's Law 推导压测参数与连接池

容量评审不拍脑袋: 业务目标先翻译成机器语言, 再反推每一层的水位。

# 大促评审: 目标 5000 单/秒, 一单 7 个请求, 全链 P99 80ms
qps_gateway = 5000 * 7             # → 35000 QPS, 这才是要压的数
concurrency = qps_gateway * 0.080  # → 2800: 压测并发下限 (Little)
# 单台 wrk 打不动 2800 连接 → 4 台压测机 × -c 700 分片发压

# DB 侧: SQL 平均 15ms → 在途 SQL = 35000 × 0.015
db_inflight = 35000 * 0.015        # → 525 条在途 SQL 的连接需求
pool_per_node, db_nodes = 20, 30   # HikariCP: core×2+spindles≈20 → 600 ≥ 525 ✓
# 关键: 连接池不是越大越好 — 越大锁竞争越凶, 够在途数即可

这套推导让评审从"感觉要扩容"变成 35000/2800/525 三个数字的对话。

场景 4 · Prometheus 直方图桶: 让 P99 可算可告警

默认桶 (.005~10s) 在 SLO 附近没有刻度, P99 算出来永远是插值幻觉。

# 关键 1: le 桶边界必须细过 SLO — 目标 P99 800ms,
#        默认桶 0.75~1s 之间只有一个刻度, 800ms 根本判不准
buckets: [.01, .025, .05, .1, .2, .3, .5, .7, .8, 1, 1.5, 2, 3]

# 关键 2: 分位数必须"先聚合桶再算", 不能对各实例的 p99 求平均
# histogram_quantile(0.99,
#   sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
# 关键 3: series 数 = label 组合 × 桶数 — user_id 这类高基数
#        绝不能进 histogram, 否则 TSDB 先被压测打挂

桶从 14 个收到 13 个且贴住 SLO: 告警判定准了, series 还少了 8%。

场景 5 · 事故排查: "系统最多 8000 QPS"其实是压测机先死

压不上去先做炮台体检: CPU、fd、TIME_WAIT 三件套, 十有八九是它。

$ wrk -t16 -c2000 -d30s http://api.local/list
# QPS 死活上不了 8000, P99 却只有 0.4ms — 系统毫发无伤?

$ top -p $(pgrep wrk)        # wrk 单线程 100%: -t16 在 8 核机上自己卷自己
$ ulimit -n                  # → 1024, -c 2000 直接 Too many open files
$ ss -s | grep -i timewait   # TIME_WAIT: 28106, 短连接把本地端口打光

# 处方四连:
#   1) -t 改 8 (≤ 核数); 不够就加机器分片, 不加线程
#   2) ulimit -n 65535 (systemd 服务写 LimitNOFILE=)
#   3) sysctl -w net.ipv4.tcp_tw_reuse=1
#   4) sysctl -w net.ipv4.ip_local_port_range="10240 65535"

改完同样 -c2000: QPS 8000 → 41000, "系统瓶颈"原来在压测机上。

场景 6 · 阶梯加压找 knee: 一小时产出容量曲线

容量不是压一次得出的, 是"抬一档、看两头 (QPS 与 P99)"找拐点。

#!/bin/bash
# 每 90s 抬一档并发, 冷却 30s 防上一档队列残余污染下一档
for c in 100 200 400 800 1600; do
  wrk -t8 -c$c -d90s --latency http://api.local/list \
    | tee "bench_c${c}.log" | grep -E "Requests/sec|  99%"
  sleep 30
done
# 实测读数:
#   c=400  QPS 6100  P99 45ms   ← 还在线性区
#   c=800  QPS 6250  P99 1900ms ← QPS 只 +2%, P99 ×42: 过 knee 了
# 结论: knee≈6200, 上线容量 = 6200 × 0.7 ≈ 4300, 写进容量台账

从此扩容申请都引用这份曲线, 不再"感觉不够了"。

场景 7 · k6 阈值化场景进 CI: 压测当发布门禁

JMeter 适合录制回放, Locust 适合 Python 团队, k6 的 thresholds 天然配 CI: 不达标直接红。

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    steady: { executor: 'constant-arrival-rate',   // open-loop 恒速防 omission
              rate: 2000, timeUnit: '1s',
              preAllocatedVUs: 500, maxVUs: 2000, duration: '5m' },
  },
  thresholds: {                                    // 不达标 → 退出码非 0
    http_req_duration: ['p(95)<300', 'p(99)<800'],
    http_req_failed:   ['rate<0.001'],
  },
};
export default function () {
  const r = http.get(`${__ENV.BASE}/list`);
  check(r, { 'status is 200': (r) => r.status === 200 });
}

合入主干每夜跑 5 分钟, 两起 N+1 与一起缓存未预热在发布前被拦。

场景 8 · 压测报告统计纪律: 4 跑弃 1 取中位

单次压测噪声 ±15%: 拿最好一次汇报是自欺, 拿最差一次是冤枉系统。

from statistics import median
# 4 轮同参数: 第 0 轮当预热丢弃, 只统计后 3 轮
runs = [parse(f"bench_c400_r{i}.log") for i in range(4)]
samples = runs[1:]
qps_list = [r["qps"] for r in samples]   # [6120, 6180, 6250]
p99_list = [r["p99"] for r in samples]   # [44, 46, 47]

report = {
    "qps":  median(qps_list),              # 6180 — 中位数抗离群
    "p99":  median(p99_list),              # 46ms
    "spread": (max(qps_list) - min(qps_list)) / median(qps_list),  # 2.1%
}
assert report["spread"] < 0.05, "run 间方差 >5% 必须重测: 环境不干净"
# 报告必附环境快照: commit / 配置 diff / 机器规格 / 数据量级

执行这套纪律后, 两次"性能抖动"复盘都发现是压测机邻居在抢网卡。

场景 9 · 数据规模对齐: 空表 P99 3ms, 亿行表 480ms

索引高度、buffer pool 命中率、执行计划全随数据量变 — 造数必须与生产同量级。

-- 影子表: 结构与索引与生产一致, 不动生产数据
CREATE TABLE orders_bench LIKE orders;
INSERT INTO orders_bench (user_id, sku_id, amount, created_at)
SELECT user_id, sku_id, amount, created_at
  FROM orders WHERE created_at >= NOW() - INTERVAL 90 DAY
  LIMIT 120000000;                     -- 1.2 亿行, 与生产同量级
ANALYZE TABLE orders_bench;            -- 统计信息对齐, 执行计划才一致
-- 压前自检: 两边 EXPLAIN 必须走同一个索引再开压
EXPLAIN SELECT * FROM orders_bench
 WHERE user_id = 9527 ORDER BY created_at DESC LIMIT 20;

重压后 P99 3ms → 480ms: 这才是上线后会发生的数字。

场景 10 · 全链路压测流量染色: 影子表 + 挡板

生产环境全链路压测的前提是压测流量"可识别、可隔离、可清理"。

// 入口中间件: 给压测流量染色, DAO 层按标记路由影子表
func LoadTestMark(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("X-Load-Test") == "1" {
            ctx := context.WithValue(r.Context(), shadowKey{}, true)
            r = r.WithContext(ctx)
        }
        next.ServeHTTP(w, r)
    })
}

// DAO 层: 染色流量读写 orders_shadow, 与真实数据物理隔离
func table(ctx context.Context) string {
    if isShadow(ctx) {
        return "orders_shadow"     // 压测脏数据进不了生产表
    }
    return "orders"
}
// 配套: 染色标全链透传 (MQ/RPC 都要带上), 第三方依赖按标记走挡板

大促前在生产打 2 倍峰值流量, 真实订单零污染, 缓存/连接池全程热态。

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 平均时延当 SLA — avg 50ms 全绿, 用户却在骂. 原因: 长尾被平均抹平. 正解: SLO 只写分位数。
# 错: 平均 RT 50ms → 达标 ✓
# 对: P99 = 1800ms > SLO 200ms → 不达标 (1% ≈ 每分钟 600 人)
坑 2 · 对分位数求平均 — pod-A p99=10ms、pod-B p99=900ms, "平均 p99 455ms"没有任何用户体验是 455ms. 正解: 聚合原始桶再 quantile。
# 错: avg(histogram_quantile(0.99, ... by (pod)))  → 455ms 幻觉
# 对: histogram_quantile(0.99, sum by (le) (rate(bucket[5m])))
坑 3 · QPS/TPS 混用 — 评审上说"撑 3500"其实只是网关 QPS, 业务只有 500 单/秒. 正解: QPS 说机器、TPS 说业务, 换算写进文档。
# 错: "网关 3500 QPS → 撑 3500 单/秒"
# 对: 3500 QPS ÷ 7 请求/单 = 500 TPS
坑 4 · 吞吐量当带宽 — QPS 还没累着, 千兆网卡先满. 原因: 只报 #/s 不看字节. 正解: Transfer/sec 与网卡规格对表。
# 错: 只汇报 Requests/sec 41015
# 对: 41015 × 10KB × 8 ≈ 3.2 Gbps → 先升万兆网卡再谈压测
坑 5 · 吞吐含失败请求 — 服务 500 且返回快, QPS 反而"涨了", 假繁荣. 正解: 只算成功口径, wrk 看 errors.status。
# 错: Requests/sec 50000 → "吞吐翻倍!"
# 对: err_status=30000 → 真实成功吞吐 20000, 立即回滚
坑 6 · ab 的 Time per request 当 RT — 第一行是 RT×c, c=200 时虚高 200 倍. 正解: 读 "across all concurrent requests" 行或除以 c。
# 错: Time per request: 83.2ms → "平均 83ms?"
# 对: 0.416ms (across all concurrent) = 1000/RPS 才是平均 RT
坑 7 · 裸 QPS 无主语 — "我们 QPS 8 万"是 nginx、app 还是 DB? 三层差 3 倍很正常. 正解: 报数字带层+接口。
# 错: "系统 QPS 80000"
# 对: "下单链路: nginx 8000 / app 8000 / DB 22000"
坑 8 · wrk 线程超核数 — -t64 跑在 8 核机, wrk 自我内卷, 测的是调度器. 正解: -t ≤ 核数, 缺炮火加机器。
# 错: wrk -t64 -c4000   (8 核压测机)
# 对: -t8, 4 台 × -c1000 分片
坑 9 · keep-alive 口径不一 — ab 默认短连接, wrk 只会长连接, 两工具数字差一个握手+慢启动. 正解: ab 加 -k 后再对比。
# 错: ab 2400 vs wrk 41000 → "wrk 快 17 倍"
# 对: ab -k 重测 39000 → 差距其实是连接模型, 不是服务
坑 10 · TIME_WAIT 打光端口 — 短连接高 QPS: 本地端口约 2.8 万个、60s 回收, 单目标 ≈470/s 封顶, 表象是"服务连不上". 正解: tw_reuse + keep-alive。
# 错: connect timeout → 判"服务挂了"
# 对: ss -s 看 TIME_WAIT 28106 → tw_reuse=1 + 长连接
坑 11 · fd 上限没调 — ulimit 1024 撞上 -c2000, wrk 直接罢工. 原因: 压测机默认配置. 正解: 开压前先提 fd。
# 错: wrk: connect() failed: Too many open files
# 对: ulimit -n 65535 再开压 (systemd: LimitNOFILE=)
坑 12 · 不预热就采数 — JIT/缓存/连接池全冷, 前 2 分钟数据污染结论. 正解: 同参数预热 5~10 分钟丢弃。
# 错: wrk -d60s 跑一次就进 PPT
# 对: 预热 5m 丢弃 → 正式 -d10m 连测 3 轮
坑 13 · 只压首页 — GET / 能扛 5 万, 下单 300 就挂. 原因: 压测画像不贴生产. 正解: 按流量比例混合编排。
# 错: ab -n 100000 http://svc/
# 对: 70% list + 25% detail + 5% order (k6 scenarios 加权)
坑 14 · 数据量不对齐 — 空表 P99 3ms 直接上线, 亿行表 480ms. 正解: 同量级造数 + ANALYZE 对齐执行计划。
# 错: 测试库 1000 行压出 P99 3ms
# 对: 1.2 亿行 + ANALYZE 后重压 → 480ms 才是真话
坑 15 · knee 之后当常态容量 — 压出峰值 6200 就按 6200 排期, 一进高峰 P99 爆炸. 正解: 容量 = 峰值 × 0.7。
# 错: capacity = knee 峰值 6200
# 对: capacity = 6200 × 0.7 ≈ 4300, 留排队余量
坑 16 · 单次压测下结论 — 环境噪声 ±15%, 抽到好一次就汇报. 正解: ≥3 轮取中位数并报方差, 方差超 5% 重测。
# 错: 1 轮 QPS 6800 → 汇报 "6800"
# 对: 4 轮弃 1 → 6180 ± 2.1%, 附环境快照
坑 17 · 单台 wrk 当万级炮台 — wrk 单线程几万 RPS 封顶, 打不到目标先怀疑炮台不是系统. 正解: 多机分片 + 服务端对账。
# 错: 1 台压测机非要打出 10 万 QPS
# 对: 4 台 × 2.5 万, 网关访问日志对账请求数
坑 18 · 压测机与服务同机 — 抢 CPU/网卡/内存带宽, 数字双输. 原因: 机器省着用. 正解: 独立压测机, 同机房同规格。
# 错: wrk 和服务跑同一台 8 核
# 对: 压测机独立部署, 核数/网卡对等
坑 19 · 直方图桶盖不住 SLO — 默认桶 0.75s/1s 之间没刻度, 800ms 的 SLO 判不了. 正解: 桶边界按 SLO 细分。
# 错: 默认桶 …0.75, 1 → SLO 800ms 落在 250ms 大缝里
# 对: 自定义桶加 .7 .8 .9 → SLO 附近分辨率 100ms
坑 20 · 并发数当线程数 — "支持 1000 并发"被理解成"开 1000 线程", 池子越调越歪. 正解: 并发 = in-flight = QPS×RT, 线程只是载体。
# 错: "1000 并发 = 线程池 max 1000"
# 对: c = 5000×0.08 = 400, worker 池 256 足够