컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 08

로드 밸런서

네트워크로드 밸런서분산

로드 밸런서는 들어오는 네트워크 트래픽을 여러 서버에 분산시켜 고가용성과 성능을 확보하는 장치입니다. 단일 서버 장애 시에도 서비스를 유지하고, 트래픽 폭증 시 수평 확장으로 대응합니다.

핵심 개념

Round Robin

요청을 서버 목록에 순서대로 순환 배분합니다. 구현이 단순하고 서버 성능이 동일할 때 효과적입니다. Weighted Round Robin은 서버 성능에 따라 가중치를 부여해 더 많은 요청을 보냅니다. 세션 유지가 필요 없는 무상태 서비스에 적합합니다.

핵심 개념

Least Connection

현재 활성 연결이 가장 적은 서버로 요청을 보냅니다. 요청 처리 시간이 다양할 때 효과적입니다. 긴 작업을 처리 중인 서버에 추가 부하가 쌓이는 것을 방지합니다. Weighted Least Connection은 서버 성능도 고려합니다.

핵심 개념

IP Hash

클라이언트 IP를 해시해 항상 같은 서버로 연결합니다. 세션 상태를 서버에 저장하는 경우 유용합니다. 같은 사용자가 항상 같은 서버에 접속해 세션 유지가 가능합니다. 단, 특정 IP에 트래픽이 집중되면 불균형이 발생할 수 있습니다.

핵심 개념

L4 vs L7 로드 밸런서

L4(전송 계층): TCP/UDP 포트와 IP 기반으로 분산합니다. 빠르고 단순하며 패킷 내용을 보지 않습니다. L7(응용 계층): HTTP 헤더, URL, 쿠키 등을 분석해 분산합니다. /api는 API 서버, /static은 파일 서버로 라우팅하는 콘텐츠 기반 분산이 가능합니다.

핵심 개념

헬스 체크

로드 밸런서는 주기적으로 서버 상태를 확인합니다. HTTP 헬스 체크는 /health 엔드포인트에 GET 요청을 보내 200 응답을 확인합니다. 서버가 응답하지 않으면 자동으로 풀에서 제외하고, 복구되면 자동으로 복귀합니다.

핵심 개념

세션 퍼시스턴스

Sticky Session이라고도 합니다. 같은 클라이언트의 요청을 항상 같은 서버로 보냅니다. 로드 밸런서가 발급하는 쿠키나 애플리케이션 쿠키(JSESSIONID 등), IP Hash 방식을 사용합니다. 단, 해당 서버 장애 시 세션이 손실되므로 Redis 같은 외부 세션 저장소 사용을 권장합니다.

Active-Passive vs Active-Active — Active-Passive는 평소엔 하나만 사용하고 장애 시 다른 하나로 전환합니다. Active-Active는 두 로드 밸런서가 동시에 트래픽을 처리합니다. Active-Active가 자원 활용률이 높지만 설정이 복잡합니다.
DSR (Direct Server Return) — 응답 트래픽이 로드 밸런서를 거치지 않고 클라이언트에 직접 전달됩니다. 응답이 요청보다 훨씬 큰 서비스(동영상 스트리밍 등)에서 로드 밸런서 병목을 줄일 수 있습니다. 요청은 LB를 거치지만 응답은 서버에서 직접 나갑니다. LVS의 Direct Routing 방식은 로드 밸런서와 서버가 같은 LAN 구간에 있어야 합니다.
nginx vs HAProxy — nginx는 HTTP(L7) 로드 밸런서로 정적 파일 서빙, SSL 종료, 캐싱도 겸하고, stream 모듈(기본 빌드에는 빠져 있어 --with-stream이 필요)로 TCP/UDP도 분산합니다. HAProxy는 TCP와 HTTP 로드 밸런싱을 모두 지원하는 전용 소프트웨어입니다. 클라우드에서는 AWS ALB/NLB, GCP Cloud Load Balancing을 사용합니다.

서버 한 대로는 부족한 이유

서버 한 대는 처리 용량에 한계가 있고, 그 한 대가 멈추면 서비스 전체가 멈춥니다. 같은 서버를 여러 대 두고 앞에 하나의 진입 주소를 세워 요청을 나누면 용량과 가용성을 함께 높일 수 있고, 이 진입점 역할이 로드 밸런서입니다. 서버를 늘려서 용량을 키우는 방식을 수평 확장이라고 합니다.

숫자로 확인하기

가용성의 숫자 — 1년은 8,760시간이라서 가용성 99.9%는 연 8.76시간, 99.99%는 약 52.6분, 99.999%는 약 5.3분의 정지만 허용합니다.
두 대를 두면 얼마나 좋아지나 — 서버 한 대의 가용성이 99%(정지 확률 0.01)이고 두 대가 서로 독립이라고 가정하면, 둘이 동시에 멈출 확률은 0.01 × 0.01 = 0.0001이라서 서버 쪽 가용성은 99.99%가 됩니다. 로드 밸런서 자체의 가용성과 서버가 공유하는 DB 같은 의존성은 따로 봐야 합니다.
해시와 서버 수 변화 — 키를 서버 수로 나눈 나머지로 서버를 고르면, 서버가 3대에서 4대로 늘 때 키를 균등하게 가정하면 같은 서버에 남는 키는 3/12, 곧 25%뿐이고 75%가 다른 서버로 옮겨 갑니다. 이런 이동을 줄이려고 일관된 해싱(consistent hashing)을 씁니다.
Least Connection 계산 — 서버 A, B, C의 활성 연결이 5, 2, 8개이면 다음 요청은 B로 갑니다. 가중치 3:1:1의 Weighted Round Robin이라면 5개 요청 중 3개가 첫 서버로 갑니다.

자주 하는 오해

로드 밸런서를 두면 장애가 없어진다 — 로드 밸런서가 단일 장애 지점이 될 수 있습니다. 그래서 로드 밸런서 자체도 이중화하거나 관리형 서비스를 씁니다.
Round Robin이면 부하가 항상 고르다 — 요청 개수는 고르게 나누지만 요청마다 처리 비용이 다르면 서버 부하는 달라집니다. 이때는 활성 연결 수를 보는 방식이 낫습니다.
L7이 항상 더 좋다 — L7은 내용을 보고 정교하게 나눌 수 있지만, HTTP를 해석해야 해서 처리 비용이 늘고 HTTPS라면 TLS를 끝내야 합니다. 단순한 TCP 분산에는 L4가 적합합니다.
헬스 체크가 200이면 정상이다 — 체크 엔드포인트가 무엇을 확인하느냐에 달려 있습니다. 프로세스만 확인하면 DB 연결이 끊긴 서버를 정상으로 볼 수 있습니다.

면접에서 이렇게 물어요

면접 질문

L4와 L7 로드 밸런서의 차이는 무엇인가요?

L4는 IP와 포트 수준에서 TCP/UDP 연결을 나누어 빠르고 단순합니다. L7은 HTTP 헤더, URL, 쿠키를 읽어 경로별 라우팅이나 쿠키 기반 고정 같은 정교한 분산이 가능하지만 처리 비용이 더 듭니다.

면접 질문

서버에 세션이 있으면 로드 밸런서에서 어떻게 처리하나요?

같은 사용자를 같은 서버로 보내는 고정 세션(sticky session)을 쓰거나, 세션을 Redis 같은 외부 저장소로 빼서 어느 서버로 가도 같은 세션을 읽게 합니다. 고정 세션은 그 서버가 죽으면 세션이 사라지고 부하가 한쪽으로 쏠릴 수 있습니다.

면접 질문

서버 한 대가 죽으면 어떻게 되나요?

헬스 체크가 연속으로 실패하면 로드 밸런서가 그 서버를 대상에서 빼고, 다시 성공하면 복귀시킵니다. 이미 연결된 요청은 실패할 수 있어서 재시도나 연결 종료 대기(draining) 설정이 필요합니다.

면접 질문

로드 밸런서 자체가 죽으면 어떻게 하나요?

Active-Passive로 대기 장비가 가상 IP를 넘겨받게 하거나, Active-Active로 여러 대가 동시에 처리하게 합니다. 클라우드의 관리형 로드 밸런서는 여러 가용 영역에 걸쳐 이중화되어 있습니다.

실제 시스템에서는

nginx upstream — nginx는 기본이 가중치 라운드 로빈이고 least_conn, ip_hash, hash ... consistent 지시어로 방식을 바꿉니다. ip_hash는 IPv4 주소의 앞 세 옥텟을 키로 씁니다. 오픈소스 nginx의 서버 상태 확인은 요청 실패를 보고 판단하는 수동 방식이고 주기적인 능동 확인은 NGINX Plus 기능입니다.
클라우드 서비스 — AWS의 Application Load Balancer는 HTTP/HTTPS를 다루는 L7 방식이고 Network Load Balancer는 TCP/UDP/TLS를 다루는 L4 방식입니다.
Spring Boot 헬스 체크 — Spring Boot Actuator의 /actuator/health 엔드포인트가 애플리케이션과 DB 같은 의존성의 상태를 모아 보여 주므로 헬스 체크 경로로 쓸 수 있습니다.

함께 보면 좋은 토픽

DNS — DNS 라운드 로빈과 로드 밸런서를 비교합니다.
HTTPS / TLS — 로드 밸런서에서 TLS를 끝낼 때의 흐름을 봅니다.
HTTP — L7 분산이 읽는 헤더와 쿠키를 봅니다.
CDN — 엣지에서 트래픽을 나누는 구조를 비교합니다.
WebSocket — 오래 유지되는 연결을 분산할 때의 고려점을 봅니다.
포워드 / 리버스 프록시 — 클라이언트와 서버 사이에서 요청을 대신 전달하는 중계 서버를 봅니다.

참고 자료

nginx 로드 밸런싱 — nginx — Using nginx as HTTP load balancer
Spring Boot Actuator — Endpoints — Spring Boot Reference
논문 — D. Karger 외, "Consistent Hashing and Random Trees", ACM STOC 1997

Java — Spring Cloud LoadBalancer 구현 (Round Robin / Random)

// ── Spring Cloud LoadBalancer 의존성 ──────────────────────
// implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'

import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.core.*;
import reactor.core.publisher.Mono;

/* ── 커스텀 Round Robin 로드 밸런서 ─────────────────────── */
public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final ObjectProvider<ServiceInstanceListSupplier> supplier;
    private final AtomicInteger position = new AtomicInteger(0);
    private final String serviceId;

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return supplier.getIfAvailable(NoopServiceInstanceListSupplier::new)
            .get(request)
            .next()
            .map(instances -> processInstanceResponse(instances));
    }

    private Response<ServiceInstance> processInstanceResponse(
            ServiceInstanceResponse instances) {
        var list = instances.getInstances();
        if (list.isEmpty()) return new EmptyResponse<>();

        // Round Robin: AtomicInteger로 스레드 안전하게 순환
        int pos  = Math.abs(position.getAndIncrement());
        var inst = list.get(pos % list.size());
        return new DefaultResponse<>(inst);
    }
}

/* ── Least Connection 로드 밸런서 ────────────────────────── */
public class LeastConnectionLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    // 서버별 활성 연결 수 추적
    private final ConcurrentHashMap<String, AtomicInteger> connectionCounts
        = new ConcurrentHashMap<>();

    public ServiceInstance selectInstance(List<ServiceInstance> instances) {
        return instances.stream()
            .min(Comparator.comparingInt(inst ->
                connectionCounts
                    .computeIfAbsent(inst.getInstanceId(),
                                      k -> new AtomicInteger(0))
                    .get()
            ))
            .orElseThrow();
    }

    public void incrementConn(String instanceId) {
        connectionCounts.computeIfAbsent(instanceId, k -> new AtomicInteger(0))
                        .incrementAndGet();
    }

    public void decrementConn(String instanceId) {
        connectionCounts.computeIfPresent(instanceId,
            (k, v) -> { v.decrementAndGet(); return v; });
    }
}

/* ── RestTemplate에 로드 밸런서 적용 ────────────────────── */
@Configuration
public class LoadBalancerConfig {

    @Bean
    @LoadBalanced  // 서비스명으로 자동 로드 밸런싱
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

// 사용: 서비스 이름으로 호출 → LB가 자동으로 인스턴스 선택
// restTemplate.getForObject("http://order-service/api/orders", ...)

Spring Cloud LoadBalancer는 서비스 디스커버리(Eureka, Consul)와 연동해 동적으로 인스턴스를 관리합니다.
@LoadBalanced 어노테이션 하나로 RestTemplate에 로드 밸런싱이 적용됩니다.
Least Connection 구현에서 AtomicInteger로 동시성을 처리합니다.
요청 완료 시 반드시 decrementConn을 호출해야 하므로, 실제로는 try-finally나 인터셉터를 사용합니다.
Round Robin에서 position.getAndIncrement()의 오버플로우를 방지하기 위해 Math.abs()를 사용합니다.
Integer.MIN_VALUE의 abs()는 여전히 음수이므로 주의가 필요합니다.

nginx — L7 로드 밸런서 설정

# /etc/nginx/conf.d/load-balancer.conf

# ── upstream 서버 풀 정의 ──────────────────────────────────
upstream backend {
    # Round Robin (기본값)
    server 10.0.0.1:8080 weight=3;  # 가중치 3 (더 많은 요청)
    server 10.0.0.2:8080 weight=2;
    server 10.0.0.3:8080 weight=1;

    # Least Connection 방식
    # least_conn;

    # IP Hash (세션 유지)
    # ip_hash;

    # 헬스 체크 설정
    keepalive 32;  # 연결 재사용
}

upstream api_backend {
    least_conn;  # API는 처리 시간 다양 → Least Connection 적합
    server 10.0.0.4:8081;
    server 10.0.0.5:8081 backup;  # 나머지 전부 다운 시에만 사용
    server 10.0.0.6:8081 down;    # 일시적으로 비활성화
}

server {
    listen 80;

    # L7 콘텐츠 기반 라우팅
    location /api/ {
        proxy_pass http://api_backend;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # keepalive 활성화

        # 헬스 체크 (nginx plus 기능)
        # health_check interval=5 fails=3 passes=2 uri=/health;
    }
}

nginx는 upstream 블록으로 서버 풀을 정의합니다. 기본값은 Round Robin이며, least_conn이나 ip_hash 디렉티브로 방식을 변경합니다.
weight 파라미터로 Weighted Round Robin을 구현합니다. weight=3인 서버는 전체 요청의 3/6=50%를 처리합니다.
backup 서버는 일반 서버가 모두 응답 불가일 때만 활성화됩니다. down은 임시 점검 시 서버를 풀에서 제외합니다.
proxy_http_version 1.1과 Connection="" 설정으로 upstream과의 keepalive 연결을 유지해 연결 오버헤드를 줄입니다.

Q1

Round Robin 로드 밸런싱 방식은?

'돌아가며'라는 이름 그대로입니다.
Round Robin은 들어오는 요청을 서버 목록 순서대로 하나씩 돌아가며 분배해 부하를 균등하게 나눕니다.

Q2

Round Robin 로드 밸런싱 방식은?

'돌아가며'라는 이름 그대로입니다.
Round Robin은 들어오는 요청을 서버 목록 순서대로 하나씩 돌아가며 분배해 부하를 균등하게 나눕니다.

Q3

Least Connections 방식이 Round Robin보다 유리할 수 있는 경우는?

이미 바쁜 서버에 계속 몰아주면 비효율적입니다.
Least Connections는 현재 연결(부하)이 가장 적은 서버로 요청을 보내, 처리 시간이 들쭉날쭉해 특정 서버에 부하가 쏠리는 상황에서 Round Robin보다 균형 있게 분산할 수 있습니다.

Q4

Least Connections 방식이 Round Robin보다 유리할 수 있는 경우는?

이미 바쁜 서버에 계속 몰아주면 비효율적입니다.
Least Connections는 현재 연결(부하)이 가장 적은 서버로 요청을 보내, 처리 시간이 들쭉날쭉해 특정 서버에 부하가 쏠리는 상황에서 Round Robin보다 균형 있게 분산할 수 있습니다.

Q5

IP Hash 방식 로드 밸런싱의 특징은?

같은 클라이언트는 항상 같은 서버로 가야 하는 경우에 씁니다.
IP Hash는 클라이언트 IP를 해시해 항상 같은 서버로 연결시켜, 세션 정보를 서버에 저장하는 경우(sticky session)에 유용합니다.

Q6

IP Hash 방식 로드 밸런싱의 특징은?

같은 클라이언트는 항상 같은 서버로 가야 하는 경우에 씁니다.
IP Hash는 클라이언트 IP를 해시해 항상 같은 서버로 연결시켜, 세션 정보를 서버에 저장하는 경우(sticky session)에 유용합니다.