초당 3천 건이 나왔는데 전부 오류 페이지였다
목표
상태 코드는 늘 200 인데 본문은 오류인 대상을 부하 시험해 보고, 요약만 보는 시험이 무엇을 놓치는지 직접 세어 확인합니다. 연결 재사용·응답 크기·캐시 열쇠를 바꿔 가며 시험 조건이 운영을 대표하는지 판단하고, 마지막에 이 결과를 믿어도 되는지 판정하는 점검표 스크립트를 만들어 종료 코드로 게이트를 세웁니다.
왜 중요한가
부하 시험 보고서에서 가장 위험한 문장은 '오류율 0%' 다. 부하 발생기가 아는 것은 상태 코드뿐이고, 200 안에 담긴 오류는 그 눈에 보이지 않는다. 게다가 오류 경로는 일을 하지 않으므로 빨리 끝나서, 시험이 망가질수록 지연 숫자가 좋아진다. 그래서 검증이 없는 부하 시험은 틀린 답이 아니라 아무 답도 아니다. 검증을 붙인 뒤에도 질문이 하나 더 남는다 — 이 시험 조건이 운영을 대표하는가. 연결을 매번 새로 열었다면 TCP 핸드셰이크를 잰 것이고, 응답이 운영의 64분의 1이었다면 직렬화와 대역폭을 한 번도 건드리지 않은 것이고, 같은 URL 만 때렸다면 캐시가 시험을 대신 통과한 것이다. 이 판단을 사람의 눈에 맡기지 않고 스크립트의 종료 코드로 못박는 것이 이 실습의 마지막 단계다.
단계
/opt/lab/lt/lt-test-validity/app.py을DELAY=0.02 ERR_EVERY=3 SIZE=1024로 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>와effect=<이 성질이 시험 결과를 어느 쪽으로 밀어내는가, 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 는 소수 둘째 자리까지입니다. 연결 수는 접근 기록의 둘째 칸에 나오는 서로 다른 값의 개수입니다. 그리고/root/lt-test-validity/04-note.txt에use_run=<on 또는 off>·conn_ratio=<off 연결 수 ÷ on 연결 수, 소수 첫째 자리>·why=<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> <초당 바이트>이고 이름은1k와64k, rps 는 소수 둘째 자리까지, 초당 바이트는 요청당 바이트 × rps 를 반올림한 정수입니다. 요청당 바이트와 rps 는 요약의Size/request와Requests/sec에서 읽습니다. 그리고/root/lt-test-validity/05-note.txt에bound_by=<latency 또는 bandwidth>·bytes_ratio=<64k 초당 바이트 ÷ 1k 초당 바이트, 소수 첫째 자리>·why=<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>이고 이름은fixed와rotate, 적중률은 백분율로 소수 첫째 자리까지, rps 는 소수 둘째 자리까지입니다. 그리고/root/lt-test-validity/06-note.txt에representative=<fixed 또는 rotate>·inflation=<fixed rps ÷ rotate rps, 소수 첫째 자리>·why=<50자 이상>세 줄을 적으세요. /root/lt-test-validity/validate.sh를 만드세요.bash validate.sh <hey 요약 파일> <접근 기록 파일>로 부르면 세 줄을 출력합니다 —status <OK|FAIL> <값>·body <OK|FAIL> <값>·volume <OK|FAIL> <값>이고 이름이 줄 맨 앞에 옵니다. status 는 요약의 비-2xx 응답이 0 일 때, body 는 접근 기록의 error 본문 비율이 1% 미만일 때, 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자 이상>입니다.
참고
- 작업 디렉터리는
/root/lt-test-validity입니다. 없으면 먼저 만드세요. - 부하 대상은
/opt/lab/lt/lt-test-validity/app.py입니다. 파일 첫머리의 주석에 환경 변수와 경로, 접근 기록의 여덟 칸이 적혀 있습니다. - 이 파드에서는 컨테이너를 띄울 수 없습니다(seccomp). 대상은 파이썬 표준 라이브러리 서버를
127.0.0.1에 직접 띄워 씁니다. 다시 띄우기 전에pkill -f 'lt-test-validity/app.py'로 앞 판을 죽이세요. nproc은 파드가 아니라 노드의 코어 수를 말합니다.hey가 안내하는 기본-cpus값도 마찬가지입니다 — 그래서 이 실습의 대상은 CPU 가 아니라time.sleep()으로 시간을 씁니다.- 흔한 실수: 두 번째 실행 전에 대상을 다시 띄우지 않아 접근 기록이 앞 실행과 섞이는 것.
- 흔한 실수:
-disable-compression과-disable-keepalive를 섞어 쓰는 것. 앞은 압축을, 뒤는 연결 재사용을 끕니다. - hey (rakyll/hey) · k6 checks · k6 thresholds · http.server · vegeta
요약만 보면 이 시험은 완벽하다
/opt/lab/lt/lt-test-validity/app.py 을 DELAY=0.02 ERR_EVERY=3 SIZE=1024 로 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> 와 effect=<이 성질이 시험 결과를 어느 쪽으로 밀어내는가, 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 는 소수 둘째 자리까지입니다. 연결 수는 접근 기록의 둘째 칸에 나오는 서로 다른 값의 개수입니다. 그리고 /root/lt-test-validity/04-note.txt 에 use_run=<on 또는 off> · conn_ratio=<off 연결 수 ÷ on 연결 수, 소수 첫째 자리> · why=<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> <초당 바이트> 이고 이름은 1k 와 64k, rps 는 소수 둘째 자리까지, 초당 바이트는 요청당 바이트 × rps 를 반올림한 정수입니다. 요청당 바이트와 rps 는 요약의 Size/request 와 Requests/sec 에서 읽습니다. 그리고 /root/lt-test-validity/05-note.txt 에 bound_by=<latency 또는 bandwidth> · bytes_ratio=<64k 초당 바이트 ÷ 1k 초당 바이트, 소수 첫째 자리> · why=<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> 이고 이름은 fixed 와 rotate, 적중률은 백분율로 소수 첫째 자리까지, rps 는 소수 둘째 자리까지입니다. 그리고 /root/lt-test-validity/06-note.txt 에 representative=<fixed 또는 rotate> · inflation=<fixed rps ÷ rotate rps, 소수 첫째 자리> · why=<50자 이상> 세 줄을 적으세요.
접근 기록의 일곱째 칸이 hit 또는 miss 입니다. /api/order/rotate 는 요청마다 다른 열쇠를 쓰므로 열쇠 가짓수가 요청 수보다 많으면 적중이 하나도 없습니다. 운영의 적중률을 모른 채 고정 열쇠로 시험하면, 캐시 뒤의 데이터베이스는 한 번도 부하를 받지 않은 채 배포됩니다.
믿어도 되는 결과인지 판정하는 점검표를 만든다
/root/lt-test-validity/validate.sh 를 만드세요. bash validate.sh <hey 요약 파일> <접근 기록 파일> 로 부르면 세 줄을 출력합니다 — status <OK|FAIL> <값> · body <OK|FAIL> <값> · volume <OK|FAIL> <값> 이고 이름이 줄 맨 앞에 옵니다. status 는 요약의 비-2xx 응답이 0 일 때, body 는 접근 기록의 error 본문 비율이 1% 미만일 때, 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자 이상> 입니다.
1단계의 요약만 보면 비-2xx 는 0 이고 응답 수도 맞습니다. 그런데도 게이트가 막는다면 어느 검사가 막았는지가 이 실습의 결론입니다. fix 에는 '무엇을 검증하도록 시험을 고칠 것인가' 를 적으세요 — 도구를 바꾸는 것도, 응답을 검사하는 층을 붙이는 것도 답이 될 수 있습니다.