TPS 초과로 인한 서버 다운
선상에서 수집된 데이터를 육상 데이터 레이크로 재전송하는 파이프라인을 구성하던 도중, 임계 테스트를 진행해야 하는 상황이 생겼습니다. 선상 파드가 보관하고 있던 메시지를 육상의 데이터 레이크로 재전송하기 위해서는 EMQX와 CCMD라는 두 개의 중계 구간을 반드시 통과해야 했는데, 이 구간에는 초당 처리할 수 있는 메시지 수, 즉 TPS 제한이 걸려 있었기 때문입니다.
여러 선상에서 통신 환경이 불안정해 데이터가 도달하지 못하는 일이 잦고, 데이터 레이크에서 배치를 통해 받지 못한 데이터들을 한꺼번에 재요청하도록 설계되었습니다. 평상시 트래픽만 놓고 보면 제한 TPS에 한참 못 미치지만, 배치 로직이 도는 짧은 순간에는 그 몇 배에 달하는 메시지가 동시에 밀려 들어갑니다. 평균 유입량을 기준으로 설계된 파이프라인이 이 순간을 버티지 못하면, 중계 구간의 서버는 다운되어 메시지는 에러 하나 없이 조용히 유실됩니다.
결국 이 상황에서 필요했던 것은 “이 파이프라인이 초당 몇 건까지 처리할 수 있는가”가 아니라, “순간적으로 몰아쳤을 때 어디서 먼저 무너지고, 얼마 만에 회복하는가”에 대한 답이었습니다. 일정한 부하를 오래 유지하는 일반적인 부하 테스트로는 이 질문에 답할 수 없었고, 그래서 선택하게 된 것이 스파이크 테스트(Spike Test)였습니다.
그래서 스파이크 테스트란 무엇인가
본격적인 테스트 설계를 살펴보기 전에, 스파이크 테스트가 다른 부하 테스트와 무엇이 다른지 짚고 넘어갈 필요가 있습니다. 부하 테스트는 부하를 어떤 모양으로 주느냐에 따라 크게 네 가지로 나뉩니다. Load Test는 예상되는 평상시 부하를 일정하게 유지하며 정상 처리 여부를 확인하고, Stress Test는 부하를 서서히 끌어올려 시스템이 무너지는 임계점을 찾습니다. Soak Test는 낮은 부하를 장시간 유지하며 메모리 누수나 리소스 고갈처럼 시간이 지나야 드러나는 문제를 확인합니다.
이에 반해 Spike Test는 짧은 시간 안에 부하를 급격히 끌어올렸다가 다시 급격히 떨어뜨리는 형태의 테스트입니다. 관심사가 최대 처리량 자체가 아니라 급변에 대한 반응과 회복력에 있다는 점이 다른 기법과의 결정적인 차이입니다. 부하가 치솟는 순간 큐가 얼마나 적체되는지, 에러율과 유실이 어느 지점부터 발생하는지, 그리고 부하가 사라진 뒤 원래 상태로 돌아오는 데 얼마나 걸리는지를 봅니다.
재발행 트래픽은 정의상 스파이크의 모양을 하고 있습니다. 평소에는 낮게 깔려 있다가 재연결 시점에만 수직으로 치솟고, 밀린 메시지를 모두 흘려보내면 다시 원래 수준으로 떨어집니다. 실제 트래픽이 이런 모양이라면, 테스트도 같은 모양이어야 의미 있는 결과를 얻을 수 있습니다.
재발행 경로와 병목 지점
테스트 대상이 된 경로는 선상 파드에서 시작해 EMQX와 CCMD를 거쳐 육상 데이터 레이크로 이어지는 구간입니다. 선상 파드가 보관 중이던 메시지를 EMQX로 발행하면, CCMD가 이를 받아 육상 구간으로 중계하고, 최종적으로 데이터 레이크에 적재되는 구조입니다. 여러 구간이 직렬로 연결된 파이프라인에서 전체 처리량을 결정하는 것은 평균이 아니라 가장 낮은 상한을 가진 구간이며, 이 경로에서는 TPS 제한이 걸려 있는 중계 구간이 그 역할을 합니다.
제한 TPS를 넘어섰을 때 나타나는 양상도 미리 정리해 둘 필요가 있었습니다. 초과분이 스로틀링되어 지연이 누적되는 경우, queue에 계속 쌓이다 백프레셔가 걸리는 경우, 그리고 최악의 경우 연결이 끊기거나 메시지가 드롭되는 경우까지 나타날 수 있습니다. 특히 곤란한 것은 발행 측에서는 정상적으로 전송한 것으로 기록되지만 실제로는 데이터 레이크까지 도달하지 못하는 경우입니다. 이런 유실은 발행 측 로그만 봐서는 절대 드러나지 않기 때문에, 발행 건수와 최종 수신 건수를 대조하는 검증이 반드시 함께 필요했습니다.
테스트 설계
스파이크 테스트에서 확인하고자 했던 것은 세 가지였습니다. 첫째, 제한 TPS를 넘어서는 순간 어느 구간에서 어떤 방식으로 문제가 드러나는지. 둘째, 유실 없이 통과할 수 있는 실질적인 상한이 어디인지. 셋째, 스파이크가 지나간 뒤 적체가 해소되어 정상 상태로 돌아오기까지 걸리는 회복 시간이 얼마인지입니다.
부하 곡선은 실제 재연결 상황을 그대로 본떠 구성했습니다. 평상시 수준의 baseline을 잠시 유지한 뒤 짧은 시간 안에 목표 TPS의 몇 배까지 수직으로 끌어올리고, 그 상태를 일정 시간 유지한 다음 다시 baseline으로 급격히 떨어뜨려 회복 과정을 관찰하는 형태입니다. 이때 상승 구간을 길게 잡으면 부하가 완만하게 올라가 사실상 Stress Test가 되어버리므로, 상승 구간은 의도적으로 짧게 유지하는 것이 중요합니다.
관측 지표는 구간별로 나누어 수집했습니다. 발행 측에서는 실제 발행 TPS와 성공·실패 응답을, 중계 구간에서는 EMQX의 inflight 및 queued 메시지 수와 CCMD의 처리 지연 및 에러율을, 수신 측에서는 데이터 레이크의 수신 건수를 확인했습니다. 여기에 발행 시점 타임스탬프와 시퀀스 번호를 메시지에 함께 실어, 종단 간 지연률과 유실률을 동시에 산출할 수 있도록 했습니다.
스파이크 테스트 수행 방법
스파이크 테스트는 k6, JMeter, Gatling과 같은 부하 테스트 도구로 수행할 수 있습니다. 다만 TPS 제한을 검증하는 것이 목적이라면, 가상 사용자 수가 아니라 초당 요청 수를 직접 제어할 수 있는 도구를 고르는 것이 중요합니다. 가상 사용자 수 기반으로 부하를 주면 응답이 느려질수록 실제 발행량이 함께 줄어들어, 정작 확인하고 싶은 초과 구간이 만들어지지 않기 때문입니다.
k6를 사용한다면 ramping-arrival-rate executor가 이 목적에 가장 적합합니다. stages로 구간별 목표 TPS를 직접 지정할 수 있어 baseline과 스파이크 구간을 원하는 모양으로 그릴 수 있고, preAllocatedVUs와 maxVUs를 충분히 확보해두면 부하가 치솟는 순간 가상 사용자를 새로 만드느라 실제 상승이 뭉개지는 현상을 막을 수 있습니다. JMeter를 사용하는 경우에는 Ultimate Thread Group과 Constant Throughput Timer를 조합해 같은 형태의 곡선을 만들 수 있습니다.
테스트를 수행할 때 주의할 점은 부하 발생기 자체가 병목이 되지 않도록 하는 것입니다. 부하 발생기를 대상 시스템과 같은 노드에서 실행하거나 커넥션 풀이 부족한 상태로 두면, 중계 구간이 아니라 테스트 도구가 먼저 한계에 부딪혀 실제와 다른 결과를 보게 됩니다. 또한 스파이크가 끝난 직후 곧바로 테스트를 종료하지 말고 baseline 상태를 몇 분간 더 유지해야, 적체된 메시지가 해소되는 회복 과정까지 관측할 수 있습니다.
스파이크 구간에서 드러난 재발행 로직의 문제
재연결 이후 일부 메시지가 유실되던 문제 상황에서 설계한 시나리오로 스파이크 테스트를 수행해보니, 제한 TPS를 넘어서는 구간부터 발행 측에는 정상 전송으로 기록되지만 데이터 레이크에는 끝내 도달하지 않는 메시지가 발생한다는 사실을 확인했습니다. 시퀀스 번호를 대조해보니 유실은 스파이크 구간 중에서도 특정 시점 이후에 집중적으로 나타나고 있었습니다.
원래라면 중계 구간이 감당할 수 있는 속도에 맞춰 흘러가야 할 메시지가 왜 한꺼번에 몰리는지 확인하기 위해 재발행 로직을 살펴보다, 보관 중이던 메시지를 별도의 속도 제어 없이 순차 루프로 즉시 전량 발행하고 있음을 발견하게 되었습니다. 더 문제가 됐던 것은 실패한 메시지를 곧바로 재시도하도록 되어 있어, 이미 포화 상태인 중계 구간에 재시도 트래픽까지 얹혀 상황을 스스로 악화시키고 있다는 점이었습니다.
결국 통신이 복구되는 순간 쌓여 있던 메시지가 제한 TPS의 몇 배에 달하는 속도로 쏟아지면서 중계 구간이 포화되었고, 스로틀링으로 인한 지연과 즉시 재시도가 맞물려 적체가 눈덩이처럼 불어나다 일부 메시지가 유실되는 상황으로 이어진 것이었습니다.
다행히 이러한 사실을 스파이크 테스트 과정을 통해 발견할 수 있었고, 재발행 구간에 토큰 버킷 기반의 속도 제한을 두어 제한 TPS 아래에서 메시지가 균일한 속도로 흘러가도록 개선하고, 실패 시에는 지수 백오프로 재시도하도록 변경해 순간적인 폭주가 중계 구간을 무너뜨리는 상황을 방지할 수 있었습니다. 개선 이후 동일한 시나리오로 다시 테스트했을 때는 유실 없이 전량이 데이터 레이크에 적재되었고, 스파이크가 지나간 뒤 적체가 해소되기까지 걸리는 시간도 예측 가능한 범위 안으로 들어왔습니다.
마치며
스파이크 테스트는 “이 시스템이 얼마나 빠른가”가 아니라 “예상치 못한 순간을 어떻게 견디고, 얼마나 빨리 제자리로 돌아오는가”를 확인하는 도구입니다. 평균 트래픽만 바라보고 설계한 시스템은 대체로 평상시에는 아무 문제 없이 동작하기 때문에, 순간의 폭주에서 드러나는 취약점은 운영 중 장애로 마주하기 전까지 좀처럼 모습을 드러내지 않습니다.
재발행이나 재시도처럼 태생적으로 버스트를 만들어내는 로직을 가지고 있고, 그 앞에 TPS 제한을 가진 구간이 놓여 있는 시스템이라면, 기능 검증을 마친 시점에 반드시 한 번은 꺼내 봐야 하는 도구로 생각하면 좋을 것 같습니다.
jungboke