负载阶梯与饱和点
目标
像爬梯一样逐级提高并发度,测量吞吐量与延迟的关系,用数字找出饱和点以及满足 SLA 的最大负载点。然后用 Little 定律验证测量本身是否有效。产物放在 /root/lt2/ 下。
为什么重要
负载测试中最隐蔽的失败,是“负载生成器没有产生目标负载”。如果一次响应占用 2 秒,同步生成器在这 2 秒内不会发送原本应到达的请求。系统最慢区间的样本会整体消失,而生成器反而配合了自己制造的背压。结果是即使提高负载,p99 也不变化,只有生产环境的尾延迟恶化。因此,每次运行都需要附加一行验收检查:报告的实际吞吐量是否与设定的目标吞吐量一致。如果不一致,该次运行的延迟分布就不可信。本实验的第 6、7 步正是进行这项检查。
准备——启动负载目标
本实验运行在 Ubuntu 24.04 VM 上,并且已安装 Docker。即便如此,我们仍以Python 进程而不是容器运行目标。不是因为做不到,而是因为这样才正确。
不能随便选择负载目标。只有单个请求成本明确的目标,负载阶梯才会呈现应有形状,第 6 步用 Little 定律进行的复核(rps × 평균 지연 ≈ 동시성)才成立。如果 nginx 只返回静态文件,每次请求甚至不到 1ms,测量到的就不是服务器,而是负载生成器自身——提高阶梯也看不到拐点;即使出现,也只是 hey 的极限,而非目标系统的极限。
下面的目标将单次请求成本设为 50ms。正因为存在这 50ms,提高并发时才能清晰看到吞吐量先上升、再停止增长的位置。
mkdir -p /root/lt2
cat > /root/lt2/target.py <<'PY'
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
BODY = b"labhub load-test target
"
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
disable_nagle_algorithm = True # 헤더와 본문이 따로 나가면 지연 ACK 로 40ms 가 얹힌다
def do_GET(self):
time.sleep(0.05) # 요청 하나의 처리 비용
self.send_response(200); self.send_header("Content-Length", str(len(BODY)))
self.end_headers(); self.wfile.write(BODY)
def log_message(self, *a): pass
ThreadingHTTPServer(("127.0.0.1", 8085), H).serve_forever()
PY
setsid nohup python3 /root/lt2/target.py >/dev/null 2>&1 &
curl -s -o /dev/null -w '%{http_code}
' http://127.0.0.1:8085/ # 200
如果一定要改用容器,也必须像 docker run -d --name lt-web -p 127.0.0.1:8085:8080 -v /root/lt2:/app python:3.12-alpine python /app/target.py 这样封装同一个 Python 目标。基于上一段的原因,改成 nginx 会使后续所有数字失去意义。
步骤
- 在
/root/lt2/plan.md中编写阶梯计划。必须包括:并发度 1, 2, 5, 10, 20 五个阶段、每个阶段的持续时间、预热计划,以及成功标准(例如 p95 阈值)。 - 创建
/root/lt2/ramp.sh并执行chmod +x。它必须把输出目录作为第一个参数($1),依次运行并发度 1, 2, 5, 10, 20,并将结果保存为$1/c<동시성>.txt(例如c10.txt)。 - 针对
/root/lt2/out运行脚本。必须生成/root/lt2/out/c1.txt、c2.txt、c5.txt、c10.txt、c20.txt五个文件;每个文件的总响应数至少为 100,并保留Requests/sec和95% in行。 - 创建
/root/lt2/ramp.csv。格式为concurrency,rps,p95,errors,写入并发度 1/2/5/10/20 的五行数据。rps和p95读取对应cN.txt的值;errors是状态码分布中非 2xx 的响应数(整数,必须精确一致)。 - 在
/root/lt2/knee.txt中写一行knee_concurrency=。按照 1 → 2 → 5 → 10 → 20 的顺序查看并发度,找到相对前一阶段 RPS 增长率首次低于 10% 的位置,并把其前一个并发度作为答案。若始终增长 10% 以上,答案为 20。 - 在
/root/lt2/little.txt中写四行:concurrency=10、rps=(out/c10.txt的 Requests/sec)、latency_avg_s=(同一文件的 Average)、product=(rps × 平均延迟,保留两位小数)。乘积与并发度 10 相差很大时,该次运行没有产生目标负载。 - 再运行一次固定到达率的(open)测试并保存到
/root/lt2/open.txt。在/root/lt2/model.md中写明:closed(固定并发)模型说明、open(固定到达率)模型说明、coordinated omission 说明,以及target_rps=(设定的目标吞吐量)和actual_rps=(从 open.txt 读取的实际吞吐量)两行。 - 在
/root/lt2/max-safe.txt中写两行:max_safe_concurrency=、max_safe_rps=。答案是ramp.csv中p95 不超过 0.2 秒且 errors 为 0 的行里最大并发度及其对应 RPS。
参考
- open 模型近似:
hey的-q限制每个工作进程每秒的请求数。hey -z 30s -c 50 -q 4 http://127.0.0.1:8085/表示目标 200 RPS(50 × 4),因此写入target_rps=200并与实际值比较。 - 脚本骨架:
for c in 1 2 5 10 20; do hey -n 300 -c "$c" http://127.0.0.1:8085/ > "$1/c$c.txt" 2>&1; done。前面加入mkdir -p "$1"。 errors可用awk '/Status code distribution/{f=1;next} f && /responses/ {gsub(/[][]/," "); if($1<200||$1>=300) e+=$2} END{print e+0}'统计。没有错误时填写0。- 常见错误 1:在
ramp.sh中硬编码输出路径。不使用第一个参数会被评分发现。 - 常见错误 2:第 5 步把“RPS 最高的并发度”写成拐点。拐点不是最高点,而是增长开始停滞位置的前一个阶段。
- 常见错误 3:每个阶段运行时间过短。少于 100 个样本时,分位数没有意义。
编写负载阶梯计划
在 /root/lt2/plan.md 中编写阶梯计划。必须包括:并发度 1, 2, 5, 10, 20 五个阶段、每个阶段的持续时间、预热计划,以及成功标准(例如 p95 阈值)。
在 /root/lt2/plan.md 中写明五个并发阶段、各阶段持续时间、预热和成功标准。如果不预先定义通过条件,就会在看到结果后才制定标准。
编写执行脚本
创建 /root/lt2/ramp.sh 并执行 chmod +x。它必须把输出目录作为第一个参数($1),依次运行并发度 1, 2, 5, 10, 20,并将结果保存为 $1/c<동시성>.txt(例如 c10.txt)。
/root/lt2/ramp.sh 接收输出目录作为第一个参数,依次运行五个阶段,并将结果保存为 c<并发度>.txt。硬编码路径后无法在其他目录重跑。不要忘记 chmod +x。
运行五个阶段
针对 /root/lt2/out 运行脚本。必须生成 /root/lt2/out/c1.txt、c2.txt、c5.txt、c10.txt、c20.txt 五个文件;每个文件的总响应数至少为 100,并保留 Requests/sec 和 95% in 行。
/root/lt2/out 下必须包含 c1、c2、c5、c10、c20 的全部结果,且每阶段至少 100 个响应。每个文件需保留摘要和延迟分布段,以供下一步读取。
创建结果表
创建 /root/lt2/ramp.csv。格式为 concurrency,rps,p95,errors,写入并发度 1/2/5/10/20 的五行数据。rps 和 p95 读取对应 cN.txt 的值;errors 是状态码分布中非 2xx 的响应数(整数,必须精确一致)。
在 /root/lt2/ramp.csv 中整理各并发度的 rps、p95、errors。数值必须来自原始输出,errors 是非 2xx 响应数。
寻找饱和点
在 /root/lt2/knee.txt 中写一行 knee_concurrency=。判定规则如下:按 1 → 2 → 5 → 10 → 20 查看并发度,找到相对前一阶段 RPS 增长率首次低于 10% 的位置,并把其前一个并发度作为答案。若始终增长 10% 以上,答案为 20。
在 /root/lt2/knee.txt 中写一行 knee_concurrency=。答案是吞吐量开始不再显著增长位置之前的并发度,精确规则已写在说明中。
用 Little 定律验证运行
在 /root/lt2/little.txt 中写四行:concurrency=10、rps=(out/c10.txt 的 Requests/sec)、latency_avg_s=(同一文件的 Average)、product=(rps × 平均延迟,保留两位小数)。乘积与并发度 10 相差很大时,该次运行没有产生目标负载。
在 /root/lt2/little.txt 中写入并发度 10 区间的 rps、平均延迟及其乘积。乘积与并发度相差很大,说明负载生成器没有真正产生目标负载。
比较开放与封闭模型
再运行一次固定到达率的(open)测试并保存到 /root/lt2/open.txt。在 /root/lt2/model.md 中写明:closed(固定并发)模型说明、open(固定到达率)模型说明、coordinated omission 说明,以及 target_rps=(设定的目标吞吐量)和 actual_rps=(从 open.txt 读取的实际吞吐量)两行。
单独运行固定到达率测试并保存到 /root/lt2/open.txt,在 model.md 中说明两种模型与 coordinated omission。并列写出目标吞吐量和实际吞吐量。
计算满足 SLA 的最大负载点
在 /root/lt2/max-safe.txt 中写两行:max_safe_concurrency=、max_safe_rps=。答案是 ramp.csv 中p95 不超过 0.2 秒且 errors 为 0 的行里最大并发度及其对应 RPS。
在 /root/lt2/max-safe.txt 中写入满足条件的最大并发度及其吞吐量。判定条件是说明中的两项,必须直接从表中选择。