본문으로 건너뛰기
yutils

load balancer 는 어떻게 동작할까?

L4 vs L7, round-robin / least-connections / IP hash / weighted 알고리즘의 실제 차이, health check, sticky session(session affinity), 그리고 load balancer 자신이 active-passive · anycast 로 고가용성을 유지하는 방식.

약 10분 읽기

서버 한 대로는 트래픽을 감당 못 한다. 그래서 여러 대 띄우고 앞에 load balancer 를 둬서 요청을 나눈다. 말로는 단순한데 실전에서는 "L4 냐 L7 이냐", "어떤 알고리즘", "세션이 한 서버에 붙어야 하나", "죽은 서버를 어떻게 빼나", 그리고 "load balancer 자신이 죽으면?" 이 전부 결정 사항이다. 이 가이드는 그 결정들을 하나씩 정리한다.

왜 필요한가

서버 1대: CPU/메모리 한계 → 동시 접속 한계 → 한 대 죽으면 전체 down

여러 대 + LB:
                 ┌──→ server A
client → [ LB ] ─┼──→ server B
                 └──→ server C

얻는 것:
- 수평 확장 (scale out): 서버 추가로 처리량 ↑
- 고가용성: 한 대 죽어도 LB 가 그 서버로 안 보냄
- 무중단 배포: 서버 한 대씩 빼고 → 배포 → 다시 넣기 (rolling)

L4 vs L7 — 어느 계층에서 분배하나

가장 큰 갈림길. OSI 계층 어디서 트래픽을 보고 결정하느냐다.

L4 (transport, TCP/UDP):
  - 패킷의 IP + port 만 보고 forward
  - 내용(HTTP path, header, cookie)은 안 봄 — 못 봄
  - connection 단위로 한 서버에 묶어 그대로 흘림 (passthrough)
  - 빠르고 가벼움, TLS 그대로 통과(termination 안 함)
  예: AWS NLB, IPVS, HAProxy(tcp mode)

L7 (application, HTTP/HTTPS):
  - HTTP 요청을 실제로 파싱 — path, header, cookie, method
  - "/api 는 backend A, /img 는 backend B" 같은 라우팅 가능
  - TLS termination, 압축, 헤더 주입, 재시도, 캐시
  - 더 똑똑하지만 CPU 비용 ↑ (요청마다 파싱)
  예: AWS ALB, Nginx, HAProxy(http mode), Envoy

간단히: L4 는 "주소만 보고 배달", L7 은 "내용 읽고 배달". 둘을 섞기도 한다 — 바깥은 L4 로 받아 대역폭 처리하고, 안쪽 L7 이 세밀한 라우팅.

분배 알고리즘

"어느 서버로 보낼까" 를 고르는 규칙. 정답은 트래픽 모양에 달렸다.

round-robin (순번):
  A → B → C → A → B → C ...
  단순/공평. 단 서버 성능 같고 요청 비용 비슷할 때만 공평.
  요청 하나가 10초, 다른 건 10ms 면 → 불균형.

weighted round-robin (가중 순번):
  A(weight 3) → A → A → B(weight 1) → A → A → A → B ...
  서버 스펙 다를 때. 8코어:4코어 = weight 2:1.

least-connections (최소 연결):
  현재 active connection 가장 적은 서버로.
  요청 길이가 들쭉날쭉할 때 round-robin 보다 균형 잘 맞음.
  long-lived connection(웹소켓 등)에 특히 유리.

least-response-time:
  연결 수 + 평균 응답시간 조합. 느려진 서버 자동 회피.

IP hash:
  hash(client IP) % 서버수 → 항상 같은 서버.
  같은 클라이언트를 같은 서버에 고정(아래 sticky session 의 한 방법).
  서버 수 바뀌면 대부분 재배치됨(consistent hashing 으로 완화).

Health check — 죽은 서버 빼기

LB 의 핵심 기능. 뒤 서버가 살아있는지 주기적으로 확인하고, 죽었으면 풀에서 빼서 트래픽을 안 보낸다.

수동(passive) health check:
  실제 트래픽의 실패를 관찰 — 연속 5xx/timeout 나면 그 서버 제외.
  추가 요청 없음(가벼움). 단 첫 사용자가 실패를 떠안음.

능동(active) health check:
  LB 가 주기적으로 probe (예: 5초마다 GET /healthz)
  - 연속 N회 실패 → unhealthy → 풀에서 제외
  - 다시 연속 M회 성공 → healthy → 복귀
  사용자 요청 전에 미리 감지.

좋은 /healthz 설계:
  - 단순 200 만 X — DB/캐시 등 의존성도 점검(shallow vs deep)
  - 단 너무 deep 하면 DB 살짝 흔들려도 전 서버 unhealthy → 전멸
  - "나는 요청 처리 가능한가" 만 답하게(liveness vs readiness 구분)

Sticky session (session affinity)

같은 클라이언트의 요청을 매번 같은 서버로 보내는 것. 서버가 세션 상태를 자기 메모리에 들고 있을 때 필요하다.

왜 필요?
  로그인 세션을 server A 메모리에 저장 → 다음 요청이 B 로 가면
  "로그인 안 됨" 상태 → 깨짐.

방법:
  1) IP hash — client IP 기준 고정. 단 NAT 뒤 수천 명이 한 IP면 쏠림.
  2) cookie 기반 — LB 가 어느 서버인지 cookie 심음(예: AWSALB).
     가장 정확. NAT 영향 없음.

대가:
  - 균형이 깨짐(특정 서버에 무거운 사용자 몰림)
  - 그 서버 죽으면 → 거기 묶인 세션 전부 날아감

더 나은 길 — stateless 서버:
  세션을 서버 메모리가 아닌 공유 저장소(Redis/DB)나 JWT 로.
  그러면 sticky 불필요 → 어느 서버든 처리 가능 → 균형/장애 내성 ↑.
  실무 권장: 가능하면 sticky 를 없애는 방향으로 설계.

LB 자신의 고가용성

LB 가 single point of failure 면 의미가 없다. LB 자체도 이중화한다.

active-passive (failover):
  LB1(active) + LB2(standby) 가 하나의 가상 IP(VIP) 공유.
  - VRRP/keepalived 로 서로 heartbeat
  - active 죽으면 → passive 가 VIP 인수(takeover) → 트래픽 이어받음
  - standby 는 평소 놀고 있음(비용)

active-active:
  여러 LB 가 동시에 트래픽 받음. 앞단 분배 필요 →
  보통 DNS round-robin 또는 anycast 로 해결.

anycast:
  같은 IP 를 여러 지역 LB 가 동시에 광고(BGP).
  네트워크가 "가장 가까운" LB 로 자동 라우팅.
  - 한 지역 LB/회선 죽으면 → BGP 가 다음 가까운 곳으로 재수렴
  - 지연 ↓ + 장애 격리. CDN/대형 서비스의 표준.

전체 그림

               DNS (anycast IP 하나 반환)
                      │
        ┌─────────────┼─────────────┐
   [LB region A]  [LB region B]  [LB region C]   ← anycast, 가까운 곳
        │
   L7 라우팅 + health check + (가능하면 no sticky)
        │
   ┌────┼────┐
  srv  srv  srv   ← stateless, 세션은 Redis/JWT
        │
   공유 세션 저장소 / DB

흔한 함정

  • health check 가 너무 deep — DB 가 잠깐 느려지면 모든 서버가 unhealthy 로 빠져 전멸. liveness(나 살아있나) 와 readiness(트래픽 받을 준비) 를 분리하라.
  • sticky 에 의존하다 서버 죽음 — 묶인 세션이 통째로 증발. 가능하면 stateless 로 가고, 어쩔 수 없으면 세션을 공유 저장소에 둬라.
  • least-connections 인데 connection ≠ 부하 — 어떤 요청은 연결만 잡고 거의 idle(웹소켓), 어떤 건 짧지만 CPU 폭발. 연결 수가 실제 부하를 반영하는지 확인.
  • graceful shutdown 없이 서버 제거 — 진행 중인 요청이 끊김. 배포 시 "draining"(새 요청은 안 받고 기존 요청만 마무리) 단계를 둬라.
  • thundering herd on recovery — 죽었던 서버가 돌아오자마자 모든 health check 가 동시에 healthy 판정 → 트래픽이 한꺼번에 쏠려 다시 죽음. slow start(가중치 점진 증가) 로 완화.
  • TLS termination 위치 혼동 — L7 에서 termination 하면 LB→서버 구간이 평문일 수 있다. 내부도 암호화할지(re-encrypt) 정책으로 결정.

마무리

Load balancer 는 단순히 "요청 나누기" 가 아니라 L4/L7 선택 · 알고리즘 · health check · 세션 전략 · 자신의 이중화 가 한 묶음인 결정이다. 핵심 원칙 하나만 챙기면: 서버를 stateless 하게 만들수록 LB 가 단순하고 강해진다. 그러면 sticky session 도, "그 서버 죽으면 세션 날아감" 도 사라진다.

가이드 목록으로