본문 바로가기
서버연구 블로그 서버연구 블로그

대규모 트래픽을 견디는 서버 아키텍처는 어떻게 설계하는가?

읽는 시간 약 15분

서비스를 운영하다 보면 언젠가는 이런 질문을 하게 된다.

“사용자가 지금보다 10배, 100배 늘어나면 서버를 어떻게 구성해야 할까?”

가장 먼저 떠올리는 방법은 서버 사양을 높이는 것이다. CPU를 더 빠른 제품으로 바꾸고 메모리를 늘리거나 더 높은 사양의 서버로 이전하면 일정 수준까지는 처리량이 증가한다.

하지만 대규모 트래픽 환경에서는 단순히 서버 한 대의 성능을 높이는 것만으로 해결하기 어렵다.

웹 서버, 애플리케이션 서버, 데이터베이스, 네트워크, 캐시, 세션, 스토리지 가운데 어느 한 곳이라도 처리 한계에 도달하면 전체 서비스의 성능이 떨어질 수 있기 때문이다.

따라서 대규모 트래픽을 처리하는 서버 아키텍처에서 가장 중요한 것은 강력한 서버 한 대가 아니다.

트래픽이 증가하더라도 특정 서버에 부하가 집중되지 않고, 일부 서버에 장애가 발생하더라도 전체 서비스가 계속 동작하도록 만드는 구조가 핵심이다.


대규모 트래픽에서 문제가 발생하는 원인

트래픽 증가가 곧바로 서버 장애를 의미하는 것은 아니다.

문제는 요청 증가 속도가 시스템의 처리 능력을 넘어설 때 발생한다.

예를 들어 하나의 웹 서버가 동시에 처리할 수 있는 요청량에 한계가 있다고 가정해 보자.

접속자가 많아지면 Connection이 증가하고 웹 서버 Worker가 처리해야 하는 요청도 함께 증가한다.

CPU 사용률이 높아질 수 있고 메모리 사용량이 증가하며 네트워크 대역폭이나 파일 디스크립터 같은 운영체제 자원까지 부족해질 수 있다.

애플리케이션 서버가 정상적으로 요청을 처리하더라도 데이터베이스가 병목이 될 수도 있다.

대표적인 병목 지점은 다음과 같다.

  • CPU 처리 능력
  • 메모리 용량
  • 네트워크 대역폭
  • TCP Connection 수
  • 웹 서버 Worker 처리 능력
  • 애플리케이션 Thread 또는 Process 수
  • 데이터베이스 Connection Pool
  • 느린 SQL Query
  • 디스크 I/O
  • 외부 API 응답 시간

이 가운데 하나라도 한계에 도달하면 다른 서버에 여유가 있어도 전체 응답 시간이 증가할 수 있다.

그래서 대규모 트래픽 아키텍처는 단순히 서버를 많이 배치하는 것이 아니라 병목이 한 곳에 집중되지 않도록 설계해야 한다.


대규모 트래픽 서버는 어떻게 동작할까?

소규모 서비스에서는 다음과 같이 하나의 서버에서 여러 기능을 처리하는 경우가 많다.

사용자
  ↓
Web Server
  ↓
Application
  ↓
Database

초기에는 이런 구조가 관리하기 쉽고 비용도 적게 든다.

하지만 트래픽이 증가하면 웹 요청과 애플리케이션 처리, 데이터베이스 접근이 하나의 시스템 자원을 서로 경쟁하게 된다.

대규모 환경에서는 일반적으로 기능을 분리한다.

사용자
  ↓
CDN
  ↓
Load Balancer
  ↓
Web / Application Server
  ↓
Cache
  ↓
Database

여기서 중요한 것은 각 계층이 맡는 역할이다.

CDN은 이미지, JavaScript, CSS 같은 정적 콘텐츠를 사용자와 가까운 위치에서 제공해 원본 서버로 전달되는 요청을 줄인다.

Load Balancer는 여러 서버 가운데 요청을 처리할 서버를 선택한다.

웹 또는 애플리케이션 서버는 실제 비즈니스 로직을 처리한다.

Redis 같은 캐시는 반복적으로 조회되는 데이터를 메모리에 저장해 데이터베이스 접근을 줄일 수 있다.

마지막으로 데이터베이스는 서비스의 실제 데이터를 관리한다.

이처럼 계층을 분리하면 특정 계층의 부하가 증가했을 때 해당 부분만 확장할 수 있다.


트래픽이 증가하면 실제로 어떤 증상이 나타날까?

대규모 트래픽 장애는 대부분 갑자기 서버가 완전히 멈추는 형태로 시작하지 않는다.

먼저 응답 시간이 조금씩 증가하는 경우가 많다.

평소 100ms 안에 처리되던 요청이 500ms, 1초, 3초까지 증가하기 시작한다.

이후 연결 대기 시간이 길어지고 Timeout이 발생한다.

Nginx 환경에서는 다음과 같은 오류가 증가할 수 있다.

502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout

하지만 HTTP 상태 코드만 보고 원인을 판단하면 안 된다.

예를 들어 504가 발생했다고 해서 반드시 Nginx가 문제라는 의미는 아니다.

Nginx는 정상적으로 동작하고 있지만 뒤쪽 애플리케이션 서버의 응답이 늦어졌을 수도 있다.

애플리케이션 서버 역시 정상일 수 있다.

데이터베이스 Query가 지연되면서 애플리케이션이 응답하지 못하고 있을 가능성도 있다.

대규모 트래픽 환경에서 가장 위험한 것은 이런 연쇄 지연이다.

DB Query 지연
      ↓
Application 응답 지연
      ↓
Connection 점유 시간 증가
      ↓
Web Server 대기 요청 증가
      ↓
전체 응답 시간 증가
      ↓
Timeout 발생

하나의 병목이 다른 계층의 자원 부족으로 확산되는 것이다.


대규모 트래픽에서는 무엇부터 분석해야 할까?

트래픽 장애가 발생하면 CPU 사용률부터 확인하는 경우가 많다.

물론 CPU는 중요한 지표지만 CPU 하나만으로 서버 상태를 판단할 수는 없다.

먼저 전체 요청 흐름을 따라가는 것이 좋다.

Client
 ↓
CDN
 ↓
Load Balancer
 ↓
Web Server
 ↓
Application
 ↓
Cache
 ↓
Database

어느 구간부터 응답 시간이 증가하는지를 확인해야 한다.

Linux 서버에서는 기본적으로 다음 지표를 살펴볼 수 있다.

uptime

Load Average를 통해 시스템에 처리 대기 작업이 증가하고 있는지 확인한다.

top

CPU와 메모리 사용량, 특정 프로세스의 자원 사용량을 확인한다.

vmstat 1

CPU 대기, 메모리, 프로세스 실행 상태를 연속적으로 확인할 수 있다.

특히 r 값이 CPU 코어 수보다 지속적으로 높은 상태라면 CPU를 기다리는 작업이 많을 가능성이 있다.

디스크 병목이 의심된다면 다음 명령어도 유용하다.

iostat -xz 1

디스크의 처리 지연 시간과 사용률을 확인한다.

네트워크 Connection 상태도 중요하다.

ss -s

ESTABLISHED, TIME_WAIT 등 TCP Connection 상태를 빠르게 확인할 수 있다.

여기서 중요한 것은 특정 숫자 하나를 보고 장애라고 판단하지 않는 것이다.

평상시 수치를 기준으로 트래픽 증가 시 어떤 지표가 달라졌는지를 비교해야 한다.


대규모 트래픽을 견디려면 어떻게 설계해야 할까?

가장 기본적인 방법은 서버를 수평 확장할 수 있도록 만드는 것이다.

서버 한 대의 CPU와 메모리를 계속 높이는 방식을 Scale Up이라고 한다.

반대로 동일한 역할을 수행하는 서버를 여러 대 배치하는 것을 Scale Out이라고 한다.

              ┌─ App Server 1
User → LB ────┼─ App Server 2
              ├─ App Server 3
              └─ App Server 4

트래픽이 증가하면 애플리케이션 서버를 추가할 수 있고 Load Balancer가 요청을 분산한다.

하지만 서버를 여러 대 추가했다고 해서 자동으로 확장 가능한 서비스가 되는 것은 아니다.

특히 Session을 특정 서버 메모리에 저장하고 있다면 문제가 발생할 수 있다.

사용자의 첫 번째 요청이 Server 1에 전달되고 다음 요청이 Server 2에 전달되면 로그인 상태를 찾지 못할 수도 있기 때문이다.

그래서 대규모 환경에서는 세션을 Redis 같은 외부 저장소에 두거나 가능한 경우 Stateless 구조로 설계한다.

App Server 1 ─┐
App Server 2 ─┼── Redis Session
App Server 3 ─┘

이렇게 하면 특정 서버가 종료되어도 다른 서버가 동일한 사용자 요청을 처리하기 쉬워진다.


캐시는 데이터베이스 부하를 줄이는 핵심 계층이다

애플리케이션 서버를 10대로 늘리더라도 모든 서버가 하나의 데이터베이스에 계속 요청하면 결국 데이터베이스가 병목이 될 수 있다.

예를 들어 자주 조회되지만 거의 변경되지 않는 데이터가 있다고 가정하자.

매 요청마다 데이터베이스에서 조회하면 동일한 Query가 반복된다.

이 데이터를 Redis 같은 캐시에 저장하면 요청 흐름을 다음과 같이 바꿀 수 있다.

Application
     ↓
   Cache
   ↓   ↓
 HIT   MISS
 ↓      ↓
응답   Database

캐시에 데이터가 있으면 데이터베이스까지 접근하지 않는다.

이를 Cache Hit이라고 한다.

캐시에 데이터가 없을 때만 데이터베이스를 조회한다.

다만 캐시는 무조건 많이 적용한다고 좋은 것은 아니다.

TTL 설정, 데이터 무효화, Cache Stampede 같은 문제를 함께 고려해야 한다.

특히 인기 데이터의 캐시가 동시에 만료되면 수많은 요청이 한꺼번에 데이터베이스로 전달될 수 있다.

따라서 캐시는 단순한 속도 향상 기능이 아니라 데이터베이스를 보호하는 하나의 트래픽 제어 계층으로 보는 것이 좋다.


데이터베이스 확장은 애플리케이션 서버보다 어렵다

웹 서버나 애플리케이션 서버는 비교적 쉽게 여러 대로 확장할 수 있다.

하지만 데이터베이스는 데이터의 일관성을 유지해야 하기 때문에 단순히 서버를 추가하는 방식으로 해결하기 어렵다.

초기에는 Query 최적화와 Index 점검이 먼저다.

그 다음 필요하다면 읽기 트래픽을 분리할 수 있다.

                  ┌─ Read Replica 1
Application ──────┼─ Read Replica 2
       │
       └──────────── Primary DB
                     Write

쓰기 작업은 Primary DB에서 처리하고 일부 읽기 작업은 Replica로 분산하는 방식이다.

하지만 Replication을 구성하면 복제 지연이라는 새로운 문제도 발생할 수 있다.

Primary DB에서 데이터를 변경한 직후 Replica에서 조회하면 아직 변경 내용이 전달되지 않았을 수도 있다.

따라서 단순히 DB 서버를 늘리는 것보다 애플리케이션이 데이터 일관성을 어떻게 요구하는지까지 고려해야 한다.


모든 요청을 실시간으로 처리할 필요는 없다

대규모 트래픽 환경에서는 요청을 받는 순간 모든 작업을 완료하려고 하면 서버 부하가 급격히 증가할 수 있다.

예를 들어 회원가입 후 이메일 발송 작업이 있다고 가정해 보자.

사용자 계정 생성과 이메일 발송을 하나의 요청에서 모두 처리할 필요는 없다.

사용자 요청
   ↓
계정 생성
   ↓
Message Queue
   ↓
즉시 응답

Worker
   ↓
이메일 발송

Kafka, RabbitMQ, SQS 같은 메시지 시스템을 사용하면 즉시 처리할 필요가 없는 작업을 비동기로 분리할 수 있다.

이 구조의 장점은 트래픽이 순간적으로 증가해도 요청을 Queue에 쌓아두고 Worker가 처리 가능한 속도로 소비할 수 있다는 점이다.

다만 Queue가 무한한 처리 능력을 제공하는 것은 아니다.

Queue 적재량과 Consumer 처리 속도 역시 반드시 모니터링해야 한다.


장애를 전제로 설계해야 한다

대규모 시스템에서 “서버가 장애 나지 않도록 만드는 것”만을 목표로 하면 한계가 있다.

서버는 언젠가 장애가 발생할 수 있다.

네트워크도 끊길 수 있고 데이터베이스도 응답하지 않을 수 있으며 외부 API도 지연될 수 있다.

따라서 중요한 질문은 다음과 같다.

서버 한 대가 죽으면 서비스가 계속 동작하는가?

DB Replica 하나가 장애 나면 요청을 다른 서버가 처리하는가?

외부 API가 느려져도 전체 서비스가 같이 느려지는가?

트래픽이 갑자기 증가하면 자동으로 서버를 늘릴 수 있는가?

대규모 아키텍처에서는 장애 자체보다 장애가 다른 시스템으로 전파되는 것을 막는 것이 중요하다.

Timeout, Retry, Circuit Breaker, Rate Limit 같은 설계가 필요한 이유도 여기에 있다.

잘못된 Retry 정책은 오히려 장애를 키울 수도 있다.

응답하지 않는 서버에 모든 클라이언트가 반복적으로 재요청하면 이미 과부하 상태인 서버에 더 많은 트래픽이 유입되기 때문이다.


정리

대규모 트래픽을 견디는 서버 아키텍처는 특정 기술 하나로 완성되지 않는다.

Load Balancer를 설치했다고 끝나는 것도 아니고 Redis를 사용한다고 대규모 서비스를 운영할 수 있는 것도 아니다.

핵심은 요청이 시스템 내부에서 이동하는 전체 흐름을 이해하는 것이다.

사용자
 ↓
CDN
 ↓
Load Balancer
 ↓
Web / Application Server
 ↓
Cache
 ↓
Database

        +
        
Message Queue
Monitoring
Auto Scaling
Failover

트래픽이 증가하면 먼저 병목 지점을 찾아야 한다.

애플리케이션 서버가 부족하다면 수평 확장하고, 반복 조회가 데이터베이스에 집중된다면 캐시를 검토하며, 시간이 오래 걸리는 작업은 비동기 처리로 분리할 수 있다.

그리고 특정 서버가 장애 나더라도 전체 서비스가 멈추지 않도록 이중화와 장애 격리 구조가 필요하다.

결국 대규모 트래픽 아키텍처의 핵심은 “엄청나게 강력한 서버를 만드는 것”이 아니다.

한 곳에 부하가 집중되지 않게 하고, 병목이 생기면 확장할 수 있으며, 일부 시스템이 실패해도 전체 서비스는 계속 동작하도록 만드는 것.

이것이 대규모 서비스를 안정적으로 운영하기 위한 서버 아키텍처의 기본 원칙이다.

instone
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.