1. 프롤로그 - 서버 한 대로는 감당이 안 된다
로드 밸런서를 처음 공부하게 된 건 단순한 의문에서 시작했다.
트래픽이 몰리면 서버 한 대로는 감당이 안 된다. 그래서 서버를 여러 대 띄운다(Scale-out). 그런데 사용자들은 어떻게 여러 서버에 분산되는 걸까?
이 질문의 답이 로드 밸런서(Load Balancer)다.
실제로 커뮤니티에 링크 하나가 올라가거나, 마케팅 이벤트가 터지면 서버에 갑자기 수십 배의 트래픽이 몰리는 사례는 흔하다. 싱글 서버를 운영하던 AWS EC2 인스턴스가 CPU 100%를 찍으며 먹통이 되어버리는 것이다.
"맛집의 줄 서기" 비유가 와닿았다.
유명한 맛집에 손님이 1,000명이 몰렸다. 주방장 혼자서는 절대 처리 못 한다. 그래서 주방장을 10명으로 늘렸다 (Scale-out). 그런데 손님들이 주방장 1번한테만 줄을 선다. 2번~10번 주방장은 논다.
이때 입구에서 "자, 1번 손님은 1번 주방장, 2번 손님은 2번 주방장으로 가세요!"라고 교통정리를 해주는 지배인이 필요하다.
이 지배인이 바로 로드 밸런서다. Nginx를 설치하고 로드 밸런싱 설정을 직접 짜보면서 "고가용성(High Availability)"이라는 개념이 비로소 손에 잡혔다.
2. 혼란의 시작 - L4? L7? 그게 뭔데?
로드 밸런서를 공부하기 시작했을 때, 가장 혼란스러웠던 게 L4 로드 밸런서와 L7 로드 밸런서의 차이였습니다. OSI 7계층은 학교에서 배웠지만, "4계층에서 동작한다"는 게 실제로 무슨 의미인지 와닿지 않았습니다.
처음엔 이렇게 생각했습니다. "L7이 더 높은 숫자니까 L4보다 좋은 거 아닌가?" 완전히 틀린 생각이었습니다. 결국 이거였습니다. "어디까지 패킷을 까보느냐"의 차이였습니다.
L4 로드 밸런서 (전송 계층)
L4는 IP 주소 + 포트 번호만 봅니다. 패킷 내용은 절대 안 봅니다. 단순히 "들어온 패킷을 저쪽 서버로 토스"만 합니다.
비유하자면, "너 빨간 모자 썼네? 저쪽으로 가." (단순 외형 보고 분류)
장점:
- 매우 빠르고 효율적입니다. 패킷 내용을 분석하지 않으니까요.
- TLS(HTTPS) 암호화/복호화를 할 필요가 없습니다.
- 낮은 지연시간(Latency)이 중요한 금융권 시스템, 게임 서버에 적합합니다.
단점:
- 섬세한 라우팅이 불가능합니다. 예를 들어
/api는 A서버로,/image는 B서버로 보내는 게 불가능합니다. - HTTP 요청 내용을 모르니 쿠키 기반의 세션 유지도 복잡합니다.
실제 사례: AWS NLB (Network Load Balancer), LVS (Linux Virtual Server), F5 Big-IP.
L7 로드 밸런서 (응용 계층)
L7은 URL, HTTP 헤더, 쿠키, 심지어 Body 내용까지 다 봅니다. 패킷을 뜯어서 HTTP 요청 내용을 이해합니다.
비유하자면, "너 피자 주문하러 왔어? 피자는 2층이야. 커피 마시러 왔어? 1층이야." (내용 듣고 분류)
장점:
- 똑똑한 라우팅이 가능합니다. MSA(Microservices Architecture)에서 필수입니다.
- SSL Termination 기능으로 인증서 처리를 로드 밸런서에서 일괄 처리할 수 있습니다.
- 콘텐츠 기반 라우팅(예: User-Agent로 모바일/PC 서버 분리)이 가능합니다.
단점:
- 패킷 분석 비용이 들어 L4보다 약간 느립니다.
- CPU와 메모리를 더 많이 사용합니다.
실제 사례: AWS ALB (Application Load Balancer), Nginx, HAProxy, Cloudflare Load Balancer.
저는 처음에 무조건 L7을 써야 한다고 생각했습니다. "더 똑똑하니까 당연히 좋은 거 아냐?" 하지만 제 서비스는 단순한 REST API만 제공하고, 마이크로서비스 구조도 아니었습니다. 결국 L4 로드 밸런서(AWS NLB)로 바꿨더니 응답 속도가 30% 개선되었습니다. "무조건 고급 기술을 쓰는 게 능사가 아니구나"를 배운 순간이었습니다.
3. 결정의 순간 - 어떤 알고리즘을 써야 하나?
로드 밸런서를 설정하면 반드시 선택해야 하는 게 "누구한테 일을 시킬지"를 결정하는 알고리즘이다. 처음에 이걸 단순하게 생각하면 예상치 못한 문제가 생긴다.
1) 라운드 로빈 (Round Robin) - "공평하게 돌아가면서"
순서대로 돌아가면서 하나씩. (A → B → C → A → B...).
가장 직관적인 방식이다. 서버 3대가 모두 같은 스펙이면 무난하다. 하지만 문제가 생기는 상황이 있다.
문제 상황: 서버 중 하나가 t3.small(2GB 램), 나머지 두 개가 t3.large(8GB 램)라면, 라운드 로빈이 무조건 공평하게 나눠주니 t3.small이 먼저 터진다.
해결책: 가중치 라운드 로빈(Weighted Round Robin)으로 바꾼다. A서버가 B서버보다 성능이 4배 좋으면? A에게 4개, B에게 1개 주는 방식이다.
upstream backend {
server 10.0.1.10:8080 weight=4; # t3.large: 가중치 4
server 10.0.1.11:8080 weight=4; # t3.large: 가중치 4
server 10.0.1.12:8080 weight=1; # t3.small: 가중치 1
}
2) 최소 연결 (Least Connections) - "가장 한가한 놈한테"
"지금 가장 한가한(연결 개수가 적은) 놈한테 줘."
이 방식은 처리 시간이 불규칙한 서비스(예: 동영상 인코딩, 이미지 리사이징)에 적합하다. 동영상 썸네일 생성 서버처럼 작업마다 처리 시간이 다른 경우에 응답 시간이 균등해진다.
upstream video_servers {
least_conn; # 최소 연결 알고리즘 적용
server 10.0.2.20:8080;
server 10.0.2.21:8080;
server 10.0.2.22:8080;
}
3) IP 해시 (IP Hash) - "너는 무조건 저 서버로"
특정 사용자(IP)는 무조건 특정 서버로만 보냅니다.
목적: 세션 유지(Sticky Session). 로그인 정보가 A서버 세션에만 있는데 B서버로 튕기면 로그아웃되니까요.
이 방식은 세션 스토어를 Redis로 분리하기 전 임시방편으로 쓸 수 있다. 하지만 문제가 있다.
문제점: 사용자가 VPN이나 모바일 네트워크로 IP가 바뀌면? 세션이 날아갑니다. 그리고 특정 IP에서 트래픽이 몰리면 부하 분산이 불균등해집니다.
교훈: IP Hash는 임시방편일 뿐이다. 세션은 Redis나 외부 DB로 분리하는 게 정답이다.
upstream app_servers {
ip_hash; # IP 해시 알고리즘
server 10.0.3.30:8080;
server 10.0.3.31:8080;
}
4) Consistent Hashing: "서버가 죽어도 괜찮아"
일반적인 해시 방식은 서버 대수가 바뀌면(예: 3대 → 4대) 모든 해시값이 바뀌어 세션이 전부 날아갑니다. 하지만 Consistent Hashing은 서버 하나가 죽어도 영향받는 사용자가 최소화됩니다.
캐시 서버(Memcached) 앞단에 적용하면 효과가 잘 드러난다. 서버 한 대 장애 시 전체 캐시가 초기화되지 않는다.
4. 실제 - Nginx로 L7 로드 밸런서 만들기
Nginx 설정 파일(/etc/nginx/nginx.conf) 몇 줄이면 내 컴퓨터도 로드 밸런서가 된다. EC2 인스턴스 하나에 설치하고 3개의 백엔드 서버 앞에 두는 구성이다.
http {
# 1. 업스트림(백엔드 서버 그룹) 정의
upstream my_backend_servers {
least_conn; # 알고리즘: 최소 연결 방식
server 10.0.0.1:8080 weight=2; # 가중치 2
server 10.0.0.2:8080;
server 10.0.0.3:8080 down; # 유지보수 중 (트래픽 안 보냄)
# Health Check 설정 (Nginx Plus 전용, 오픈소스는 passive만)
# check interval=3000 rise=2 fall=5 timeout=1000;
}
server {
listen 80;
server_name myapp.com;
# 2. 모든 요청을 위 그룹으로 전달 (Reverse Proxy)
location / {
proxy_pass http://my_backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 타임아웃 설정
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
# 3. 경로별 라우팅 (L7의 강점)
location /api/ {
proxy_pass http://api_servers;
}
location /images/ {
proxy_pass http://image_servers;
proxy_cache my_cache; # 캐싱도 가능
}
}
}
Health Check: "너 살았니?"
만약 서버 1대가 죽으면(Health Check 실패), Nginx는 자동으로 그 서버를 명단에서 제외하고 나머지 서버로만 트래픽을 보냅니다. 이것이 Failover입니다.
Nginx 오픈소스 버전은 passive health check만 지원합니다. 즉, 실제 요청이 실패하면 그때 제외하는 방식입니다. 반면 Nginx Plus(유료)나 AWS ALB는 active health check를 지원합니다. 주기적으로 /health 엔드포인트를 찔러보고, 응답이 없으면 미리 제외하는 방식입니다.
비용 관점에서 처음엔 Nginx 오픈소스로 시작하고, 이후 AWS ALB로 마이그레이션하는 흐름이 일반적이다. 그 이유는 다음 섹션에서 다룬다.
5. AWS에서의 선택: NLB vs ALB
AWS에서 로드 밸런서를 설정할 때 부딪히는 선택지를 정리해본다.
AWS NLB (Network Load Balancer) - L4
특징:
- 초당 수백만 요청(Millions of requests per second) 처리 가능.
- 고정 IP 제공. (Elastic IP 할당 가능)
- 극도로 낮은 지연시간(
<100 microseconds).
언제 쓰나:
- TCP/UDP 프로토콜 기반 서비스 (예: 게임 서버, IoT)
- 금융권처럼 초저지연이 필요한 시스템
- TLS Termination이 필요 없는 경우
사례: WebSocket 기반 채팅 서버 앞단에 NLB를 쓰는 경우, ALB는 WebSocket을 지원하긴 하지만 Connection 수가 많아지면 비용이 급증한다. NLB로 바꾸면 월 비용이 상당히 감소한다는 사례가 흔하다.
# AWS CLI로 NLB 생성
aws elbv2 create-load-balancer \
--name my-network-lb \
--type network \
--subnets subnet-abc123 subnet-def456 \
--scheme internet-facing
AWS ALB (Application Load Balancer) - L7
특징:
- HTTP/HTTPS 프로토콜 전용.
- 경로 기반 라우팅(Path-based routing):
/api/*→ 백엔드,/admin/*→ 관리 서버. - 호스트 기반 라우팅:
api.myapp.com→ API서버,www.myapp.com→ 웹서버. - Lambda 함수로 직접 라우팅 가능(Serverless 아키텍처).
- WAF(Web Application Firewall) 통합 가능.
언제 쓰나:
- 마이크로서비스 아키텍처(MSA)
- 컨테이너 기반 서비스(ECS, EKS)
- HTTP/2, WebSocket over HTTP 필요
사례: MSA로 전환하면서 ALB의 진가가 드러난다. /auth/*는 인증 서버로, /payment/*는 결제 서버로, /media/*는 미디어 서버로 자동 분기되니 코드에서 라우팅 로직을 뺄 수 있다.
# AWS CLI로 ALB 생성
aws elbv2 create-load-balancer \
--name my-application-lb \
--type application \
--subnets subnet-abc123 subnet-def456 \
--security-groups sg-123456
# Target Group 생성 (Health Check 포함)
aws elbv2 create-target-group \
--name my-targets \
--protocol HTTP \
--port 80 \
--vpc-id vpc-123456 \
--health-check-protocol HTTP \
--health-check-path /health \
--health-check-interval-seconds 30 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3
Health Check 설정 - 삽질의 기록
초기에 Health Check 설정을 대충 했다가 False Positive 문제를 겪었습니다. 서버는 살아있는데 Health Check가 실패했다고 판단해서 트래픽을 안 보내는 겁니다.
문제 원인: Health Check 경로를 /로 설정했는데, 홈페이지 렌더링이 느려서 타임아웃(5초)을 넘는 경우가 있었습니다.
해결책: 전용 Health Check 엔드포인트 /health를 만들고, 단순히 200 OK만 반환하도록 했습니다.
// Express.js 예시
app.get('/health', (req, res) => {
// DB 연결 체크 (선택)
if (db.isConnected()) {
res.status(200).send('OK');
} else {
res.status(503).send('Service Unavailable');
}
});
깨달은 점: Health Check는 "서버가 요청을 받을 수 있는 상태인가?"를 판단하는 거지, "서비스가 완벽히 동작하는가?"를 판단하는 게 아닙니다. 너무 복잡한 로직(DB 쿼리 10개 실행)을 넣으면 오히려 역효과입니다.
6. 디버깅 삽질 - Sticky Session의 함정
제가 초기에 가장 고생했던 게 "왜 사용자 세션이 자꾸 풀리지?" 문제였습니다.
상황: 로그인한 사용자가 페이지를 새로고침하면 간헐적으로 로그아웃됩니다. 로그를 보니 요청마다 다른 서버로 분산되고 있었습니다.
원인: 세션 데이터를 서버 로컬 메모리(Express Session의 기본 MemoryStore)에 저장하고 있었고, 라운드 로빈 방식이라 요청마다 다른 서버로 가는 겁니다.
시도 1 - Sticky Session 활성화: ALB에서 Target Group Attributes에 Stickiness를 켰습니다.
aws elbv2 modify-target-group-attributes \
--target-group-arn arn:aws:elasticloadbalancing:... \
--attributes Key=stickiness.enabled,Value=true \
Key=stickiness.type,Value=lb_cookie \
Key=stickiness.lb_cookie.duration_seconds,Value=86400
결과: 대부분 해결되었지만, 쿠키를 차단하는 브라우저나 모바일 앱에서는 여전히 문제가 발생했습니다.
시도 2 - Redis Session Store로 마이그레이션: 세션을 외부 Redis에 저장하도록 변경했습니다. 이제 어느 서버가 요청을 받든 Redis에서 세션을 가져오니 문제가 완전히 해결되었습니다.
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: 'redis://my-redis-cluster:6379' });
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'my-secret',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 86400000 } // 24시간
}));
결국 이거였다: Sticky Session은 임시방편이고, Stateless Architecture(세션을 외부에 저장)가 정답이라는 걸 온몸으로 깨달았습니다.
7. 글로벌 확장 - GSLB와 DNS 기반 로드 밸런싱
서비스에 해외 사용자가 늘어나면 새로운 문제가 생긴다. 한국 서버만 있으면 미국 사용자는 응답 속도가 느리다. 이때 쓰는 게 GSLB (Global Server Load Balancing)이다.
DNS 기반 로드 밸런싱의 원리
사용자가 myapp.com을 입력하면, DNS가 사용자의 지리적 위치를 보고 가장 가까운 서버 IP를 반환합니다.
예시:
- 한국 사용자 →
13.124.x.x(서울 리전) - 미국 사용자 →
54.183.x.x(캘리포니아 리전)
AWS Route 53의 Geolocation Routing
AWS Route 53에서 Geolocation Routing Policy를 설정하면 된다.
# Route 53에서 Record Set 생성 (서울 리전)
{
"Name": "myapp.com",
"Type": "A",
"SetIdentifier": "Seoul-Region",
"GeoLocation": {
"ContinentCode": "AS"
},
"AliasTarget": {
"HostedZoneId": "Z1234567890ABC",
"DNSName": "my-seoul-alb-123456.ap-northeast-2.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
# Route 53에서 Record Set 생성 (버지니아 리전)
{
"Name": "myapp.com",
"Type": "A",
"SetIdentifier": "US-Region",
"GeoLocation": {
"CountryCode": "US"
},
"AliasTarget": {
"HostedZoneId": "Z0987654321XYZ",
"DNSName": "my-us-alb-654321.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
효과: 미국 사용자의 평균 응답 속도가 800ms → 200ms로 개선되는 수준의 변화를 볼 수 있다.
DNS 기반 로드 밸런싱의 한계
하지만 한계가 있다. 서울 리전 서버가 다운되어도 DNS는 여전히 그 IP를 반환한다. DNS는 기본적으로 Health Check를 하지 않기 때문이다.
해결책: Route 53의 Health Check 기능을 활성화한다. 서울 서버가 다운되면 자동으로 버지니아 서버 IP를 반환하도록 Failover 설정을 추가한다.
핵심 교훈: DNS 로드 밸런싱은 지리적 분산에는 좋지만, 실시간 부하 분산에는 부족하다. L4/L7 로드 밸런서와 병행해야 완벽하다.
8. Connection Draining: 배포 시 요청 끊김 문제
새 버전을 배포할 때마다 사용자 요청이 중간에 끊기는 문제가 생길 수 있다.
상황: 서버를 종료하는 순간, 로드 밸런서는 아직 그 서버로 요청을 보내고 있다. 요청은 처리 중인데 서버가 꺼지니 502 Bad Gateway 에러가 발생한다.
해결책: Connection Draining (또는 Deregistration Delay)
ALB에서 Target Group의 Deregistration Delay를 300초(5분)로 설정했습니다. 서버를 종료 요청하면, 로드 밸런서는:
- 새 요청은 더 이상 이 서버로 안 보냄.
- 기존에 처리 중인 요청은 300초 동안 완료될 때까지 기다림.
- 300초 후에야 완전히 제거.
aws elbv2 modify-target-group-attributes \
--target-group-arn arn:aws:elasticloadbalancing:... \
--attributes Key=deregistration_delay.timeout_seconds,Value=300
코드 레벨에서의 Graceful Shutdown:
Node.js 서버에서도 SIGTERM 신호를 받으면 즉시 종료하지 않고, 진행 중인 요청을 완료하도록 코드를 수정했습니다.
const server = app.listen(8080);
process.on('SIGTERM', () => {
console.log('SIGTERM received. Closing server gracefully...');
server.close(() => {
console.log('All connections closed. Exiting process.');
process.exit(0);
});
// 30초 후에도 종료 안 되면 강제 종료
setTimeout(() => {
console.error('Forced shutdown after 30s timeout.');
process.exit(1);
}, 30000);
});
결과: 배포 중 502 에러가 완전히 사라진다. 이것이 "무중단 배포(Zero-Downtime Deployment)"의 실체다.
9. Blue-Green 배포와 로드 밸런서
Connection Draining을 넘어서, 더 안전한 배포 전략이 Blue-Green 배포다.
개념:
- Blue: 현재 운영 중인 서버 그룹 (예: v1.0)
- Green: 새로 배포할 서버 그룹 (예: v1.1)
배포 절차:
- Green 환경에 새 버전을 배포하고 테스트합니다.
- 로드 밸런서의 트래픽을 Blue → Green으로 전환합니다.
- 문제 발생 시 즉시 Blue로 롤백합니다.
AWS ALB에서의 구현:
Target Group을 두 개 만들고, ALB Listener Rule의 가중치(Weight)를 조정합니다.
# Blue Target Group에 100% 트래픽
aws elbv2 modify-listener \
--listener-arn arn:aws:elasticloadbalancing:... \
--default-actions Type=forward,TargetGroupArn=arn:blue-tg,Weight=100
# 트래픽을 Green으로 천천히 이동 (Canary 배포)
# Blue 90%, Green 10%
aws elbv2 modify-rule \
--rule-arn arn:aws:elasticloadbalancing:... \
--actions Type=forward,ForwardConfig='{
"TargetGroups": [
{"TargetGroupArn": "arn:blue-tg", "Weight": 90},
{"TargetGroupArn": "arn:green-tg", "Weight": 10}
]
}'
# 문제 없으면 Green 100%로 전환
aws elbv2 modify-listener \
--listener-arn arn:aws:elasticloadbalancing:... \
--default-actions Type=forward,TargetGroupArn=arn:green-tg,Weight=100
효과: 배포 리스크가 급격히 줄어든다. "언제든 롤백할 수 있다"는 확신이 생기니 더 자주 배포하게 된다.
10. SPOF와 고가용성 - 로드 밸런서도 죽는다
로드 밸런서가 모든 트래픽의 입구인데, 로드 밸런서 자체가 죽으면 어떻게 되나요?
전체 서비스가 마비됩니다. 이것을 SPOF (Single Point of Failure)라고 합니다.
해결책: 로드 밸런서도 2대(Active-Passive)를 둡니다.
Keepalived와 VRRP 프로토콜
온프레미스 환경에서는 Keepalived를 사용해 두 대의 로드 밸런서가 VIP (Virtual IP)를 공유하도록 설정합니다.
- Active: 평소에 VIP를 가지고 일합니다.
- Passive: 옆에서 지켜보다가 Active가 Heartbeat 신호를 보내지 않으면 즉시 VIP를 넘겨받아 투입됩니다.
# Keepalived 설정 (/etc/keepalived/keepalived.conf)
vrrp_instance VI_1 {
state MASTER # MASTER(Active) 또는 BACKUP(Passive)
interface eth0
virtual_router_id 51
priority 100 # Master가 더 높은 값
advert_int 1
authentication {
auth_type PASS
auth_pass secret123
}
virtual_ipaddress {
192.168.1.100 # VIP
}
}
AWS의 경우: ALB/NLB는 자동으로 Multi-AZ(가용 영역 3개 이상)에 분산되어 있어서 SPOF 걱정이 없습니다. AWS가 알아서 고가용성을 보장합니다.
11. 요약 - 제가 로드 밸런서에서 배운 것들
결국 제가 로드 밸런서를 통해 이해한 핵심은 이겁니다.
- 서버를 늘리는 건 쉽지만, 트래픽을 나누는 건 어렵다. 로드 밸런서 없이는 Scale-out이 무의미합니다.
- L4 vs L7은 "성능 vs 기능"의 트레이드오프입니다. 무조건 L7이 좋은 게 아닙니다.
- Sticky Session은 피하고, Stateless Architecture를 지향하라. 세션은 Redis 같은 외부 저장소에 두는 게 정답입니다.
- Health Check와 Connection Draining이 없으면 무중단 배포는 불가능합니다.
- 로드 밸런서도 죽을 수 있다. SPOF를 제거하려면 로드 밸런서도 이중화해야 합니다.
한 줄 요약: "로드 밸런서는 단순한 트래픽 분산 도구가 아니라, 고가용성 시스템의 핵심 구성 요소다."
Load Balancer: The Traffic Cop of High Scale Systems
1. Prologue: One Server Can't Handle Everything
My interest in load balancers started with a simple question:
When traffic spikes, a single server isn't enough. So you scale out and add more servers. But how do users actually get distributed across them?
The answer is the Load Balancer.
This scenario plays out constantly. A link goes up on a popular forum, a marketing campaign fires, something goes viral. Suddenly, a single AWS EC2 instance that handled normal traffic is pegged at 100% CPU and dies.
The metaphor that clicked: "Restaurant line management."
Imagine a famous restaurant flooded with 1,000 customers. One chef can't handle it. So you hire 10 chefs (Scale-out). But all customers queue at Chef #1 while #2-#10 sit idle.
You need a host at the entrance: "Customer 1, go to Chef 1. Customer 2, go to Chef 2!"
That host is the Load Balancer. Setting up Nginx as a load balancer and watching it distribute requests across servers is when "High Availability" stops being a buzzword and starts making sense.
2. The Confusion: L4? L7? What's the Difference?
When I started learning about load balancers, the most confusing part was the difference between L4 Load Balancer and L7 Load Balancer. I learned about OSI 7 layers in school, but "operates at Layer 4" didn't mean much in practice.
At first, I thought: "L7 is a higher number, so it must be better than L4, right?" Completely wrong. It all came down to: "How deep do you inspect the packet?"
L4 Load Balancer (Transport Layer)
L4 only looks at IP Address + Port Number. It never peeks inside the packet. It simply tosses incoming packets to backend servers.
Think of it as: "You're wearing a red hat? Go that way." (Sorting by external appearance)
Pros:
- Extremely fast and efficient. No packet inspection overhead.
- No need to encrypt/decrypt TLS (HTTPS).
- Perfect for low-latency systems like banking, gaming servers.
Cons:
- No intelligent routing. Can't send
/apito Server A and/imagesto Server B. - Doesn't understand HTTP, so cookie-based session persistence is complex.
Real-world examples: AWS NLB (Network Load Balancer), LVS (Linux Virtual Server), F5 Big-IP.
L7 Load Balancer (Application Layer)
L7 inspects URL, HTTP headers, cookies, even request body. It tears open the packet and understands HTTP content.
Think of it as: "Ordering pizza? Go upstairs. Ordering coffee? Downstairs." (Sorting by content)
Pros:
- Intelligent routing. Essential for Microservices Architecture (MSA).
- SSL Termination feature handles certificates at the load balancer level.
- Content-based routing (e.g., User-Agent to separate mobile/desktop servers).
Cons:
- Packet analysis costs make it slightly slower than L4.
- Uses more CPU and memory.
Real-world examples: AWS ALB (Application Load Balancer), Nginx, HAProxy, Cloudflare Load Balancer.
I initially thought I had to use L7. "More intelligent means better, right?" But my service only provided a simple REST API without microservices. When I switched to AWS NLB (L4), response time improved by 30%. I learned that "using advanced tech for its own sake isn't always the answer."
3. The Algorithm Decision: How to Distribute Work?
When configuring a load balancer, you must choose an algorithm to decide "Who gets the work?" This choice matters more than it looks.
1) Round Robin: "Take turns fairly"
Requests go in order: A → B → C → A → B...
This is the most intuitive starting point. Works fine if all servers have identical specs. But there's a common failure case.
The Problem: If one server is t3.small (2GB RAM) while the others are t3.large (8GB RAM), Round Robin distributes evenly — so the t3.small crashes first.
Solution: Switch to Weighted Round Robin. If Server A is 4x more powerful than Server B, give A four requests for every one to B.
upstream backend {
server 10.0.1.10:8080 weight=4; # t3.large: weight 4
server 10.0.1.11:8080 weight=4; # t3.large: weight 4
server 10.0.1.12:8080 weight=1; # t3.small: weight 1
}
2) Least Connections: "Give it to whoever's least busy"
"Send traffic to the server with the fewest active connections."
This algorithm shines for services with unpredictable processing times (e.g., video encoding, image resizing). Applied to video thumbnail generation servers, response times even out noticeably.
upstream video_servers {
least_conn; # Least connections algorithm
server 10.0.2.20:8080;
server 10.0.2.21:8080;
server 10.0.2.22:8080;
}
3) IP Hash: "You always go to that server"
Routes a specific user (IP) to always hit the same server.
Purpose: Session Persistence (Sticky Session). If login info only exists in Server A's session, routing to Server B logs you out.
This works as a temporary fix before migrating sessions to Redis. But there are problems.
Issues:
- If a user's IP changes (VPN, mobile network), their session is lost.
- If specific IPs generate heavy traffic, load distribution becomes uneven.
Lesson learned: IP Hash is a band-aid. The real answer is externalizing sessions to Redis or an external DB.
upstream app_servers {
ip_hash; # IP hash algorithm
server 10.0.3.30:8080;
server 10.0.3.31:8080;
}
4) Consistent Hashing: "Server death? No problem"
Regular hashing breaks when server count changes (e.g., 3 → 4 servers) because all hash values change, killing all sessions. But Consistent Hashing minimizes affected users when a server dies.
Applied to cache servers (Memcached), the benefit is clear: when one server fails, the entire cache doesn't reset.
4. Real Implementation: Building an L7 Load Balancer with Nginx
A few lines in Nginx config (/etc/nginx/nginx.conf) turns any machine into a load balancer. The typical setup: one EC2 instance running Nginx in front of three backend servers.
http {
# 1. Define upstream (backend server group)
upstream my_backend_servers {
least_conn; # Algorithm: least connections
server 10.0.0.1:8080 weight=2; # Weight 2
server 10.0.0.2:8080;
server 10.0.0.3:8080 down; # Maintenance mode (no traffic)
# Health check (Nginx Plus only, open-source has passive only)
# check interval=3000 rise=2 fall=5 timeout=1000;
}
server {
listen 80;
server_name myapp.com;
# 2. Forward all requests to upstream group (Reverse Proxy)
location / {
proxy_pass http://my_backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Timeout settings
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
# 3. Path-based routing (L7's strength)
location /api/ {
proxy_pass http://api_servers;
}
location /images/ {
proxy_pass http://image_servers;
proxy_cache my_cache; # Caching possible too
}
}
}
Health Check: "Are you alive?"
If one server dies (Health Check fails), Nginx automatically removes it from the pool and routes traffic only to surviving servers. This is Failover.
Nginx open-source supports only passive health checks. It removes a server after actual requests fail. Nginx Plus (paid) and AWS ALB support active health checks, periodically pinging a /health endpoint and preemptively removing unresponsive servers.
Starting with Nginx open-source and migrating to AWS ALB as needs grow is a common path. The reasons are covered in the next section.
5. AWS Choice: NLB vs ALB
Here's what matters when choosing between AWS load balancer options.
AWS NLB (Network Load Balancer) - L4
Features:
- Handles millions of requests per second.
- Provides static IP (Elastic IP assignment).
- Ultra-low latency (
<100 microseconds).
When to use:
- TCP/UDP protocol services (e.g., game servers, IoT)
- Systems needing ultra-low latency (finance)
- When TLS Termination isn't needed
Use case: NLB in front of a WebSocket chat server is a common pattern. ALB supports WebSocket, but costs rise significantly with high connection counts. Switching to NLB often produces substantial monthly cost savings.
# Create NLB with AWS CLI
aws elbv2 create-load-balancer \
--name my-network-lb \
--type network \
--subnets subnet-abc123 subnet-def456 \
--scheme internet-facing
AWS ALB (Application Load Balancer) - L7
Features:
- HTTP/HTTPS protocols only.
- Path-based routing:
/api/*→ Backend,/admin/*→ Admin server. - Host-based routing:
api.myapp.com→ API,www.myapp.com→ Web. - Can route directly to Lambda functions (Serverless).
- WAF (Web Application Firewall) integration.
When to use:
- Microservices Architecture (MSA)
- Container-based services (ECS, EKS)
- Needing HTTP/2, WebSocket over HTTP
Use case: In an MSA setup, ALB's real power becomes clear. /auth/* routes to the auth server, /payment/* to the payment server, /media/* to the media server — all automatically, without code-level routing logic.
# Create ALB with AWS CLI
aws elbv2 create-load-balancer \
--name my-application-lb \
--type application \
--subnets subnet-abc123 subnet-def456 \
--security-groups sg-123456
# Create Target Group (with Health Check)
aws elbv2 create-target-group \
--name my-targets \
--protocol HTTP \
--port 80 \
--vpc-id vpc-123456 \
--health-check-protocol HTTP \
--health-check-path /health \
--health-check-interval-seconds 30 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3
Health Check Setup: A Common Pitfall
A careless Health Check configuration leads to False Positive issues. Servers are alive but Health Checks declare them dead, refusing to send traffic.
Root cause: Setting the Health Check path to / is common but risky. If homepage rendering is slow, it can exceed the timeout (often 5 seconds), causing false failures.
Solution: Created a dedicated /health endpoint that simply returns 200 OK.
// Express.js example
app.get('/health', (req, res) => {
// Optional DB connection check
if (db.isConnected()) {
res.status(200).send('OK');
} else {
res.status(503).send('Service Unavailable');
}
});
Key insight: Health Checks determine "Can the server accept requests?" not "Is the service perfectly operational?" Overly complex logic (running 10 DB queries) backfires.
6. The Sticky Session Trap
A common stumbling block: "Why do user sessions keep expiring?"
Situation: Logged-in users get logged out after page refreshes. Logs show requests hitting different servers each time.
Root cause: Session data is stored in local server memory (Express Session's default MemoryStore), and Round Robin sends requests to different servers.
Attempt 1 - Enable Sticky Session: Enable Stickiness in ALB Target Group Attributes.
aws elbv2 modify-target-group-attributes \
--target-group-arn arn:aws:elasticloadbalancing:... \
--attributes Key=stickiness.enabled,Value=true \
Key=stickiness.type,Value=lb_cookie \
Key=stickiness.lb_cookie.duration_seconds,Value=86400
Result: Mostly fixed, but browsers blocking cookies and mobile apps still have issues.
Attempt 2 - Migrate to Redis Session Store: Store sessions in external Redis. Now any server can retrieve sessions from Redis, completely solving the problem.
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: 'redis://my-redis-cluster:6379' });
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'my-secret',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 86400000 } // 24 hours
}));
The real lesson: Sticky Session is a band-aid. Stateless Architecture (externalizing sessions) is the answer.
7. Global Scale: GSLB and DNS-based Load Balancing
When a service expands internationally, geographic latency becomes a real problem. Korean servers mean slow responses for US users. This is where GSLB (Global Server Load Balancing) comes in.
DNS-based Load Balancing Principle
When users type myapp.com, DNS checks their geographic location and returns the nearest server IP.
Example:
- Korean users →
13.124.x.x(Seoul region) - US users →
54.183.x.x(California region)
AWS Route 53 Geolocation Routing
Configuring Geolocation Routing Policy in AWS Route 53 looks like this.
# Create Record Set in Route 53 (Seoul region)
{
"Name": "myapp.com",
"Type": "A",
"SetIdentifier": "Seoul-Region",
"GeoLocation": {
"ContinentCode": "AS"
},
"AliasTarget": {
"HostedZoneId": "Z1234567890ABC",
"DNSName": "my-seoul-alb-123456.ap-northeast-2.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
# Create Record Set in Route 53 (Virginia region)
{
"Name": "myapp.com",
"Type": "A",
"SetIdentifier": "US-Region",
"GeoLocation": {
"CountryCode": "US"
},
"AliasTarget": {
"HostedZoneId": "Z0987654321XYZ",
"DNSName": "my-us-alb-654321.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
Impact: US users' average response time can improve from 800ms → 200ms with this setup.
Limitations of DNS-based Load Balancing
But there's a limitation. Even when Seoul servers go down, DNS still returns that IP. DNS doesn't perform Health Checks by default.
Solution: Enable Route 53's Health Check feature and add Failover configuration to automatically return the Virginia server IP when Seoul servers fail.
Key takeaway: DNS load balancing excels at geographic distribution but falls short on real-time load distribution. Combine with L4/L7 load balancers for completeness.
8. Connection Draining: Deployment Request Cutoffs
Every new version deployment can cause user requests to get cut off mid-stream if this isn't handled.
Situation: The moment a server shuts down, the load balancer is still sending requests to it. Requests in flight when the server dies cause 502 Bad Gateway errors.
Solution: Connection Draining (or Deregistration Delay)
Set ALB Target Group's Deregistration Delay to 300 seconds (5 minutes). When shutting down a server, the load balancer:
- Stops sending new requests to this server.
- Waits up to 300 seconds for existing requests to complete.
- Only then fully removes the server.
aws elbv2 modify-target-group-attributes \
--target-group-arn arn:aws:elasticloadbalancing:... \
--attributes Key=deregistration_delay.timeout_seconds,Value=300
Code-level Graceful Shutdown:
Modified Node.js server to not immediately terminate on SIGTERM signal, but wait for in-flight requests to complete.
const server = app.listen(8080);
process.on('SIGTERM', () => {
console.log('SIGTERM received. Closing server gracefully...');
server.close(() => {
console.log('All connections closed. Exiting process.');
process.exit(0);
});
// Force shutdown after 30s if still not done
setTimeout(() => {
console.error('Forced shutdown after 30s timeout.');
process.exit(1);
}, 30000);
});
Result: 502 errors during deployment completely disappear. This is what "Zero-Downtime Deployment" actually means in practice.
9. Blue-Green Deployment with Load Balancers
Beyond Connection Draining, Blue-Green Deployment takes deployment safety further.
Concept:
- Blue: Currently running server group (e.g., v1.0)
- Green: New deployment server group (e.g., v1.1)
Deployment process:
- Deploy new version to Green environment and test.
- Switch load balancer traffic from Blue → Green.
- If issues arise, immediately rollback to Blue.
Implementation with AWS ALB:
Create two Target Groups, adjust ALB Listener Rule weights.
# 100% traffic to Blue Target Group
aws elbv2 modify-listener \
--listener-arn arn:aws:elasticloadbalancing:... \
--default-actions Type=forward,TargetGroupArn=arn:blue-tg,Weight=100
# Gradually shift traffic to Green (Canary deployment)
# Blue 90%, Green 10%
aws elbv2 modify-rule \
--rule-arn arn:aws:elasticloadbalancing:... \
--actions Type=forward,ForwardConfig='{
"TargetGroups": [
{"TargetGroupArn": "arn:blue-tg", "Weight": 90},
{"TargetGroupArn": "arn:green-tg", "Weight": 10}
]
}'
# If no issues, switch to 100% Green
aws elbv2 modify-listener \
--listener-arn arn:aws:elasticloadbalancing:... \
--default-actions Type=forward,TargetGroupArn=arn:green-tg,Weight=100
Impact: Deployment risk drops dramatically. Knowing "I can always rollback" leads to more frequent, lower-stress deployments.
10. SPOF and High Availability: Load Balancers Die Too
If the load balancer is the entry point for all traffic, what happens when the load balancer itself dies?
The entire service goes down. This is called SPOF (Single Point of Failure).
Solution: Run two load balancers (Active-Passive).
Keepalived and VRRP Protocol
In on-premise environments, use Keepalived to configure two load balancers sharing a VIP (Virtual IP).
- Active: Holds the VIP and handles traffic normally.
- Passive: Monitors from the side. If Active stops sending Heartbeat signals, immediately takes over the VIP.
# Keepalived config (/etc/keepalived/keepalived.conf)
vrrp_instance VI_1 {
state MASTER # MASTER(Active) or BACKUP(Passive)
interface eth0
virtual_router_id 51
priority 100 # Master has higher value
advert_int 1
authentication {
auth_type PASS
auth_pass secret123
}
virtual_ipaddress {
192.168.1.100 # VIP
}
}
AWS case: ALB/NLB automatically distributes across Multi-AZ (3+ availability zones), eliminating SPOF concerns. AWS guarantees high availability automatically.