작성일 :

부하를 어떻게 분산하는가

웹 서비스를 처음 만들 때는 서버 한 대면 충분합니다. 하루 방문자가 수백 명이라면, 요청 하나를 처리하는 데 문제가 없습니다.

하지만 서비스가 성장하면서 상황이 달라집니다. 사용자가 수천, 수만으로 늘어나면 어느 순간 단일 서버로는 감당할 수 없는 지점에 도달합니다. 네트워크 경로를 최적화해도 서버 자체의 처리 용량이 늘어나지는 않습니다.


확장성의 두 가지 방향

서버의 처리 용량을 늘리는 접근은 두 가지입니다. 서버 자체를 강화하거나, 서버의 수를 늘리거나.

Scale-up (수직 확장)

기존 서버의 하드웨어를 업그레이드하는 방식입니다. CPU, RAM, 스토리지를 더 좋은 것으로 교체합니다.

애플리케이션 코드를 수정할 필요가 없으므로 가장 단순한 방법입니다. 하지만 하드웨어에는 물리적 한계가 있고, 고성능 장비로 갈수록 성능 대비 비용이 급격히 높아집니다.

서버가 한 대인 이상, 그 서버에 장애가 발생하면 서비스 전체가 중단됩니다. 이를 단일 장애점(SPOF, Single Point of Failure)이라 합니다.

Scale-out (수평 확장)

서버를 여러 대로 늘려서 처리 용량을 확장하는 방식입니다. 범용 서버를 추가하면 되므로 비용 효율이 높고, 이론상 무한히 확장할 수 있습니다.

다만 요청을 여러 서버에 분배하는 메커니즘이 필요하고, 세션이나 캐시 같은 상태를 서버 간에 공유해야 하는 문제가 생깁니다.

언제 어떤 방식을 선택하는가

실무에서는 두 방식을 조합합니다. 데이터베이스는 상태 동기화가 복잡하므로 Scale-up을 우선 적용하고, 앞단의 웹 서버는 Scale-out으로 확장하는 구성이 일반적입니다.

현대의 웹 서비스 대부분은 Scale-out을 기본 전략으로 채택합니다. Scale-out에는 요청을 여러 서버에 분배하는 메커니즘이 필요합니다. 이것이 로드 밸런싱(Load Balancing)이고, 이를 수행하는 장치가 로드 밸런서(Load Balancer)입니다.


로드 밸런서의 역할

클라이언트는 하나의 주소로 요청을 보냅니다. 그 뒤에 서버가 몇 대인지는 알 수 없습니다.

1
2
3
4
5
6
7
8
9
10
클라이언트들                 로드 밸런서                서버들
                                │
   사용자1 ─────────────────►   │   ─────► 서버 A
                                │
   사용자2 ─────────────────►   │   ─────► 서버 B
                                │
   사용자3 ─────────────────►   │   ─────► 서버 C
                                │
   사용자4 ─────────────────►   │   ─────► 서버 A
                                │

로드 밸런서가 요청을 받아 서버를 선택하고 전달합니다. 클라이언트에게는 서버 한 대와 통신하는 것처럼 보이므로, 뒤에서는 서버를 자유롭게 추가하거나 제거할 수 있습니다.


L4 vs L7 로드 밸런싱

로드 밸런서가 요청을 분배하려면, 요청에 대한 정보가 필요합니다. 어디까지 들여다보느냐에 따라 로드 밸런싱의 방식이 달라집니다.

L4 로드 밸런싱 (Transport Layer)

전송 계층(TCP/UDP)에서 동작하며, IP 주소와 포트 번호만 확인합니다.

1
2
3
4
5
6
7
8
클라이언트 요청:
┌─────────────────────────────────────────┐
│ IP 헤더     │ TCP 헤더    │ 데이터      │
│ Dst:LB IP   │ Dst:80     │ HTTP 요청...│
└─────────────────────────────────────────┘

L4 로드 밸런서가 보는 것: IP, 포트만
데이터(HTTP 요청) 내용은 모름

패킷의 내용을 해석하지 않으므로 처리 속도가 빠르고, 초당 수백만 개의 연결도 감당할 수 있습니다. HTTP뿐 아니라 어떤 프로토콜이든 처리할 수 있습니다. SSL/TLS 트래픽도 내용을 해석하지 않고 그대로 백엔드에 전달(패스스루)합니다.

다만 요청 내용을 알 수 없으므로, URL이나 헤더에 따라 서버를 선택하는 정교한 라우팅은 불가능합니다.

L7 로드 밸런싱 (Application Layer)

애플리케이션 계층에서 동작하며, HTTP 헤더, URL, 쿠키 등 요청 내용을 확인할 수 있습니다.

1
2
3
4
5
6
HTTP 요청:
GET /api/users HTTP/1.1
Host: example.com
Cookie: session=abc123

L7 로드 밸런서가 보는 것: URL 경로, 헤더, 쿠키 전부

요청 내용을 파악할 수 있으므로 정교한 라우팅이 가능합니다. /api 경로는 API 서버로, /static 경로는 정적 파일 서버로 보내는 식입니다.

SSL/TLS도 로드 밸런서에서 처리할 수 있습니다. 클라이언트와 로드 밸런서 사이만 암호화하고, 백엔드에는 복호화된 요청을 전달합니다. 이를 SSL Termination이라 하며, 백엔드 서버의 암호화 부담을 줄입니다.

반면 요청 전체를 해석해야 하므로 L4보다 처리 비용이 높고, 지연 시간이 추가됩니다.

비교

1
2
3
4
5
6
7
8
9
10
11
12
13
┌────────────────┬─────────────────────┬─────────────────────┐
│     항목       │        L4           │         L7          │
├────────────────┼─────────────────────┼─────────────────────┤
│ 결정 기준      │ IP, 포트            │ URL, 헤더, 쿠키 등   │
├────────────────┼─────────────────────┼─────────────────────┤
│ 성능           │ 매우 빠름           │ 상대적으로 느림      │
├────────────────┼─────────────────────┼─────────────────────┤
│ SSL 처리       │ 패스스루(그대로 전달)│ 종료 가능           │
├────────────────┼─────────────────────┼─────────────────────┤
│ 유연성         │ 낮음                │ 높음                │
├────────────────┼─────────────────────┼─────────────────────┤
│ 사용 사례      │ 대용량 TCP 트래픽   │ 웹 애플리케이션     │
└────────────────┴─────────────────────┴─────────────────────┘

로드 밸런싱 알고리즘

로드 밸런서가 요청을 받으면, 어떤 서버로 보낼지 결정해야 합니다.

Round Robin

가장 단순한 방법입니다. 서버 목록을 순서대로 돌아가며 하나씩 선택합니다.

1
2
3
4
5
6
요청 1 → 서버 A
요청 2 → 서버 B
요청 3 → 서버 C
요청 4 → 서버 A  (다시 처음부터)
요청 5 → 서버 B
...

구현이 쉽고 오버헤드가 거의 없지만, 서버 사양이나 요청 처리 시간의 차이를 고려하지 않습니다.

Weighted Round Robin

서버마다 가중치를 부여하여, 성능이 좋은 서버에 더 많은 요청을 보냅니다.

1
2
3
서버 A (가중치 3), 서버 B (가중치 2), 서버 C (가중치 1)

요청 순서: A, A, A, B, B, C, A, A, A, B, B, C, ...

가중치는 정적으로 설정하므로, 실시간 부하 변화는 반영하지 못합니다.

Least Connections

현재 연결 수가 가장 적은 서버를 선택합니다.

1
2
3
서버 A: 50개 연결
서버 B: 30개 연결  ← 선택
서버 C: 45개 연결

WebSocket처럼 연결이 오래 유지되는 서비스에서 효과적이지만, 서버 사양의 차이는 고려하지 않습니다.

Weighted Least Connections

Least Connections에 가중치를 결합한 방식입니다. 연결수 / 가중치가 가장 낮은 서버를 선택합니다.

1
2
서버 A (가중치 3): 30개 연결 → 30/3 = 10
서버 B (가중치 1): 8개 연결  → 8/1 = 8  ← 선택

IP Hash

클라이언트 IP 주소를 해시하여 서버를 선택합니다. 같은 IP는 항상 같은 서버로 연결되므로, 세션 지속성이 필요할 때 사용합니다.

1
2
3
hash(192.168.1.10) % 3 = 0 → 서버 A
hash(192.168.1.11) % 3 = 2 → 서버 C
hash(192.168.1.12) % 3 = 1 → 서버 B

서버 수가 바뀌면 hash % N의 결과가 달라져, 대부분의 요청이 다른 서버로 재배치됩니다.

Consistent Hashing

해시값의 범위를 원형(링)으로 구성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
해시 링 (0 ~ 2^32-1):

        0
        │
   서버C │        서버A
        │       /
        ●──────●
       / \
      /   \
     ●     ●
   서버B   요청 해시값

요청의 해시값에서 시계방향으로 가장 가까운 서버 선택

서버가 추가되면 해당 서버와 이전 서버 사이의 요청만 재분배되고, 나머지는 영향을 받지 않습니다. 서버 3대에서 4대로 늘릴 때 전체의 약 1/4만 이동합니다.

단순한 웹 서비스라면 Round Robin으로 시작하고, 서버 사양이 다르면 Weighted를 추가하며, 세션이 중요하면 IP Hash나 Cookie 기반 세션 지속성을 고려합니다.


세션 지속성 (Session Persistence)

온라인 쇼핑몰에서 장바구니에 상품을 추가하는 상황을 생각해봅시다.

1
2
요청 1 (서버 A): 상품 추가 → 서버 A 세션에 저장
요청 2 (서버 B): 장바구니 조회 → 서버 B에는 세션 없음!

세션이 특정 서버에 저장되어 있으면, 같은 클라이언트의 요청은 같은 서버로 보내야 합니다. 이를 세션 지속성(Sticky Sessions)이라 합니다.

Source IP 기반

클라이언트 IP 주소를 해시하여 항상 같은 서버로 보냅니다. IP 주소만으로 동작하므로 L4 로드 밸런서에서도 사용할 수 있습니다.

하지만 NAT 환경에서 문제가 생깁니다. 회사나 카페의 사용자 수백 명이 하나의 공인 IP를 공유하면, 모두 같은 서버로 몰립니다. 모바일 환경에서는 셀 타워를 이동할 때마다 IP가 바뀌어 세션이 끊길 수도 있습니다.

로드 밸런서가 HTTP 응답에 쿠키를 삽입하여 서버를 식별합니다.

1
2
3
4
5
6
첫 번째 응답:
Set-Cookie: SERVERID=server-a

이후 요청:
Cookie: SERVERID=server-a
→ 서버 A로 라우팅

NAT 뒤의 사용자도 정확히 구분할 수 있지만, L7 로드 밸런서가 필요하고 쿠키를 지원하지 않는 클라이언트(일부 API 클라이언트, IoT 기기)에서는 동작하지 않습니다.

더 나은 접근: Stateless 설계

세션이 특정 서버에 묶여 있기 때문에 Source IP나 Cookie가 필요했습니다. 세션을 서버 외부에 저장하면 이 제약 자체가 없어집니다.

세션을 Redis나 Memcached 같은 인메모리 저장소에 보관하면, 모든 서버가 같은 세션 데이터에 접근할 수 있습니다.

1
2
3
4
5
6
세션을 Redis/Memcached에 저장
           │
   ┌───────┴───────┐
   ▼               ▼
서버 A          서버 B
(세션 조회)     (세션 조회)

어느 서버로 요청이 가든 같은 세션에 접근할 수 있으므로, 세션 지속성이 필요 없어집니다. 서버 장애 시에도 세션이 유실되지 않습니다.

다만 모든 요청에서 저장소로의 네트워크 왕복이 추가되고, 저장소 자체의 가용성도 확보해야 합니다.


헬스 체크

다운된 서버로 요청이 가면 사용자가 오류를 받습니다. 장애 서버를 자동으로 감지하고 제외해야 합니다.

Active Health Check

로드 밸런서가 주기적으로 서버에 요청을 보내 상태를 확인합니다.

TCP 체크는 해당 포트에 TCP 연결을 시도하여, 연결이 되면 정상으로 판단합니다. 가장 단순하지만, 애플리케이션이 정상 동작하는지는 알 수 없습니다.

HTTP 체크는 실제 HTTP 요청을 보내고, 응답 상태 코드(200 OK 등)까지 확인합니다.

1
2
3
4
5
6
7
8
9
10
# NGINX Plus 예시 (오픈소스 Nginx에서는 별도 모듈 필요)
upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;

    health_check interval=5s
                 fails=3
                 passes=2
                 uri=/health;
}

실패 임계값

한 번 실패했다고 바로 서버를 제외하지 않습니다. 일시적인 네트워크 지연이나 타임아웃을 고려하여 임계값을 설정합니다.

앞선 Nginx 설정 예시에서 fails=3은 3번 연속 실패하면 unhealthy, passes=2는 2번 연속 성공하면 다시 healthy, interval=5s는 5초마다 체크한다는 의미입니다.

이 경우 서버가 다운되었을 때 최대 15초(5초 x 3회) 후에 감지됩니다. 복구 시에는 10초(5초 x 2회) 후에 트래픽이 다시 유입됩니다. 감지 속도와 오탐 방지 사이의 균형을 조정하는 값입니다.

Passive Health Check

실제 트래픽의 응답 결과로 상태를 판단합니다.

1
2
3
4
요청 → 서버 A → 5xx 에러 (실패 카운트 +1)
요청 → 서버 A → timeout (실패 카운트 +1)
요청 → 서버 A → 5xx 에러 (실패 카운트 +1, 임계값 도달)
→ 서버 A를 unhealthy로 표시

별도의 헬스 체크 트래픽이 필요 없지만, 실제 사용자가 오류를 경험한 후에야 장애를 감지한다는 단점이 있습니다.

대부분의 환경에서는 Active + Passive를 함께 사용하여, 사전 예방과 실시간 감지를 병행합니다.


마무리: 분산이 확장의 핵심

단일 서버의 한계를 여러 서버로 분산하되, 클라이언트에게는 하나의 서비스처럼 보이게 하는 것. 이것이 로드 밸런싱의 핵심입니다.

하지만 로드 밸런서 자체도 하나의 진입점이라면, 여기에도 부하가 몰릴 수 있지 않을까요?

Part 2에서는 DNS를 활용한 로드 밸런싱을 살펴봅니다. 도메인 이름 하나에 여러 IP를 연결하는 방식, 지리적 라우팅, DNS 기반 분산의 한계를 다룹니다.

Part 3에서는 고가용성 아키텍처를 다룹니다.



관련 글

시리즈

Tags: L4, L7, 고가용성, 네트워크, 로드밸런싱

Categories: ,