목차
새 다리를 개통하기 전에는 트럭을 올려 본다. 서버도 같은 일을 할 수 있는데, 트럭을 어떻게 올리느냐가 답을 통째로 바꾼다.
개통 전에 트럭을 올려본다
다리를 다 지으면 바로 차를 다니게 하지 않는다. 먼저 무거운 트럭을 줄줄이 올려 놓고 얼마나 휘는지 잰다. 설계상 견딘다는 계산과 실제로 견디는 것은 다르기 때문이다.
서버에서 그 일을 하는 것이 부하 테스트다. 아직 오지 않은 트래픽을 인위적으로 만들어서, 어디까지 버티는지를 미리 본다.
왜 미리 보나. 안 보면 한계를 사용자가 대신 찾아 주기 때문이다. 그리고 그 발견은 항상 가장 나쁜 순간에 온다 - 이벤트를 연 날, 기사에 실린 날, 결제가 몰리는 날이다. 그때 알게 되는 값은 똑같은 숫자인데 대가가 다르다.
병목을 찾는 이야기와는 방향이 반대다. 그쪽은 이미 도는 시스템에서 느린 자리를 되짚는 일이고, 이쪽은 아직 아무 일도 안 일어난 시스템에 일부러 부하를 만들어 거는 일이다. 되짚을 대상이 없으니 내가 만들어야 하고, 그래서 만드는 방식이 곧 답의 품질이 된다.
무엇을 알아내려는지부터 정한다
부하 테스트에서 가장 흔한 실패는 도구를 못 다뤄서가 아니다. 무엇을 알고 싶은지 안 정하고 도구부터 켜는 것이다.
그러면 숫자는 나온다. 초당 몇 건, 평균 몇 밀리초, 실패율 몇 퍼센트. 그런데 이 숫자를 받아 들고 할 수 있는 게 없다. 비교할 기준이 없기 때문이다. 초당 400건이 좋은 건지 나쁜 건지는 그 숫자만 봐서는 영영 모른다.
그래서 걸기 전에 문장 하나를 먼저 적는다.
“동시 사용자 O명이 붙었을 때, 주문 API의 p95 응답이 O초 안에 들어오는가.”
이 문장이 있으면 결과가 곧장 판정이 된다. 없으면 결과는 그냥 자료다. 트럭을 올려 보기 전에 **“몇 톤까지 통과시킬 다리인가”**를 정해 놓는 것과 같다. 그게 없으면 트럭을 올려 놓고도 합격인지 불합격인지 말할 수가 없다.
거는 방식이 네 가지다
무엇을 알고 싶은지에 따라 거는 모양이 달라진다. 이름은 도구마다 다르지만 실제로 쓰이는 건 넷이다.
| 방식 | 어떻게 거나 | 무엇을 알아내나 |
|---|---|---|
| 부하 테스트 | 목표한 만큼 걸고 유지한다 | 기대하는 트래픽을 감당하는가 |
| 스트레스 테스트 | 될 때까지 계속 올린다 | 한계가 어디이고 넘으면 어떻게 되는가 |
| 스파이크 테스트 | 한 번에 확 올렸다 내린다 | 갑작스러운 몰림을 흡수하는가 |
| 내구 테스트 | 보통 부하로 오래 건다 | 시간이 지나며 나빠지는 게 있는가 |
넷 중 스트레스 테스트가 “한계는 어디인가”에 답하는 유일한 방식이다. 목표치만 걸어 보고 통과하면 “이만큼은 된다”만 알 뿐, 여유가 얼마나 남았는지는 모른다. 목표의 두 배에서 죽는 시스템과 열 배에서 죽는 시스템은 같은 테스트를 똑같이 통과한다.
내구 테스트는 유독 자주 빠진다. 오래 걸어야 하니 비싸고, 짧게 돌린 것과 결과가 똑같아 보이기 때문이다. 그래서 부하 테스트를 통과하고도 터지는 이유의 상당수가 여기서 나온다.
처리량은 어느 지점부터 안 오른다
부하를 조금씩 올려 가며 재면 눈에 띄는 모양이 하나 나온다.
부하를 더 줘도 처리량은 안 오르는데 응답 시간만 치솟는다. 한계는 죽는 지점이 아니라 둘이 갈라지는 자리다.
처음에는 부하를 올린 만큼 처리량도 오른다. 그런데 어느 지점부터 처리량이 더 안 오른다. 자원이 다 찼거나 어딘가의 상한에 닿은 것이다.
문제는 그 지점부터 응답 시간이 치솟는다는 것이다. 처리하는 양은 그대로인데 못 들어간 요청이 줄을 서기 시작하고, 줄이 길어지면 기다리는 시간이 곧 응답 시간이 된다.
그래서 찾으려는 한계는 서버가 죽는 지점이 아니라 이 갈라지는 지점이다. 죽는 지점만 재면 “1,200명까지 버팁니다”라고 말하게 되는데, 900명일 때 이미 응답이 8초였다면 그 시스템은 900명에서 못 쓰게 된 것이다. 살아 있는 것과 쓸 만한 것은 다르다.
그래서 결과를 볼 때 처리량과 응답 시간을 같은 부하 축 위에 나란히 놓는다. 따로 보면 “처리량이 안 오른다”와 “응답이 느려졌다”가 별개의 문제로 보인다. 실은 같은 사건의 앞뒤다.
가짜 부하는 가짜 답을 준다
여기서부터가 부하 테스트의 진짜 어려움이다. 트럭을 올리긴 올렸는데 그게 진짜 트럭이 아니었던 경우들이다.
숫자는 멀쩡하게 나온다. 에러도 안 난다. 그래서 통과했다고 믿게 되는데, 정작 잰 것이 시스템이 아니었다.
같은 것만 부르면 캐시가 다 받아낸다
부하 스크립트를 만들 때 가장 손쉬운 방법은 요청 하나를 만들어 놓고 그걸 반복하는 것이다. 상품 1번 조회를 초당 500번.
같은 상품만 100번 조회
부하 생성기가 흔히 만드는 모양
1건DB까지 간 요청
서로 다른 상품 100번 조회
실제 사용자가 만드는 모양
78건DB까지 간 요청
요청 수는 두 판이 똑같이 100건이다. 조회 대상만 바꿨는데 DB가 실제로 받은 일이 78배가 된다. 캐시가 다 받아내는 부하 테스트는 캐시를 잰 것이지 시스템을 잰 것이 아니다.
그러면 첫 요청 이후로는 전부 캐시가 받아낸다. DB는 거의 놀고 있고, 응답 시간은 아름답게 나온다. 잰 것은 캐시의 속도다.
실제 사용자는 각자 다른 상품을 본다. 캐시 적중률이 다르면 뒤에 있는 것들이 받는 부하가 통째로 달라진다. 그래서 조회 대상을 실제 분포에 가깝게 흩어야 한다. 완벽하게 맞출 필요는 없고, “다 같은 것”만 아니면 결과가 확 달라진다.
같은 함정이 인증에도 있다. 사용자 한 명으로 전부 걸면 그 사용자의 데이터만 캐시에 올라가고, 사용자별로 갈라지는 자리가 통째로 안 눌린다.
차갑게 시작한 숫자
방금 띄운 서버는 느리다. 커넥션 풀이 안 찼고, 캐시가 비었고, JIT가 아직 기계어를 안 만들었다. 이 상태의 숫자를 그대로 평균에 넣으면 결과가 통째로 나빠진다.
그래서 부하를 걸기 전에 얼마간 돌려 데워 놓고, 그 구간은 결과에서 버린다. 부하 생성 도구들이 “워밍업”을 따로 두는 이유가 이것이다.
반대 방향의 실수도 있다. 워밍업을 너무 길게 잡고 같은 데이터로 데우면, 본 측정에 들어갈 때는 이미 캐시가 다 차 있다. 데우려던 것이 아니라 앞 항목의 함정을 만든 셈이 된다.
부하를 거는 쪽이 먼저 무너진다
노트북 한 대로 초당 수천 건을 만들려고 하면, 서버보다 부하 생성기가 먼저 한계에 닿는다. 이때 나오는 그래프는 서버의 한계처럼 생겼다. 처리량이 눕고 응답 시간이 오른다.
가르는 법은 하나다. 거는 쪽의 자원도 같이 본다. 생성기의 CPU가 포화거나 소켓이 마르거나 네트워크 대역이 찼으면, 그 그래프는 서버 이야기가 아니다.
이건 조용히 틀린 결론을 만든다. 서버 지표만 보면 “CPU도 여유 있는데 처리량이 안 오른다”로 읽혀서, 있지도 않은 병목을 며칠 찾게 된다.
한꺼번에 때리면 큐만 보인다
0명에서 500명으로 순식간에 올리면 시작 순간에 요청이 통째로 쌓인다. 그 뒤의 응답 시간은 전부 그 쌓인 줄을 소화하는 시간이라, 시스템의 성질이 아니라 시작 방식의 성질이다.
그래서 보통은 천천히 올린다. 몇 분에 걸쳐 단계적으로 늘리고, 각 단계에서 안정된 뒤의 구간만 읽는다.
다만 일부러 한꺼번에 때리는 것이 목적일 때도 있다. 티켓 오픈이나 선착순 이벤트는 실제로 그렇게 오기 때문이다. 그게 스파이크 테스트다. 천천히 올리는 것과 확 때리는 것은 틀리고 맞고가 아니라 서로 다른 질문이고, 어느 쪽인지 알고 골라야 한다.
어디서 거느냐가 숫자를 정한다
같은 스크립트를 돌려도 어디에 거느냐에 따라 답이 달라진다.
- 스테이징이 프로덕션보다 작다. 그런데 절반 크기라고 결과가 절반이 되지 않는다. 캐시가 다 안 들어가는 순간 성질이 통째로 바뀌고, DB 데이터가 적으면 인덱스가 훨씬 잘 먹는다
- 외부 의존이 목으로 바뀌어 있다. 결제사·배송사를 가짜로 세워 두면 그 응답은 항상 1밀리초다. 실제로는 그 호출이 가장 느린 구간일 수 있다
- 같은 망 안에서 건다. 사용자는 이동 통신망을 타고 오는데 테스트는 옆 서버에서 온다
이걸 다 맞출 수는 없다. 그래서 목표를 **“프로덕션을 똑같이 재현한다”가 아니라 “무엇이 다른지 알고 그만큼 깎아서 읽는다”**로 잡는다. 스테이징이 프로덕션의 절반이면 결과를 절반으로 나누는 게 아니라, **“이 숫자는 이 조건에서 나온 것”**이라고 조건을 함께 적어 둔다.
프로덕션에 직접 거는 선택지도 있다. 가장 정확하지만 실제 사용자와 같은 자원을 나눠 쓰게 되므로 트래픽이 적은 시간대에 하고, 언제든 멈출 수 있게 해 두고, 만들어진 주문·회원 같은 데이터를 어떻게 걷어낼지를 먼저 정한다.
여기서 다리 비유가 깨진다
비유를 여기까지 끌고 왔으니 안 맞는 자리를 밝혀 둔다. 다리는 트럭을 올려도 부서지지 않는다. 하중 시험은 설계 한계 안에서 하고, 다리는 시험이 끝나면 원래대로 돌아온다.
서버는 그렇지 않다. 한계를 넘기면 실제로 죽고, 죽는 방식이 지저분하다. 커넥션이 남고, 처리하다 만 데이터가 남고, 재시작해도 쌓인 큐가 다시 밀어 넣는다. 그래서 부하 테스트는 다리처럼 실물에 대고 하지 못하고, 따로 세운 환경에서 한다. 바로 앞 절이 통째로 그 대가에 대한 이야기였다.
한 가지가 더 다르다. 다리는 한 번 지으면 그대로지만 서버는 매주 바뀐다. 지난달 시험 결과는 지난달 코드의 것이다. 이 차이가 다음 이야기로 이어진다.
실무에서: 언제 걸고 무엇을 남기나
부하 테스트가 실무에서 자리를 못 잡는 이유는 대개 한 번 돌리고 끝나기 때문이다. 결과는 문서에 남고, 코드는 계속 바뀌고, 반년 뒤 그 문서는 아무 말도 안 한다.
그래서 실제로 값이 나는 형태는 셋이다.
- 기준선을 만든다. 지금 이 코드로 잰 값을 적어 둔다. 절대값이 아니라 다음번과 비교할 기준으로 쓰려는 것이다
- 바뀔 때마다 같은 조건으로 다시 잰다. 배포 파이프라인에 붙여 두면 “이번 변경으로 처리량이 30% 떨어졌다”를 배포 전에 안다. 매일 돌릴 필요는 없고, 성능이 걸린 변경에서만 돌려도 값이 난다
- 결과에 조건을 같이 적는다. 어떤 환경, 어떤 데이터 크기, 어떤 시나리오였는지. 이게 없으면 다음 사람이 그 숫자를 비교할 수가 없다
목표는 “우리 서비스는 초당 몇 건을 처리한다”는 문장을 갖는 게 아니다. 트래픽이 두 배가 될 때 무엇이 먼저 막히는지 미리 알고, 그때 무엇을 손댈지 정해 두는 것이다. 숫자 하나보다 이 판단이 오래 간다.
정리
- 부하 테스트는 아직 오지 않은 트래픽을 만들어 한계를 미리 보는 일이다. 안 하면 사용자가 대신 찾아 준다.
- 무엇을 알고 싶은지 문장으로 먼저 적는다. 없으면 나온 숫자가 합격인지 불합격인지 말할 수 없다.
- 찾으려는 한계는 죽는 지점이 아니라 처리량이 눕고 응답 시간이 치솟는 갈라지는 지점이다.
- 거는 방식이 답을 정한다. 같은 데이터만 부르면 캐시를, 차갑게 재면 워밍업을, 한꺼번에 때리면 큐를 잰 것이 된다.
- 부하를 거는 쪽이 먼저 무너질 수 있다. 생성기의 자원도 같이 봐야 있지도 않은 병목을 안 찾는다.
- 환경 차이는 못 없앤다. 없애는 대신 무엇이 다른지 알고 조건을 함께 적어 둔다.
다음 글은 반대편을 본다. 잘 걸었고 통과까지 했는데도 프로덕션에서 터지는 경우다. 부하 테스트가 구조적으로 못 보는 것이 있다.