사용자가 몰리면 서버는 어디부터 느려질까?
요청이 서버와 데이터베이스를 지나가는 길을 따라가며 병목이 생기는 이유와 서버를 무작정 늘리면 해결되지 않는 이유를 쉽게 설명한다.
대용량 트래픽 시리즈 2/7
서비스는 어떻게 100만 사용자를 버틸까? — 대용량 트래픽과 데이터베이스를 공부하며 이해한 것들
1편에서는 회원 100만 명과 동시 접속자 100만 명이 다른 말이며, 서버가 실제로 처리하는 것은 사용자가 만든 요청이라는 점을 살펴봤습니다.
이번에는 그 요청이 한꺼번에 들어오면 어디에서 줄이 생기는지 따라가 보겠습니다.
점심시간 종이 울리자 학생 300명이 급식실로 몰려옵니다. 실제 학교의 측정값이 아니라 병목을 이해하기 위한 가상 장면입니다.
한 학생이 식사를 시작하려면 다음 단계를 차례로 지나야 합니다.
- 입구에서 식판 받기
- 밥 받기
- 반찬 받기
- 계산 또는 확인하기
- 자리로 이동하기
입구가 넓고 식판과 밥을 빠르게 받을 수 있어도, 반찬을 담아 주는 사람이 한 명뿐이라면 그 앞에 긴 줄이 생깁니다. 앞 단계가 빠르게 학생을 보내는 만큼 반찬 코너가 처리하지 못하기 때문입니다.
다른 곳에 직원을 더 배치해도 반찬 코너가 그대로라면 전체 줄은 줄어들지 않습니다. 모든 학생이 다섯 단계를 통과해야 하므로, 가장 느린 한 곳이 급식실 전체의 속도를 결정합니다.
급식실의 모든 구간이 느린 것이 아니라, 처리 속도가 가장 낮은 반찬 코너 앞에서 줄이 생기는 모습을 단순화한 그림입니다.
시스템의 속도는 가장 빠른 곳이 아니라 가장 느린 곳이 결정합니다.
이 글은 대용량 트래픽과 데이터베이스를 공부하며 이해한 원리를 제 언어로 정리한 학습 기록입니다. 제가 실제 대규모 서비스를 운영하며 측정한 결과를 소개하는 글이 아닙니다.
병목이란 무엇일까?
이런 상황을 Bottleneck(병목)이라고 부릅니다. 병의 몸통은 넓지만 목 부분은 좁아서 액체가 한꺼번에 나오지 못하는 모습에서 나온 말입니다.
서버에서 병목은 요청이 들어오는 속도보다 처리하는 속도가 느린 지점입니다. 앞 단계가 일을 계속 보내는데 다음 단계가 그만큼 끝내지 못하면, 끝내지 못한 요청은 그 앞에서 기다립니다.
병목은 데이터베이스나 특정 제품의 이름이 아닙니다. 어느 지점이든 처리 능력이 부족해 줄이 생기면 그 순간에는 병목이 될 수 있습니다.
급식실에서는 반찬 코너였지만 다른 날에는 식판이 부족하거나 결제 기계가 고장 날 수도 있습니다. 서버도 요청의 종류와 시스템 상태에 따라 가장 느린 지점이 달라집니다.
요청은 어떤 길을 지나갈까?
사용자가 화면의 버튼을 누르면 곧바로 결과가 나타나는 것처럼 보입니다. 하지만 요청은 여러 단계를 지나 일을 마친 뒤 다시 사용자에게 돌아옵니다.
가장 단순하게 줄이면 다음과 같습니다.
사용자 → 네트워크 → 애플리케이션 서버 → 캐시 또는 데이터베이스 → 필요하다면 외부 서비스 → 사용자에게 응답
네트워크는 요청과 응답이 이동하는 길입니다. 사용자와 서버의 거리가 멀거나 길이 혼잡하면 데이터가 오가는 데 시간이 더 걸립니다.
애플리케이션 서버는 무엇을 해야 하는지 판단하고 실제 업무 순서를 실행하는 곳입니다. 로그인 여부를 확인하고, 필요한 데이터를 요청하며, 결과를 화면에 맞는 형태로 정리합니다.
캐시는 자주 사용하는 정보를 가까운 곳에 잠시 보관하는 공간입니다. 이미 준비된 값을 빠르게 꺼낼 수 있지만, 모든 데이터를 무조건 캐시에 넣을 수 있는 것은 아닙니다.
데이터베이스는 회원, 게시글, 주문처럼 잃어버리면 안 되는 데이터를 저장하고 찾아오는 곳입니다. 필요한 데이터를 찾거나 여러 요청이 같은 값을 바꾸려 하면 시간이 길어질 수 있습니다.
외부 서비스는 본인 인증, 결제, 문자 발송처럼 다른 시스템에 부탁하는 작업입니다. 내 서버가 빨라도 상대 시스템의 답이 늦으면 요청 전체가 기다리게 됩니다.
실제 서비스의 길은 이보다 복잡할 수 있습니다. 이번 글에서는 전체 흐름만 보고, 데이터베이스의 Index, Connection Pool, Transaction, Lock은 3편에서 하나씩 설명하겠습니다.
줄은 언제 생길까?
이번 계산은 실제 서비스 수치가 아닌 이해를 위한 가정입니다. 한 구간에 초당 120개의 요청이 들어오는데 그 구간은 초당 100개만 처리할 수 있다고 해보겠습니다.
120개 - 100개 = 매초 20개가 대기
이 차이가 30초 동안 그대로 이어지면 대기 요청은 다음처럼 늘어납니다.
20개 × 30초 = 30초 후 600개가 대기
120에서 100을 빼면 20이고, 20에 30을 곱하면 600입니다. 단, 이 계산은 들어오는 속도와 처리 속도가 30초 동안 일정하고 대기 공간도 충분하다고 단순화한 결과입니다.
실제 시스템에서는 요청이 고르게 들어오지 않습니다. 대기 공간이 가득 차면 새 요청을 거절할 수도 있고, 일부 요청은 기다리다 제한 시간을 넘겨 먼저 실패할 수도 있습니다.
처리되지 않은 요청이 계속 쌓이면 문제가 차례로 번집니다.
- 대기 줄이 길어집니다.
- 자기 차례가 올 때까지 기다리므로 응답시간이 늘어납니다.
- 정해진 제한 시간을 넘긴 요청이 실패합니다.
- 답을 받지 못한 사용자가 버튼을 다시 누르거나 시스템이 요청을 재시도합니다.
- 새 요청이 더 늘어나 병목이 심해질 수 있습니다.
실패한 요청의 재시도가 한꺼번에 몰려 장애를 더 키우는 현상을 Retry Storm(재시도 폭풍)이라고 합니다. 재시도 자체가 나쁜 것은 아니지만 횟수와 간격을 제한하지 않으면, 이미 바쁜 곳에 일을 더 보내는 결과가 됩니다.
이해를 위한 가정
그림에서 모든 단계가 동시에 느려진 것은 아닙니다. 데이터베이스 앞에서 처음으로 처리 속도가 부족해졌고, 그곳의 대기가 앞 단계의 응답까지 늦춘 것입니다.
서버를 더 추가하면 해결될까?
급식실에서 주문을 받는 직원을 한 명에서 다섯 명으로 늘렸다고 해보겠습니다. 주문은 더 빨리 접수되지만 요리사가 여전히 한 명이라면 주방 앞 대기표만 더 빠르게 늘어납니다.
서버도 마찬가지입니다. Scale-out(스케일 아웃)은 서버 수를 늘려 처리 능력을 높이는 방식입니다. 애플리케이션 서버의 계산 능력이 부족한 상황이라면 같은 일을 나누어 맡길 서버를 추가하는 것이 도움이 될 수 있습니다.
하지만 데이터베이스가 병목이라면 애플리케이션 서버만 늘려도 데이터베이스로 향하는 요청이 더 많아질 수 있습니다. 데이터베이스의 처리 능력은 그대로이므로 줄이 사라지지 않고 오히려 더 빨리 길어질 수도 있습니다.
외부 결제 API가 병목인 경우도 같습니다. 내부 서버를 열 대로 늘려도 외부 시스템이 답하는 속도는 빨라지지 않습니다.
따라서 “서버를 늘리면 해결된다”가 아니라 “병목이 애플리케이션 서버라면 서버 추가가 해결책 후보가 된다”라고 말해야 정확합니다. 해결책은 서버의 개수보다 병목의 위치에서 시작합니다.
병목은 어디에서 생길 수 있을까?
어느 한 곳이 항상 먼저 느려지는 것은 아닙니다. 들어오는 요청의 종류, 데이터의 양, 서버 구성과 외부 시스템 상태에 따라 병목은 달라집니다.
네트워크
데이터가 지나가는 길이 느리거나 멀면 요청과 응답이 오가는 데 시간이 걸립니다. 큰 파일을 보내거나 불안정한 이동통신망을 사용할 때는 서버 계산이 빨라도 사용자가 늦다고 느낄 수 있습니다.
애플리케이션 서버
복잡한 계산으로 CPU가 바쁘거나 동시에 처리할 수 있는 작업 수가 부족하면 요청이 서버 앞에서 기다립니다. 메모리가 부족해 자주 정리 작업을 하는 경우에도 멈춤이 길어질 수 있습니다.
데이터베이스
필요한 데이터를 찾는 방법이 비효율적이거나 사용할 수 있는 연결이 부족하면 조회가 늦어집니다. 여러 요청이 같은 데이터를 바꾸려고 서로 차례를 기다릴 때도 병목이 생길 수 있습니다.
외부 서비스
본인 인증, 결제, 문자 발송을 맡은 외부 시스템의 응답이 늦으면 내 서버도 그 답을 기다립니다. 외부 서비스에 장애가 생겼을 때 기다림을 어디에서 끊을지도 미리 정해야 합니다.
이 네 곳 중 하나만 병목이라는 뜻도 아닙니다. 첫 병목을 완화하면 그동안 가려져 있던 다음 병목이 드러날 수 있으므로, 변경 뒤에는 다시 측정해야 합니다.
느린 곳을 어떻게 찾을까?
환자가 아프다고 해서 의사가 모든 약을 한꺼번에 먹이지는 않습니다. 어디가 아픈지 먼저 묻고 검사한 뒤 원인에 맞는 치료를 고릅니다.
시스템도 먼저 관찰해야 합니다. 이때 서로 다른 종류의 기록이 도움을 줍니다.
Metrics(메트릭)는 시스템 상태를 숫자로 측정한 값입니다. 초당 요청 수, 대기 중인 요청 수, CPU 사용량, 응답시간과 오류율이 시간에 따라 어떻게 변했는지 보여줍니다.
Logs(로그)는 시스템에서 일어난 사건의 기록입니다. 어떤 요청이 실패했고 그때 어떤 오류가 발생했는지 구체적인 단서를 남깁니다.
Traces(트레이스)는 요청 하나가 지나간 경로와 각 구간에서 쓴 시간을 따라간 기록입니다. 전체 응답이 2초 걸렸다면 네트워크, 애플리케이션, 데이터베이스, 외부 서비스 중 어디에서 오래 머물렀는지 찾는 데 도움을 줍니다.
이 용어들을 암기하는 것이 목적은 아닙니다. 다음 질문에 근거를 가지고 답하기 위해 사용하는 도구입니다.
- 요청은 어느 단계에서 오래 기다렸는가?
- CPU나 메모리가 부족했는가?
- 데이터베이스 연결을 기다렸는가?
- 외부 서비스의 응답이 느렸는가?
- 대기 중인 요청 수가 계속 증가했는가?
평균 응답시간만 확인해서도 안 됩니다. 대부분의 요청이 빨라도 일부 요청이 매우 오래 걸리면 그 사용자는 실패를 경험하므로, 느린 요청이 얼마나 자주 생기는지도 함께 봐야 합니다.
흔히 하는 잘못된 대응
병목을 찾지 않고 익숙한 기술부터 적용하면 문제를 다른 곳으로 옮길 수 있습니다.
- 측정하지 않고 서버부터 늘리기: 병목이 데이터베이스나 외부 서비스라면 비용과 요청 수만 늘 수 있습니다.
- 모든 데이터를 무조건 캐시에 넣기: 자주 바뀌는 데이터는 원본과 캐시가 달라질 수 있고, 캐시를 관리하는 새로운 문제가 생깁니다.
- 제한 없이 재시도하기: 실패한 곳에 요청을 더 보내 재시도 폭풍을 만들 수 있습니다.
- 평균 응답시간만 보기: 매우 느린 일부 요청과 제한 시간을 넘긴 실패를 평균이 가릴 수 있습니다.
- 가장 익숙한 곳부터 고치기: 개발자가 잘 아는 코드가 아니라 요청이 실제로 기다리는 지점을 먼저 확인해야 합니다.
핵심은 기술을 먼저 선택하지 않고 병목부터 찾는 것입니다. 측정으로 위치를 확인한 다음에야 서버 추가, 쿼리 개선, 캐시, 제한 시간 같은 대안을 비교할 수 있습니다.
가장 느린 한 곳에서 시작하자
핵심은 세 줄로 정리할 수 있습니다.
- 병목은 요청이 들어오는 속도보다 처리 속도가 느린 지점입니다.
- 시스템 전체의 속도는 가장 느린 단계가 결정합니다.
- 서버를 추가하기 전에 요청이 어디에서 기다리는지 먼저 측정해야 합니다.
서버가 느려지는 장소는 항상 같지 않습니다. 요청이 들어오는 속도보다 처리 속도가 느린 첫 번째 지점에서 줄이 생기고, 그곳이 전체 시스템의 병목이 됩니다.
많은 서비스에서 병목 후보로 가장 먼저 의심받는 곳은 데이터베이스입니다.
다음 글 — 데이터베이스는 데이터가 많아지면 왜 느려질까?
다음 편에서는 인덱스, 연결 풀, 트랜잭션과 잠금을 따라 데이터베이스가 실제로 일을 많이 하거나 기다리는 지점을 살펴봅니다.