每秒三千次请求,结果全是错误页面
目标
对一个状态码始终是 200、响应体却是错误的对象做压力测试,亲手统计,确认只看摘要的测试会漏掉什么。通过改变连接复用、响应大小和缓存键,判断测试条件是否能代表生产,最后制作一个判定“这个结果能不能信”的检查清单脚本,用退出码建立门禁。
为什么重要
压力测试报告中最危险的一句话是“错误率 0%”。负载生成器只知道状态码,而装在 200 里的错误,在它眼里是看不见的。而且错误路径不做实际工作,所以结束得很快,测试坏得越严重,延迟数字就越好看。所以没有验证的压力测试,不是错误的答案,而是没有答案。加上验证之后,还剩下一个问题——这个测试条件能否代表生产。如果每次都新开连接,测的就是 TCP 握手;如果响应只有生产的 64 分之一,就一次也没有触及序列化和带宽;如果只请求同一个 URL,就是缓存替测试通过了。不把这种判断交给人的眼睛,而是用脚本的退出码固定下来,就是本实验的最后一步。
步骤
- 用
DELAY=0.02 ERR_EVERY=3 SIZE=1024把/opt/lab/lt/lt-test-validity/app.py启动在 127.0.0.1:8080 上。访问日志保存到/root/lt-test-validity/01-access.log。启动之后调用一次/reset,运行hey -n 300 -c 10 'http://127.0.0.1:8080/api/order?id=1',把原始输出保存到/root/lt-test-validity/01-summary.txt。然后从该摘要中读取并在/root/lt-test-validity/01-claim.txt中写四行total=、status_200=、non_2xx=、rps=。rps 请原样写摘要中 Requests/sec 的值。 - 统计
/root/lt-test-validity/01-access.log的第六个字段(kind),创建/root/lt-test-validity/02-truth.tsv。共两行,每行是以制表符分隔的两个字段ok <수>和error <수>(占位符为数量)(ok 在前)。然后在/root/lt-test-validity/02-note.txt中写三行——error_ratio=是错误响应体的比例,以百分比表示,保留到小数点后第一位,hey_non2xx=是第 1 步摘要中的非 2xx 响应数,why=用至少 40 个字符写明这两个数字为什么差这么多。 - 用
/root/lt-test-validity/01-access.log的第八个字段(dur_ms)分别求出正常响应和错误响应的中位数,用两行写入/root/lt-test-validity/03-durations.tsv。每行是以制表符分隔的两个字段ok <중앙값>和error <중앙값>(占位符为中位数),毫秒保留到小数点后第一位。中位数取升序排列的 n 个值中的第int(n/2)+1个(从 1 开始计数)。然后在/root/lt-test-validity/03-note.txt中写两行faster=<ok 또는 error>(占位符为 ok 或 error)和effect=<이 성질이 시험 결과를 어느 쪽으로 밀어내는가, 40자 이상>(占位符为这种性质会把测试结果推向哪一边,至少 40 个字符)。 - 用关闭了错误注入的对象(
ERR_EVERY=0 DELAY=0.02 SIZE=1024),对同样的负载测量两次。一次用默认值(/root/lt-test-validity/04-on.txt,访问日志/root/lt-test-validity/04-on.log),一次加上-disable-keepalive(/root/lt-test-validity/04-off.txt,访问日志/root/lt-test-validity/04-off.log),两次都是hey -n 200 -c 10 'http://127.0.0.1:8080/api/order?id=1'。在/root/lt-test-validity/04-keepalive.tsv中写两行——on <연결 수> <rps>和off <연결 수> <rps>(占位符依次为连接数、rps),rps 保留到小数点后第二位。连接数是访问日志第二个字段中出现的不同值的个数。然后在/root/lt-test-validity/04-note.txt中写三行use_run=<on 또는 off>(占位符为 on 或 off)、conn_ratio=<off 연결 수 ÷ on 연결 수, 소수 첫째 자리>(占位符为 off 的连接数 ÷ on 的连接数,保留到小数点后第一位)、why=<50자 이상>(占位符为至少 50 个字符)。请假定生产的客户端使用连接池。 - 对同样的负载(
hey -n 200 -c 10 'http://127.0.0.1:8080/api/order?id=1',ERR_EVERY=0),只改变响应大小,测量两次——SIZE=1024保存为/root/lt-test-validity/05-1k.txt,SIZE=65536保存为/root/lt-test-validity/05-64k.txt。在/root/lt-test-validity/05-size.tsv中写两行。每行是以制表符分隔的四个字段<이름> <요청당 바이트> <rps> <초당 바이트>(占位符依次为名称、每个请求的字节数、rps、每秒字节数),名称是1k和64k,rps 保留到小数点后第二位,每秒字节数是每个请求的字节数 × rps 四舍五入后的整数。每个请求的字节数和 rps 从摘要的Size/request和Requests/sec中读取。然后在/root/lt-test-validity/05-note.txt中写三行bound_by=<latency 또는 bandwidth>(占位符为 latency 或 bandwidth)、bytes_ratio=<64k 초당 바이트 ÷ 1k 초당 바이트, 소수 첫째 자리>(占位符为 64k 每秒字节数 ÷ 1k 每秒字节数,保留到小数点后第一位)、why=<50자 이상>(占位符为至少 50 个字符)。 - 用开启了缓存的对象(
CACHE=1 ERR_EVERY=0 DELAY=0.02 SIZE=1024)测量两次。一次用键固定的'http://127.0.0.1:8080/api/order?id=1'(/root/lt-test-validity/06-fixed.txt,记录/root/lt-test-validity/06-fixed.log),一次用键轮换的http://127.0.0.1:8080/api/order/rotate(/root/lt-test-validity/06-rotate.txt,记录/root/lt-test-validity/06-rotate.log),两次都是hey -n 200 -c 10。第二次请用ROTATE_KEYS=500启动。在/root/lt-test-validity/06-cache.tsv中写两行。每行是以制表符分隔的五个字段<이름> <적중> <빗나감> <적중률> <rps>(占位符依次为名称、命中、未命中、命中率、rps),名称是fixed和rotate,命中率以百分比表示,保留到小数点后第一位,rps 保留到小数点后第二位。然后在/root/lt-test-validity/06-note.txt中写三行representative=<fixed 또는 rotate>(占位符为 fixed 或 rotate)、inflation=<fixed rps ÷ rotate rps, 소수 첫째 자리>(占位符为 fixed 的 rps ÷ rotate 的 rps,保留到小数点后第一位)、why=<50자 이상>(占位符为至少 50 个字符)。 - 创建
/root/lt-test-validity/validate.sh。以bash validate.sh <hey 요약 파일> <접근 기록 파일>(占位符依次为 hey 摘要文件、访问日志文件)调用时,输出三行——status <OK|FAIL> <값>、body <OK|FAIL> <값>、volume <OK|FAIL> <값>(占位符依次为 OK 或 FAIL、值),名称位于行首。status 在摘要中的非 2xx 响应为 0 时为 OK,body 在访问日志中 error 响应体的比例低于 1% 时为 OK,volume 在访问日志的行数与摘要中的响应总数相同时为 OK。只要有一项是 FAIL,就必须以退出码 1 结束,全部为 OK 则以 0 结束。评分器会用它自己制作的四组输入来运行这个脚本,确认通过和失败两种情况。 - 用制作好的检查清单判定第 1 步的结果。把
bash validate.sh 01-summary.txt 01-access.log的输出保存到/root/lt-test-validity/08-gate-out.txt,并在/root/lt-test-validity/08-verdict.txt中写四行——gate_exit=<종료 코드>、failed_check=<FAIL 이 난 검사 이름>、trustworthy=<yes 또는 no>、fix=<이 시험을 다시 하려면 무엇을 고쳐야 하는가, 60자 이상>(占位符依次为退出码、出现 FAIL 的检查名称、yes 或 no、要重新做这个测试需要修正什么,至少 60 个字符)。
参考
- 工作目录是
/root/lt-test-validity。如果不存在,请先创建。 - 压力对象是
/opt/lab/lt/lt-test-validity/app.py。文件开头的注释中写有环境变量、路径和访问日志的八个字段。 - 这个 Pod 中无法启动容器(seccomp)。对象直接在
127.0.0.1上启动 Python 标准库服务器来使用。重新启动之前,请用pkill -f 'lt-test-validity/app.py'杀掉前一个版本。 nproc说的不是 Pod,而是节点的核心数。hey提示的默认-cpus值也是如此——所以本实验的对象不是消耗 CPU,而是用time.sleep()来消耗时间。- 常见错误:第二次运行之前没有重新启动对象,导致访问日志与上一次运行混在一起。
- 常见错误:把
-disable-compression和-disable-keepalive混用。前者关闭压缩,后者关闭连接复用。 - hey (rakyll/hey)、k6 checks、k6 thresholds、http.server、vegeta
只看摘要,这个测试堪称完美
用 DELAY=0.02 ERR_EVERY=3 SIZE=1024 把 /opt/lab/lt/lt-test-validity/app.py 启动在 127.0.0.1:8080 上。访问日志保存到 /root/lt-test-validity/01-access.log。启动之后调用一次 /reset,运行 hey -n 300 -c 10 'http://127.0.0.1:8080/api/order?id=1',把原始输出保存到 /root/lt-test-validity/01-summary.txt。然后从该摘要中读取并在 /root/lt-test-validity/01-claim.txt 中写四行 total=、status_200=、non_2xx=、rps=。rps 请原样写摘要中 Requests/sec 的值。
摘要的 Status code distribution 下面的行,形如 [<코드>]<탭><수> responses(占位符依次为状态码、制表符、数量)。用 awk 去掉方括号后,第一个字段是状态码,第二个字段是数量。想等服务器起来,可以反复向 /stats 发送 curl。这一步得到的数字,以后会被证明是“错误”的数字。
统计服务器实际返回了什么
统计 /root/lt-test-validity/01-access.log 的第六个字段(kind),创建 /root/lt-test-validity/02-truth.tsv。共两行,每行是以制表符分隔的两个字段 ok <수> 和 error <수>(占位符为数量)(ok 在前)。然后在 /root/lt-test-validity/02-note.txt 中写三行——error_ratio= 是错误响应体的比例,以百分比表示,保留到小数点后第一位,hey_non2xx= 是第 1 步摘要中的非 2xx 响应数,why= 用至少 40 个字符写明这两个数字为什么差这么多。
用 awk -F'\t' '$6=="ok"' 01-access.log | wc -l 来统计。访问日志的行数必须与 hey 统计的响应数相同——如果不同,就意味着有中途断开的请求。状态码只是契约的一部分,响应体才是其余部分。
错误更快,所以数字看起来更好
用 /root/lt-test-validity/01-access.log 的第八个字段(dur_ms)分别求出正常响应和错误响应的中位数,用两行写入 /root/lt-test-validity/03-durations.tsv。每行是以制表符分隔的两个字段 ok <중앙값> 和 error <중앙값>(占位符为中位数),毫秒保留到小数点后第一位。中位数取升序排列的 n 个值中的第 int(n/2)+1 个(从 1 开始计数)。然后在 /root/lt-test-validity/03-note.txt 中写两行 faster=<ok 또는 error>(占位符为 ok 或 error)和 effect=<이 성질이 시험 결과를 어느 쪽으로 밀어내는가, 40자 이상>(占位符为这种性质会把测试结果推向哪一边,至少 40 个字符)。
用 awk -F'\t' '$6=="ok"{print $8}' 01-access.log | sort -n 排序后,按行号选取。错误路径既不访问数据库,也不经过队列,所以很快就结束。因此,混进的错误越多,平均值和百分位数就越好看,测试坏掉的信号,不是“变慢了”,而是以“变快了”的形式到来。
是否复用连接,会改变测试的一半
用关闭了错误注入的对象(ERR_EVERY=0 DELAY=0.02 SIZE=1024),对同样的负载测量两次。一次用默认值(/root/lt-test-validity/04-on.txt,访问日志 /root/lt-test-validity/04-on.log),一次加上 -disable-keepalive(/root/lt-test-validity/04-off.txt,访问日志 /root/lt-test-validity/04-off.log),两次都是 hey -n 200 -c 10 'http://127.0.0.1:8080/api/order?id=1'。在 /root/lt-test-validity/04-keepalive.tsv 中写两行——on <연결 수> <rps> 和 off <연결 수> <rps>(占位符依次为连接数、rps),rps 保留到小数点后第二位。连接数是访问日志第二个字段中出现的不同值的个数。然后在 /root/lt-test-validity/04-note.txt 中写三行 use_run=<on 또는 off>(占位符为 on 或 off)、conn_ratio=<off 연결 수 ÷ on 연결 수, 소수 첫째 자리>(占位符为 off 的连接数 ÷ on 的连接数,保留到小数点后第一位)、why=<50자 이상>(占位符为至少 50 个字符)。请假定生产的客户端使用连接池。
访问日志的第二个字段,是承载该请求的 TCP 连接的编号。用 cut -f2 04-on.log | sort -u | wc -l 来统计。第二次运行之前必须重新启动对象,访问日志才不会混在一起。hey 的选项名称不是 -disable-compression,而是 -disable-keepalive——两者关闭的是不同的东西。
把响应放大 64 倍,会改变什么
对同样的负载(hey -n 200 -c 10 'http://127.0.0.1:8080/api/order?id=1',ERR_EVERY=0),只改变响应大小,测量两次——SIZE=1024 保存为 /root/lt-test-validity/05-1k.txt,SIZE=65536 保存为 /root/lt-test-validity/05-64k.txt。在 /root/lt-test-validity/05-size.tsv 中写两行。每行是以制表符分隔的四个字段 <이름> <요청당 바이트> <rps> <초당 바이트>(占位符依次为名称、每个请求的字节数、rps、每秒字节数),名称是 1k 和 64k,rps 保留到小数点后第二位,每秒字节数是每个请求的字节数 × rps 四舍五入后的整数。每个请求的字节数和 rps 从摘要的 Size/request 和 Requests/sec 中读取。然后在 /root/lt-test-validity/05-note.txt 中写三行 bound_by=<latency 또는 bandwidth>(占位符为 latency 或 bandwidth)、bytes_ratio=<64k 초당 바이트 ÷ 1k 초당 바이트, 소수 첫째 자리>(占位符为 64k 每秒字节数 ÷ 1k 每秒字节数,保留到小数点后第一位)、why=<50자 이상>(占位符为至少 50 个字符)。
如果每秒请求数几乎不变,只有每秒字节数跳高几十倍,就意味着这个对象受限的不是带宽,而是等待的时间。如果只用每秒请求数来看吞吐量,就看不出这个差别。如果生产的响应是 64KB,却用 1KB 来测试,那个测试就一次也没有触及序列化和带宽。
只请求同一个 URL,缓存就替测试通过了
用开启了缓存的对象(CACHE=1 ERR_EVERY=0 DELAY=0.02 SIZE=1024)测量两次。一次用键固定的 'http://127.0.0.1:8080/api/order?id=1'(/root/lt-test-validity/06-fixed.txt,记录 /root/lt-test-validity/06-fixed.log),一次用键轮换的 http://127.0.0.1:8080/api/order/rotate(/root/lt-test-validity/06-rotate.txt,记录 /root/lt-test-validity/06-rotate.log),两次都是 hey -n 200 -c 10。第二次请用 ROTATE_KEYS=500 启动。在 /root/lt-test-validity/06-cache.tsv 中写两行。每行是以制表符分隔的五个字段 <이름> <적중> <빗나감> <적중률> <rps>(占位符依次为名称、命中、未命中、命中率、rps),名称是 fixed 和 rotate,命中率以百分比表示,保留到小数点后第一位,rps 保留到小数点后第二位。然后在 /root/lt-test-validity/06-note.txt 中写三行 representative=<fixed 또는 rotate>(占位符为 fixed 或 rotate)、inflation=<fixed rps ÷ rotate rps, 소수 첫째 자리>(占位符为 fixed 的 rps ÷ rotate 的 rps,保留到小数点后第一位)、why=<50자 이상>(占位符为至少 50 个字符)。
访问日志的第七个字段是 hit 或 miss。/api/order/rotate 每个请求使用不同的键,所以如果键的种类多于请求数,就一次命中也没有。如果不知道生产的命中率,就用固定的键来测试,缓存后面的数据库就会一次负载也没受过,就被部署上去了。
制作判定结果能不能信的检查清单
创建 /root/lt-test-validity/validate.sh。以 bash validate.sh <hey 요약 파일> <접근 기록 파일>(占位符依次为 hey 摘要文件、访问日志文件)调用时,输出三行——status <OK|FAIL> <값>、body <OK|FAIL> <값>、volume <OK|FAIL> <값>(占位符依次为 OK 或 FAIL、值),名称位于行首。status 在摘要中的非 2xx 响应为 0 时为 OK,body 在访问日志中 error 响应体的比例低于 1% 时为 OK,volume 在访问日志的行数与摘要中的响应总数相同时为 OK。只要有一项是 FAIL,就必须以退出码 1 结束,全部为 OK 则以 0 结束。评分器会用它自己制作的四组输入来运行这个脚本,确认通过和失败两种情况。
三项检查要分别运行,最后只能给出一次退出码——如果中途 exit,其余的检查行就不会输出。小数比较用 awk 'BEGIN{exit !(r < 1.0)}' 这样的 awk 退出码来完成。把名称放在行首,是因为读它的不是人,而是流水线。
让第 1 步那个出色的结果通过一下门禁
用制作好的检查清单判定第 1 步的结果。把 bash validate.sh 01-summary.txt 01-access.log 的输出保存到 /root/lt-test-validity/08-gate-out.txt,并在 /root/lt-test-validity/08-verdict.txt 中写四行——gate_exit=<종료 코드>、failed_check=<FAIL 이 난 검사 이름>、trustworthy=<yes 또는 no>、fix=<이 시험을 다시 하려면 무엇을 고쳐야 하는가, 60자 이상>(占位符依次为退出码、出现 FAIL 的检查名称、yes 或 no、要重新做这个测试需要修正什么,至少 60 个字符)。
只看第 1 步的摘要,非 2xx 为 0,响应数也对得上。但如果门禁仍然拦住了,是哪一项检查拦住的,就是本实验的结论。在 fix 中请写下“要让测试验证什么才能修好它”——换工具,或者加上检查响应的层,都可以是答案。