목차
하중 시험을 통과한 다리가 몇 년 뒤에 무너지는 일이 있다. 시험이 엉터리여서가 아니라 시험으로 볼 수 없는 것이 있어서다.
시험을 통과한 다리도 무너진다
트럭을 올려 봤고, 계산대로 휘었고, 합격 도장을 받았다. 그런데 개통하고 몇 년이 지나 무너진다.
원인은 대개 하중 시험이 못 본 것이다. 시험은 트럭을 하루 올렸는데 실제 다리는 십 년 동안 진동을 받는다. 시험은 무게를 봤는데 실제로는 소금과 습기가 철근을 삭힌다. 시험이 틀린 게 아니라 시험의 창이 좁았던 것이다.
부하 테스트도 정확히 같은 구조를 갖는다. 부하를 제대로 거는 법을 다 지켜도, 그래서 결과가 진짜여도, 여전히 못 보는 것들이 남는다. 이 글은 그 목록이다.
통과가 말하는 것과 말하지 않는 것
먼저 통과라는 결과가 정확히 무슨 뜻인지 좁혀 두면 나머지가 쉬워진다.
| 통과가 말하는 것 | 통과가 말하지 않는 것 |
|---|---|
| 그 환경에서 | 프로덕션에서도 |
| 그 시나리오로 | 실제 사용자가 하는 모든 일에 대해 |
| 그 데이터 크기로 | 반년 뒤 커진 데이터로 |
| 그 시점의 코드가 | 오늘 배포한 코드가 |
| 그 시간 동안 | 계속 돌아도 |
왼쪽 다섯 개가 전부 조건이다. 조건이 하나라도 어긋나면 결과는 그만큼 약해진다. 그런데 결과지에는 “통과”라고만 적히니, 시간이 지날수록 조건은 잊히고 판정만 남는다.
그래서 부하 테스트 결과는 **“안전하다”가 아니라 “이 조건에서 여기까지는 됐다”**로 읽어야 한다. 아래 다섯 절은 그 조건이 어긋나는 대표적인 자리들이다.
30분으로는 안 보이고 30일이면 보인다
가장 크게 벌어지는 자리는 시간이다. 부하 테스트가 보는 창은 길어야 몇십 분인데, 어떤 고장은 그보다 훨씬 느리게 자란다.
회수될 때마다 내려가지만 바닥이 조금씩 높아진다. 30분 안에서는 그 기울기가 보이지 않아 완벽하게 정상으로 읽히고, 몇 주를 이어 봐야 드러난다.
메모리가 조금씩 새는 상황이 딱 이 모양이다. 회수될 때마다 내려가긴 하는데 바닥이 매번 조금씩 높아진다. 30분 안에서는 그 기울기가 너무 완만해서 완벽하게 정상으로 읽힌다. 그리고 몇 주 뒤 새벽에 죽는다.
같은 방식으로 자라는 것이 몇 가지 더 있다.
- 커넥션과 파일 핸들 - 어떤 예외 경로에서만 안 닫으면, 그 경로가 드물수록 늦게 터진다
- 디스크 - 로그와 임시 파일이 쌓인다. 부하 테스트 30분이 만드는 양은 며칠 치의 몇백분의 일이다
- 캐시 - 만료를 안 걸어 둔 항목이 계속 쌓여 메모리를 먹는다
GC가 있는 언어에도 누수는 있다. 아무도 안 쓰는데 어딘가에서 참조를 붙잡고 있으면 회수 대상이 아니다. 이런 누수는 코드를 읽어서는 잘 안 보이고, 오래 걸어 보는 것 말고는 드러낼 방법이 마땅치 않다.
그래서 내구 테스트는 다른 방식들과 성격이 다르다. 몇 시간에서 며칠을 그냥 돌려 두는 일이라 비싸고 지루한데, 그 지루한 시간이 유일하게 이 부류를 잡는다.
사용자는 고르게 오지 않는다
부하 테스트는 대개 고른 부하를 만든다. 초당 500건이면 매초 500건씩. 실제 사용자는 그렇게 오지 않는다.
몰림은 세 가지 모양으로 온다.
- 시간에 몰린다. 점심시간, 퇴근길, 광고가 나간 직후. 하루 평균으로는 여유가 넘치는데 그 10분에 무너진다
- 대상에 몰린다. 모든 상품이 고르게 팔리지 않는다. 한 상품에 주문이 쏠리면 그 행 하나에 잠금이 몰리고, 나머지가 아무리 한가해도 거기서 막힌다
- 같은 순간에 겹친다. 캐시 항목이 한꺼번에 만료되면 그 순간 모두가 DB로 간다. 평소 캐시가 받아내던 부하가 통째로 뒤로 넘어간다
셋 다 평균으로는 안 보인다. 그리고 부하 테스트가 만드는 것은 대개 평균이다. 그래서 부하 테스트가 통과한 시스템이 이벤트 시작 5초 만에 죽는다.
캐시가 한꺼번에 만료되는 문제는 만료 시간을 조금씩 흩어 주는 것으로 상당 부분 눌린다. 다만 부하 테스트로 이 문제를 발견하기는 어렵다 - 만료가 겹치는 상황을 일부러 만들지 않으면 재현이 안 된다.
데이터가 작았다
테스트 환경의 DB에는 대개 몇만 건이 들어 있다. 프로덕션에는 몇억 건이 들어 있다.
이 차이는 성능을 비례해서 나쁘게 만드는 게 아니라 성질을 바꾼다.
- 데이터가 작으면 인덱스와 데이터가 통째로 메모리에 들어간다. 디스크를 거의 안 읽으니 인덱스가 왜 빠른지를 확인할 기회조차 없다. 커지면 그때부터 디스크 읽기가 시작된다
- 데이터가 작으면 인덱스를 안 타도 빠르다. 1만 건 전체를 훑는 것과 1억 건 전체를 훑는 것은 완전히 다른 일인데, 테스트에서는 둘 다 통과한다
- 정렬·집계처럼 양에 비례해 무거워지는 연산은 작은 데이터에서 거의 공짜로 보인다
그래서 부하 테스트에서 놓쳤다가 나중에 터지는 쿼리의 상당수가 처음부터 잘못 짠 쿼리다. 잘못된 게 그때는 안 아팠을 뿐이다.
대응은 명확하지만 손이 간다. 테스트 데이터를 실제 규모에 맞춰 부풀린다. 다 채울 수 없으면 최소한 가장 큰 테이블 한둘만이라도 실제 크기에 맞춘다. 여기서 다시 어디서 거느냐가 숫자를 정한다는 이야기가 값을 한다 - 다 못 맞추면 무엇이 다른지를 조건으로 적어 둔다.
목으로 세운 것은 절대 안 느려진다
외부 결제사, 배송 조회, 알림 발송. 부하 테스트에서 이것들을 실제로 호출할 수는 없다. 남의 시스템에 우리 부하를 걸 수는 없기 때문이다.
그래서 가짜로 세운다. 그리고 가짜는 항상 1밀리초 안에 성공한다.
실제로는 그 호출이 200밀리초 걸리고, 가끔 3초 걸리고, 어떤 날은 아예 응답을 안 한다. 그 시간 동안 우리 쪽 스레드는 잡혀 있다. 외부 하나가 느려지면 우리 서버의 처리량이 통째로 떨어지는데, 목을 세운 테스트에서는 이 연결이 아예 존재하지 않는다.
이건 완전히 없앨 수 없는 한계다. 대신 줄일 수는 있다.
- 가짜에 실제 지연을 넣는다. 평소 걸리는 시간만큼 일부러 기다리게 한다. 이 한 줄만으로도 결과가 크게 달라진다
- 느려지는 경우를 따로 건다. 외부 응답을 5초로 고정해 놓고 걸면, 우리 쪽이 그때 무엇을 하는지가 드러난다
- 타임아웃이 실제로 걸리는지 본다. 설정만 해 두고 안 먹는 경우가 흔하다
통과한 뒤에 배포한 코드
마지막 자리는 가장 단순하다. 부하 테스트 결과는 잰 그 순간의 코드에 대한 것이다.
그 뒤로 배포가 스무 번 나갔다면, 그 결과지는 이제 존재하지 않는 시스템을 설명하고 있다. 그런데 문서에는 여전히 “초당 800건 처리 확인”이라고 적혀 있고, 아무도 그 날짜를 안 본다.
성능이 나빠지는 변경은 대개 티가 안 난다. 조회 하나를 반복문 안으로 옮기거나, 인덱스를 안 타는 조건이 하나 붙거나, 응답에 필드가 몇 개 늘어난다. 기능은 멀쩡히 동작하니 리뷰에서도 안 걸린다. 부하를 걸어 보기 전까지는 아무도 모른다.
그래서 한 번 잘 재는 것보다 같은 조건으로 반복해서 재는 것이 오래 간다. 절대값이 아니라 지난번 대비 얼마나 나빠졌나를 보는 것이고, 이건 자동으로 돌 때만 실제로 지켜진다.
여기서 다리 비유가 다시 깨진다
이 비유를 여기까지 끌고 왔는데, 마지막에 안 맞는 자리가 둘 남는다.
다리는 무너지기 전에 균열이 보인다. 정기 점검에서 눈으로 찾을 수 있고, 못 찾아도 소리가 나고 처짐이 생긴다. 서버는 그런 게 없다. 지표를 안 남겨 두면 아무 신호 없이 죽는다. 메모리가 3주 동안 차오르는 동안 화면에는 아무 일도 안 일어난다.
그리고 다리에 오르는 하중은 예측 가능하다. 차량 무게는 법으로 정해져 있고 통행량은 통계로 잡힌다. 서비스 트래픽은 하루아침에 백 배가 될 수 있다. 그래서 다리는 “설계 하중 안에서 쓴다”가 성립하지만 서버는 설계 하중을 넘는 날이 온다는 전제로 만들어야 한다.
이 둘이 같은 결론을 가리킨다. 미리 재는 것만으로는 부족하고, 도는 동안 계속 봐야 한다.
실무에서: 미리 재는 것과 계속 재는 것을 같이 둔다
그래서 부하 테스트의 자리는 “이걸로 안전을 확인한다”가 아니다. 두 가지를 짝으로 두는 것이다.
| 부하 테스트 | 관측 | |
|---|---|---|
| 언제 | 배포 전, 정해진 조건에서 | 도는 내내 |
| 무엇을 | 아직 안 온 부하 | 실제로 온 부하 |
| 강점 | 사고 전에 안다 | 조건이 진짜다 |
| 약점 | 조건이 가짜다 | 이미 겪은 뒤다 |
약점이 정확히 서로 반대다. 그래서 하나로 다른 하나를 대신할 수 없다. 관측가능성이 붙어 있으면 부하 테스트가 못 본 것이 프로덕션에서 신호로 먼저 나타난다. 반대로 부하 테스트가 있으면 그 신호를 사용자가 아니라 우리가 먼저 만든다.
실제로 붙이는 순서는 이렇다.
- 관측을 먼저 붙인다. 부하 테스트를 아무리 잘 짜도 안 보이는 게 남는데, 관측이 없으면 그게 나타나도 모른다
- 부하 테스트에서 본 한계를 알림 기준으로 옮긴다. 갈라지는 지점이 초당 700건이었다면 500건에서 알림을 건다. 재 놓고 안 옮기면 그 숫자는 문서에만 남는다
- 터진 뒤에는 시나리오를 늘린다. 사고 하나가 곧 “다음부터 걸어 볼 부하” 하나다. 이렇게 해야 테스트가 실제 사용자를 따라간다
정리
- 부하 테스트 결과는 **“안전하다”가 아니라 “이 조건에서 여기까지는 됐다”**로 읽는다.
- 시간이 가장 크게 벌어진다. 누수·디스크처럼 몇 주에 걸쳐 자라는 것은 30분 안에서 완벽하게 정상으로 보인다.
- 사용자는 고르게 오지 않는다. 시간·대상·만료 시점에 몰리는데, 고른 부하로는 이게 안 만들어진다.
- 데이터 크기는 성능을 비례해 나쁘게 하는 게 아니라 성질을 바꾼다. 작은 데이터에서는 잘못 짠 쿼리도 통과한다.
- 목으로 세운 외부 의존은 절대 안 느려진다. 최소한 실제 지연만큼은 넣어 둔다.
- 결과는 잰 그 시점 코드의 것이다. 한 번 잘 재는 것보다 같은 조건으로 반복해 재는 것이 오래 간다.
- 미리 재는 것과 도는 동안 계속 보는 것은 약점이 정반대라 서로를 대신하지 못한다. 둘을 짝으로 둔다.