부하 테스트 · 시험이 거짓말하는 자리 · 이론
200 을 받았다는 것은 일을 했다는 뜻이 아니다
한 줄 요약
부하 시험의 숫자는 무엇을 쟀는지 시험 스스로 검증할 때만 뜻이 있다. 상태 코드만 보는 시험은 오류 페이지를 초당 3천 건 찍어 내고도 "통과" 라고 말한다.
왜 이게 필요했나
새 결제 API 의 부하 시험 결과가 회의실에 올라왔다. 초당 3,012건, p95 는 8밀리초, 오류율 0%. 아무도 이의를 달지 않았고 그 주에 배포가 나갔다. 그리고 배포 30분 만에 결제가 멈췄다.
원인은 시험 쪽에 있었다. 부하 발생기가 보낸 요청에는 인증 헤더가 없었고, 게이트웨이는 인증이 없는 요청에 대해 200 과 함께 {"status":"error","message":"upstream unavailable"} 를 돌려주고 있었다. 오래된 프런트엔드가 4xx 를 못 다뤄서 그렇게 만들어 둔 것이었다. 부하 발생기는 상태 코드만 봤으므로 30만 건 전부를 성공으로 셌다.
숫자가 좋았던 것도 같은 이유다. 오류 경로는 데이터베이스도 큐도 타지 않으니 빨리 끝난다. 그래서 오류가 섞일수록 평균과 백분위가 좋아진다. 시험이 망가졌다는 신호가 "느려졌다" 가 아니라 "빨라졌다" 로 오는 것이다. 이것이 부하 시험에서 가장 자주 밟는 지뢰다.
어떻게 동작하나
부하 발생기가 아는 것은 세 가지뿐이다 — 요청을 몇 개 보냈는가, 응답의 상태 코드가 무엇이었는가, 응답이 오기까지 얼마나 걸렸는가. hey 의 요약이 보여 주는 Status code distribution 은 딱 그만큼이다. 본문이 무엇이었는지, 서버가 실제로 일을 했는지는 발생기가 알 수 없다.
그래서 시험에 검증 층을 직접 붙여야 한다. 세 가지를 함께 본다.
| 층 | 무엇을 보는가 | 놓치면 생기는 일 |
| --- | --- | --- |
| 상태 코드 | 2xx 가 아닌 응답이 몇 개인가 | 502 폭풍을 "부하를 견뎠다" 로 읽는다 |
| 본문 | 응답이 실제로 기대한 모양인가 | 200 에 담긴 오류를 성공으로 센다 |
| 응답 수 | 서버가 센 요청 수와 발생기가 센 응답 수가 같은가 | 중간에서 끊긴 요청이 통계에서 사라진다 |
k6 는 이 층을 checks 라는 이름으로 표준 기능에 넣어 두었고, 그 결과를 thresholds 에 걸어 종료 코드로 만들 수 있다. hey 는 그런 기능이 없으므로 사람이 직접 붙여야 한다. 도구를 고르는 기준이 여기에 하나 생긴다 — 응답을 검증할 자리가 있는가.
검증을 붙였다고 끝이 아니다. 시험 조건이 운영을 대표하는지도 따로 물어야 한다. 셋이 특히 자주 어긋난다.
연결 재사용. hey 는 기본으로 TCP 연결을 재사용한다. -disable-keepalive 를 주면 요청마다 새 연결을 연다. 동시성 10 으로 200건을 보내면 앞은 연결 10개, 뒤는 200개다. 운영의 클라이언트가 연결 풀을 쓰는데 시험만 연결을 매번 새로 열면, 시험은 서비스가 아니라 TCP 핸드셰이크와 방화벽 상태표를 재게 된다.
응답 크기. 시험용 응답이 1KB 인데 운영 응답이 64KB 라면, 초당 요청 수는 비슷하게 나와도 초당 바이트는 64배 차이가 난다. 대역폭이나 직렬화가 병목인 서비스에서는 이 차이가 곧 잘못된 용량 산정이 된다. 처리량은 초당 요청 수만이 아니라 초당 바이트로도 봐야 한다.
캐시 열쇠. 같은 URL 만 반복해서 때리면 첫 요청 뒤로는 전부 캐시 적중이다. 적중률 99.5% 인 시험 결과로 적중률 20% 인 운영의 용량을 잡으면, 캐시 뒤의 데이터베이스는 시험에서 한 번도 부하를 받지 않은 채 배포된다.
현장에서 만나는 모습
가장 흔한 모양은 "시험 결과가 운영보다 좋다" 이다. 그리고 그 원인은 거의 언제나 시험이 운영보다 쉬운 일을 했기 때문이다. 인증을 건너뛰었거나, 같은 데이터만 읽었거나, 응답이 작았거나, 오류 경로로 빠졌거나.
반대 방향도 있다. 어떤 팀은 시험 결과가 운영의 3분의 1밖에 안 나와서 서버를 세 배로 늘렸다. 나중에 보니 부하 발생기가 연결 재사용을 꺼 둔 채 돌고 있었다. 늘린 서버 값은 그대로 청구서에 남았다.
셋째 모양은 더 조용하다. 시험이 몇 달 동안 잘 돌다가 어느 날부터 결과가 좋아지는 경우다. 대개 그 사이에 대상 쪽에서 무언가가 바뀌었다 — 인증 방식이 바뀌어 시험용 토큰이 만료됐거나, 경로가 옮겨져 라우터가 기본 응답을 돌려주고 있거나, 기능 깃발이 꺼져 실제 계산이 건너뛰어졌거나. 시험은 그대로인데 대상이 다른 일을 하기 시작한 것이고, 검증 층이 없으면 이 변화는 "성능이 개선됐다" 로 기록된다. 그래서 검증은 한 번 붙이고 끝나는 것이 아니라 시험과 함께 계속 돌아야 한다.
그래서 부하 시험 보고서에는 숫자보다 먼저 적어야 하는 것이 있다. 이 시험이 무엇을 검증했고 무엇을 검증하지 않았는가. 검증하지 않은 것이 하나라도 있으면 그 숫자는 상한도 하한도 아니다. 그리고 그 검증은 사람이 보고서를 읽으며 확인하는 것이 아니라 스크립트의 종료 코드로 남아야 한다 — 사람은 좋은 숫자를 의심하지 않기 때문이다.
다음 실습에서 할 것
상태 코드는 늘 200 이지만 세 번에 한 번은 본문이 오류인 대상을 띄우고, hey 의 요약만 보면 완벽해 보인다는 것을 먼저 확인한다. 그다음 서버가 남긴 접근 기록으로 실제로 무엇이 나갔는지 세고, 오류 응답이 정상 응답보다 빠르다는 것을 중앙값으로 확인한다. 연결 재사용을 껐다 켜며 연결 수가 10개와 200개로 갈리는 것을 보고, 응답 크기를 1KB 와 64KB 로 바꿔 초당 바이트를 비교하고, 같은 열쇠와 도는 열쇠로 캐시 적중률이 99.5% 와 0% 로 갈리는 것을 본다. 마지막으로 이 세 가지를 검사하는 점검표 스크립트를 만들고, 채점기가 자기가 만든 입력으로 그 스크립트를 네 번 돌려 통과와 실패를 모두 확인한다.