로드밸런서: 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 동작 방식: 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=2DSR의 제약: 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 헬스체크를 네이티브 지원한다.
트래픽 수신 중
풀에서 제거됨
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
세션 어피니티 (스티키 세션)
전통적인 웹 앱은 사용자 세션 데이터를 서버 메모리에 저장한다. 두 번째 요청이 다른 서버로 라우팅되면 세션이 사라진다. 이를 방지하는 것이 세션 어피니티다.
올바른 해법은 세션 상태를 외부화하는 것이다(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 web2insert: 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 checkAWS ALB vs NLB
| 항목 | ALB (Application LB) | NLB (Network LB) |
|---|---|---|
| OSI 레이어 | L7 | L4 |
| 프로토콜 | HTTP, HTTPS, HTTP/2, gRPC, WebSocket | TCP, UDP, TLS, TCP_UDP |
| 라우팅 기준 | Host, Path, 헤더, 쿼리 파라미터 | IP/포트 5-tuple |
| 클라이언트 IP | X-Forwarded-For 헤더 | 백엔드에서 직접 보임 (SNAT 없음) |
| 정적 IP | 없음 (DNS 기반, IP 변경 가능) | AZ당 Elastic IP 고정 |
| 레이턴시 | ~1–2ms 오버헤드 | 서브밀리초 |
| 최대 처리량 | ~수십만~백만 RPS | 수백만 RPS |
| TLS 종료 | 지원 (ACM 연동) | 선택적 (TLS 리스너) |
| 헬스체크 | HTTP, HTTPS, gRPC | TCP, HTTP, HTTPS |
| 스티키 세션 | AWSALB 쿠키 | 플로우 해시 (연결 단위) |
| Lambda 타겟 | 지원 | 미지원 |
| 인증 (OIDC) | 지원 | 미지원 |
| UDP 지원 | 미지원 | 지원 |
| 사용 기준 | HTTP 마이크로서비스, gRPC, 인증, WebSocket | UDP, 게임, 커스텀 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=60HAProxy 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.sockDRAIN: 기존 연결 정상 완료 허용. 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/