LLM WikiAccess-protected knowledge portal
← 스터디 홈
4편 · 약 28분

로드밸런서: L4/L7, 헬스체크, 세션 어피니티

로드밸런서를 블랙박스로 두면 장애가 된다

롤링 배포 중 신규 인스턴스의 헬스체크가 200을 반환했다. 로드밸런서는 즉시 트래픽을 보내기 시작했다. 그런데 DB 커넥션 풀이 아직 워밍업되지 않은 상태에서 쿼리가 밀려들어 오류율이 치솟았다. 헬스체크 경로가 DB 연결을 실제로 확인하지 않는 shallow check였기 때문이다.

다른 장애에서는 배포 직후 503이 산발적으로 발생했다. 구 인스턴스를 LB에서 제거했지만 deregistration delay 없이 프로세스를 먼저 종료한 탓에, LB가 아직 보내던 연결이 끊겼다.

로드밸런서는 투명한 인프라처럼 느껴지지만 헬스체크 방식, 알고리즘, 세션 어피니티 정책, 드레이닝 시간을 잘못 설정하면 그 자체가 장애 원인이 된다.


L4 vs L7: 어느 레이어에서 라우팅하는가

OSI 모델에서 L4 로드밸런서는 전송 계층에서 동작한다. 패킷의 소스 IP, 목적지 IP, 포트, 프로토콜로 구성된 5-tuple만 보고 백엔드를 선택한다. HTTP 헤더나 URL은 읽을 수 없다. L7 로드밸런서는 애플리케이션 계층에서 동작한다. 클라이언트의 TCP 연결을 직접 종료하고, HTTP 헤더·Host·URL 경로·쿠키를 파싱한 뒤 라우팅을 결정하고, 선택한 백엔드로 새 TCP 연결을 맺어 요청을 전달한다.

L4 로드밸런서 클라이언트 src: 1.2.3.4:54321 L4 LB 5-tuple → 백엔드 선택 backend 1 backend 2 TCP 연결 모델: NAT 변환으로 패킷 전달 클라이언트 ─TCP─ LB(dst IP 재작성) ─TCP─ 백엔드 라우팅 기준 / 특성 • src_ip, dst_ip, src_port, dst_port, protocol • HTTP 헤더·URL·쿠키 불가시 (TLS 암호화 여부 무관) ✅ 모든 TCP/UDP 프로토콜 지원 (DB, DNS, SSH, 게임 서버) ✅ 수백만 RPS, 서브밀리초 오버헤드 ✅ DSR 모드 시 클라이언트 IP 백엔드까지 보존 가능 ⚠ Host/Path 라우팅, WAF, 인증 기능 없음 대표: AWS NLB, LVS, DPDK 기반 LB 적합: DB 프록시, UDP, 고처리량 TCP, 정적 IP 필요 서비스 커넥션 테이블: 플로우당 ~200–500 bytes 메모리 사용 TIME_WAIT 처리: FIN 이후 최대 240초 엔트리 유지 L7 로드밸런서 클라이언트 GET /api/users HTTP/1.1 L7 LB HTTP 파싱 · SSL 종료 backend 1 backend 2 TCP A TCP B 연결 모델: 두 개의 독립 TCP 연결 클라이언트 ─TCP A─ LB(파싱) ─TCP B(풀링)─ 백엔드 라우팅 기준 / 특성 • Host 헤더, URL 경로, HTTP 메서드, 쿠키 • 하나의 LB로 여러 도메인 / 서비스 처리 ✅ SSL/TLS 종료, gzip 압축, 요청 버퍼링·재시도 ✅ WAF, OIDC/Cognito 인증, 쿠키 삽입 ✅ gRPC, WebSocket, HTTP/2 네이티브 지원 ⚠ TLS 핸드셰이크 + HTTP 파싱 오버헤드: ~1–2ms ⚠ 클라이언트 IP는 X-Forwarded-For로 복원 필요 대표: AWS ALB, Nginx, HAProxy, Envoy, Cloudflare 적합: HTTP 마이크로서비스, gRPC, 인증 필요, 다중 도메인 백엔드 커넥션 풀: keepalive로 3-way 핸드셰이크 재사용
L4 vs L7 로드밸런서 아키텍처 비교

L4 동작 방식: NAT 모드와 DSR

NAT 모드

가장 일반적인 L4 LB 방식이다. 패킷이 들어오면 LB는 목적지 IP를 VIP(가상 IP)에서 선택한 백엔드 IP로 바꾸고(DNAT), 응답 패킷은 역방향으로 소스 IP를 다시 VIP로 바꿔(SNAT) 클라이언트에 전달한다. 모든 트래픽이 LB를 통과한다.

SNAT 포트 소진 문제: LB가 소스 IP도 변환하면, 백엔드 한 대와 연결할 수 있는 세션 수는 LB의 에페머럴 포트 범위(약 55,000개)로 제한된다. 백엔드 수가 늘거나, LB IP 추가로 대응한다.

커넥션 테이블: LB는 각 TCP 플로우의 NAT 변환 정보를 메모리에 저장한다. 메모리 2 GB 기준 약 400만~1,000만 동시 플로우를 추적할 수 있다. TIME_WAIT 상태 엔트리는 연결 종료 후 최대 240초 동안 유지된다.

DSR (Direct Server Return)

응답 트래픽이 많은 서비스(스트리밍, 대용량 파일 다운로드)에서 LB의 아웃바운드 대역폭이 병목이 된다. DSR은 이 문제를 해결한다. LB는 목적지 MAC 주소만 백엔드 서버로 바꿔 패킷을 전달한다. 백엔드는 요청을 처리하고 LB를 거치지 않고 클라이언트에게 직접 응답한다.

DSR을 위해 백엔드 서버는 VIP를 루프백 인터페이스에 추가하고, 해당 IP에 대한 ARP를 억제해야 한다.

# 백엔드 서버: VIP를 lo에 등록, ARP 억제
ip addr add 10.0.0.100/32 dev lo
sysctl -w net.ipv4.conf.lo.arp_ignore=1
sysctl -w net.ipv4.conf.lo.arp_announce=2

DSR의 제약: LB와 백엔드가 동일 L2 세그먼트에 있어야 한다. 다른 서브넷이라면 IP-in-IP 터널링(LVS-TUN)을 써야 한다.


L7 동작 방식: SSL 오프로딩과 백엔드 커넥션 풀

SSL 오프로딩

L7 LB는 TLS 인증서와 개인키를 보유하고 클라이언트와의 TLS 핸드셰이크를 직접 처리한다. 복호화된 HTTP 요청을 파싱해 라우팅하고, 백엔드로는 평문 HTTP(또는 재암호화)로 전달한다.

장점: 인증서 갱신을 한 곳에서 관리, CPU 집약적인 비대칭 암호화 연산을 백엔드에서 제거, TLS 세션 재사용(session ticket)으로 재연결 RTT 감소.

# Nginx SSL 오프로딩 핵심 설정
ssl_certificate     /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
ssl_protocols       TLSv1.2 TLSv1.3;
ssl_session_cache   shared:SSL:10m;   # 세션 티켓 캐시
ssl_session_timeout 1d;

# 백엔드로 평문 HTTP 전달
proxy_pass http://backend_upstream;
proxy_set_header X-Forwarded-Proto $scheme;

백엔드 커넥션 풀

L7 LB는 백엔드와의 TCP 연결을 keepalive로 재사용한다. 각 요청마다 3-way 핸드셰이크를 새로 맺지 않아도 된다. 클라이언트와의 연결(TCP A)과 백엔드와의 연결(TCP B)은 독립적이므로, 클라이언트가 연결을 끊어도 백엔드 연결은 풀에서 재사용된다.

# Nginx: 백엔드 keepalive 풀
upstream backend {
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
    keepalive 32;          # 워커당 유지할 유휴 연결 수
    keepalive_timeout 65s;
}

Host/Path 기반 라우팅

# HAProxy: 호스트 기반 라우팅
frontend https_in
    bind *:443 ssl crt /etc/ssl/certs/combined.pem
    mode http
    acl is_api   hdr(host) -i api.example.com
    acl is_static path_beg /static/
    use_backend api_servers    if is_api
    use_backend static_servers if is_static
    default_backend web_servers

헬스체크 메커니즘

헬스체크는 unhealthy 백엔드를 자동으로 풀에서 제거하고, 회복되면 다시 추가하는 시스템이다. 세 가지 방식이 있다.

TCP 헬스체크: LB가 백엔드 포트로 TCP 3-way 핸드셰이크를 시도한다. 핸드셰이크가 완료되면 healthy 판정. 빠르지만 포트가 열려 있어도 앱이 내부적으로 멈춰 있을 수 있다.

HTTP 헬스체크: LB가 지정한 경로(예: /health)로 HTTP 요청을 보내고 기대하는 상태 코드(예: 200)를 확인한다. 앱 의존성(DB 연결, 캐시 연결)을 포함한 deep check를 구현하면 실제 서비스 가능 여부를 정확히 판단할 수 있다.

gRPC 헬스체크: gRPC 표준 프로토콜(grpc.health.v1.Health/Check)을 호출해 SERVING 응답을 확인한다. AWS ALB는 gRPC 헬스체크를 네이티브 지원한다.

헬스체크 상태 전이
HEALTHY
트래픽 수신 중
fall N 연속 실패 → ← rise N 연속 성공
UNHEALTHY
풀에서 제거됨
핵심 파라미터
interval 프로브 간격. 일반: 5–10s
timeout 응답 대기 최대 시간. 3–5s
fall (unhealthy) 연속 실패 N회 → 제거. 보통 3
rise (healthy) 연속 성공 N회 → 복귀. 보통 2
deep check DB·캐시 연결도 확인하는 /health
HAProxy 헬스체크 설정 예시
option httpchk GET /health HTTP/1.1\r\nHost:\ example.com http-check expect status 200 server web1 10.0.1.11:8080 check inter 5s fall 3 rise 2
주의: 배포 시 워밍업 vs 헬스체크 타이밍
프로세스 시작 /health 200 반환 시작 rise N 성공 확인 풀에 추가됨
DB 커넥션 풀·캐시 워밍업이 완료된 뒤에야 /health를 200으로 반환하도록 설계해야 한다.
헬스체크 상태 머신과 핵심 파라미터

세션 어피니티 (스티키 세션)

전통적인 웹 앱은 사용자 세션 데이터를 서버 메모리에 저장한다. 두 번째 요청이 다른 서버로 라우팅되면 세션이 사라진다. 이를 방지하는 것이 세션 어피니티다.

올바른 해법은 세션 상태를 외부화하는 것이다(Redis, DB). 스티키 세션은 외부화 전환 중 또는 전환이 불가한 레거시 앱을 위한 임시 방편이다.

쿠키 기반 어피니티

LB가 첫 응답에 Set-Cookie 헤더를 삽입한다. 이후 요청에서 해당 쿠키를 읽어 동일 백엔드로 라우팅한다.

# HAProxy 쿠키 기반 스티키 세션
backend web_servers
    balance roundrobin
    cookie SERVERID insert indirect nocache
    server web1 10.0.1.11:8080 check cookie web1
    server web2 10.0.1.12:8080 check cookie web2

insert: LB가 쿠키를 삽입. indirect: 쿠키를 백엔드로 전달하지 않음. nocache: CDN이 쿠키를 캐시하지 못하도록 Cache-Control: no-cache 추가.

AWS ALB는 AWSALB 쿠키를 자동 삽입한다. 지속 시간은 1초~7일로 설정 가능하다.

IP 해시 어피니티

backend_index = hash(client_ip) % 백엔드_수로 고정 라우팅한다. 쿠키 없이도 동일 클라이언트를 동일 서버로 보낼 수 있다.

# Nginx IP 해시
upstream backend {
    ip_hash;
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
}

IP 해시의 함정: 기업 NAT 환경에서는 수천 명의 사용자가 같은 공인 IP를 공유한다. 이 경우 해당 IP가 해시하는 백엔드 한 대로 트래픽이 집중된다. 또한 백엔드 수가 바뀌면 기존 매핑의 대부분이 깨진다. 일관 해시(Consistent Hashing) 는 백엔드가 추가/제거될 때 재매핑 비율을 1/N으로 줄인다.

스티키 세션 공통 문제점

문제내용
부하 불균형"무거운" 사용자가 몰린 백엔드만 과부하
장애 시 세션 유실백엔드 크래시 시 해당 서버에 고정된 세션 전부 소실
오토스케일링 마찰신규 인스턴스가 세션 없어 즉시 거의 트래픽 미수신
롤링 배포 지연기존 핀된 세션이 드레이닝될 때까지 구 서버 유지 필요

로드밸런싱 알고리즘

알고리즘동작적합한 상황주의
Round Robin순서대로 분배요청 처리시간이 균일한 환경느린 백엔드에도 계속 보냄
Weighted Round Robin가중치 비율로 분배사양이 다른 서버 혼합 운영가중치를 수동으로 관리해야 함
Least Connections현재 연결 수가 가장 적은 서버로처리 시간 편차가 큰 워크로드연결 수 ≠ CPU 부하
IP Hash클라이언트 IP 해시로 고정세션 어피니티 필요기업 NAT 핫스팟, 재배포 시 리매핑
P2C (Power of Two Choices)2개 무작위 선택 후 더 가벼운 쪽으로분산 LB, 대규모 플릿전역 최적은 아니지만 실용적

P2C(Power of Two Choices) 는 Mitzenmacher(1996)가 제안한 알고리즘이다. 무작위로 백엔드 2개를 뽑아 더 부하가 낮은 쪽으로 보낸다. 순수 랜덤 대비 최대 부하가 지수적으로 개선되며(O(log log N)), 전역 상태 공유 없이 각 LB 인스턴스가 독립적으로 결정할 수 있어 분산 환경에 적합하다. Envoy의 LEAST_REQUEST 정책과 HAProxy의 balance random(2)가 이 방식을 채용한다.

# HAProxy P2C
backend web_servers
    balance random(2)
    server web1 10.0.1.11:8080 check
    server web2 10.0.1.12:8080 check
    server web3 10.0.1.13:8080 check

AWS ALB vs NLB

항목ALB (Application LB)NLB (Network LB)
OSI 레이어L7L4
프로토콜HTTP, HTTPS, HTTP/2, gRPC, WebSocketTCP, UDP, TLS, TCP_UDP
라우팅 기준Host, Path, 헤더, 쿼리 파라미터IP/포트 5-tuple
클라이언트 IPX-Forwarded-For 헤더백엔드에서 직접 보임 (SNAT 없음)
정적 IP없음 (DNS 기반, IP 변경 가능)AZ당 Elastic IP 고정
레이턴시~1–2ms 오버헤드서브밀리초
최대 처리량~수십만~백만 RPS수백만 RPS
TLS 종료지원 (ACM 연동)선택적 (TLS 리스너)
헬스체크HTTP, HTTPS, gRPCTCP, HTTP, HTTPS
스티키 세션AWSALB 쿠키플로우 해시 (연결 단위)
Lambda 타겟지원미지원
인증 (OIDC)지원미지원
UDP 지원미지원지원
사용 기준HTTP 마이크로서비스, gRPC, 인증, WebSocketUDP, 게임, 커스텀 TCP, 정적 IP, 최고 처리량

커넥션 드레이닝: 무중단 배포의 핵심

배포, 오토스케일 스케일인, 유지보수 시 백엔드를 LB 풀에서 제거할 때 이미 처리 중인 요청을 완료시킨 뒤 서버를 내리는 것이 커넥션 드레이닝이다.

AWS ALB/NLB Deregistration Delay

  • 기본값: 300초 (설정 범위: 0–3600초)
  • 타겟 등록 해제 시: LB는 해당 타겟을 "Draining" 상태로 표시 → 신규 요청 차단 → 기존 연결은 완료 허용 → 딜레이 만료 또는 모든 연결 종료 시 완전 제거
  • 권장 설정: p99 요청 처리 시간 + 여유 시간. 빠른 REST API는 30–60초, WebSocket/스트리밍은 300–600초.
  • 중요: 백엔드 프로세스가 딜레이 만료 전에 종료되면 연결이 강제로 끊겨 5xx 발생. 프로세스는 드레이닝 완료 후 종료해야 한다.
# AWS CLI: deregistration delay 수정
aws elbv2 modify-target-group-attributes \
  --target-group-arn arn:aws:elasticloadbalancing:... \
  --attributes Key=deregistration_delay.timeout_seconds,Value=60

HAProxy DRAIN/MAINT

# 드레이닝: 신규 연결 차단, 기존 연결은 완료
echo "set server web_pool/web1 state drain" | socat stdio /run/haproxy/admin.sock

# 세션 수가 0이 되면 서버 재시작 후 복귀
echo "set server web_pool/web1 state ready" | socat stdio /run/haproxy/admin.sock

DRAIN: 기존 연결 정상 완료 허용. MAINT: 기존 연결도 즉시 강제 종료. 긴급 제거 시에만 사용.


X-Forwarded-For와 클라이언트 IP 복원

L7 LB가 요청을 전달하면 백엔드의 $remote_addr는 LB의 IP가 된다. 원래 클라이언트 IP를 복원하기 위해 헤더를 사용한다.

X-Forwarded-For (XFF): 요청이 거친 프록시 IP를 쉼표로 이어 기록한다. Nginx는 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;로 기존 XFF에 현재 $remote_addr를 덧붙인다. AWS ALB는 자동으로 추가한다.

X-Real-IP: 체인 없이 클라이언트 IP 하나만 포함한다. 첫 번째 프록시에서 설정한다.

X-Forwarded-Proto: 클라이언트가 사용한 프로토콜(http/https)을 백엔드에 전달한다. 리다이렉트 URL 생성과 HSTS 처리에 필수다.

# Nginx 헤더 삽입 관용 설정
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host              $host;

XFF 신뢰 주의: X-Forwarded-For: 127.0.0.1을 공격자가 조작해 보낼 수 있다. 접근 제어에 XFF를 그대로 신뢰하면 안 된다. 실제 LB에서 덧붙인 마지막 IP만 신뢰하거나, PROXY Protocol(L4 수준에서 클라이언트 IP를 전달하는 방식)을 사용해야 한다.


핵심 요약

  • L4 LB는 5-tuple만 보고 라우팅한다. 처리 비용이 낮고 모든 TCP/UDP 프로토콜에 동작하지만, Host/Path 라우팅과 SSL 종료가 불가하다.
  • L7 LB는 HTTP 헤더 기반 라우팅, SSL 오프로딩, 재시도를 제공한다. 두 개의 독립 TCP 연결로 동작하며 ~1–2ms 오버헤드가 있다.
  • 헬스체크는 interval/timeout/fall/rise로 설정한다. DB 연결을 실제로 확인하는 deep check가 shallow check보다 훨씬 안전하다.
  • 세션 어피니티는 임시 방편이다. 궁극적 해법은 세션 상태 외부화(Redis 등)다. IP 해시는 기업 NAT 환경에서 핫스팟을 만든다.
  • AWS ALB는 HTTP/gRPC/WebSocket·인증·WAF에, NLB는 UDP·정적 IP·최고 처리량에 적합하다.
  • 커넥션 드레이닝을 빠뜨리면 배포마다 5xx가 발생한다. deregistration delay를 p99 요청 시간보다 충분히 크게 설정하고, 프로세스 종료 전에 드레이닝이 완료될 수 있도록 배포 스크립트를 설계한다.

References

  • Loadbalancer.org, "Layer 4 vs Layer 7 load balancing — we still love DSR", https://www.loadbalancer.org/blog/layer-4-vs-layer-7-load-balancing-we-still-love-dsr/
  • Loadbalancer.org, "Complete guide to Layer 4 load balancing", https://www.loadbalancer.org/blog/complete-guide-to-layer-4-load-balancing/
  • HAProxy Technologies, "Load Balancing: Affinity, Persistence, Sticky Sessions", https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know
  • HAProxy Technologies, "Power of Two Load Balancing", https://www.haproxy.com/blog/power-of-two-load-balancing
  • gRPC, "Health Checking Protocol", https://grpc.io/docs/guides/health-checking/
  • Cloudcraft, "ALB vs NLB: Which AWS load balancer fits your needs?", https://blog.cloudcraft.co/alb-vs-nlb-which-aws-load-balancer-fits-your-needs/
  • AWS, "Deregistration delay — Elastic Load Balancing", https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#deregistration-delay
  • Envoy Proxy, "Load Balancer — Supported load balancers", https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers
  • Envoy Proxy, "Draining", https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/draining
  • NGINX, "HTTP Load Balancing", https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/
  • HAProxy Technologies, "Health Checks documentation", https://www.haproxy.com/documentation/haproxy-configuration-tutorials/reliability/health-checks/
  • Imperva, "Sticky Sessions (Session Persistence)", https://www.imperva.com/learn/availability/sticky-session-persistence-and-cookies/