서버를 두 대 이상으로 늘리고 나면 그 앞에 뭘 세울지가 다음 고민이 된다. 클라우드 관리형 로드밸런서(ALB, NLB)를 쓰면 편하지만, 온프레미스나 단일 VM 위에서 프로세스를 여러 개 띄워 분산하는 경우, 세밀한 헬스체크가 필요한 경우엔 HAProxy를 직접 세우는 쪽이 손에 잡힌다. 예전에 Nginx로 upstream 분산만 걸어 쓰던 구성을 HAProxy로 옮기면서 정리한 내용을 여기 남긴다.
결론부터 말하자면, HAProxy L7 로드밸런싱의 뼈대는 frontend(수신)와 backend(분배 대상 서버 묶음) 두 블록이다. 트래픽을 어떻게 나눌지는 balance로 정하고(웹은 roundrobin, 요청 비용 편차가 큰 API는 leastconn), 죽은 노드를 빼는 건 option httpchk 헬스체크가 맡는다. 여기서 헬스체크는 기본적으로 GET이 아니라 OPTIONS 요청을 /로 보낸다는 점을 알고 시작해야 헬스 엔드포인트와 방화벽, 로그 필터를 잘못 잡지 않는다.
1. 용어 정리: L7 로드밸런싱과 HAProxy의 구조
L7(레이어 7) 로드밸런싱은 HTTP 요청 내용, 즉 URL 경로, 헤더, 쿠키까지 들여다보고 분배 대상을 정하는 방식이다. TCP 연결만 보고 나누는 L4와 달리 경로별 라우팅이나 쿠키 기반 세션 고정을 걸 수 있다.
HAProxy(High Availability Proxy)는 TCP/HTTP 로드밸런서 겸 프록시다. 설정은 크게 네 블록으로 나뉜다.
global- 프로세스 전역 설정(로그, 최대 연결 수, 실행 사용자).defaults- 아래 블록들이 공유할 기본값(모드, 타임아웃).frontend- 클라이언트 요청을 받는 입구. 어떤 포트를 열고 어느 backend로 넘길지 정한다.backend- 실제 요청을 처리할 서버 목록과 분배 규칙.
발행 시점(2026-09) 기준 최신 LTS는 3.4 LTS(2026-06 릴리스, 최신 3.4.4)이고, 그 이전 LTS인 3.2 LTS(2025-05 릴리스, 최신 3.2.23)도 여전히 유지보수 대상이다. 새로 까는 서버라면 3.4 또는 3.2 계열을 권한다. 세부 패치 번호는 배포판 패키지 상태에 따라 다를 수 있다.
# RHEL 9 계열 sudo dnf install -y haproxy # Ubuntu 24.04 sudo apt install -y haproxy haproxy -v # HAProxy version 3.2.x ... - https://haproxy.org/
배포판 기본 저장소 버전이 낮으면 벤더 제공 패키지 저장소를 붙여 최신 LTS를 받는다. 설정 파일은 /etc/haproxy/haproxy.cfg 한 곳이다.
2. frontend와 backend 최소 구성
웹 서버 두 대를 뒤에 두는 가장 단순한 형태다. mode http가 L7 처리를 켜는 스위치다.
global
log /dev/log local0
maxconn 20000
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
timeout connect 5s
timeout client 30s
timeout server 30s
frontend web_front
bind *:80
default_backend web_back
backend web_back
balance roundrobin
server web1 10.0.1.11:8080 check
server web2 10.0.1.12:8080 check
server 줄 끝의 check가 헬스체크를 켠다. 이게 없으면 HAProxy는 노드가 죽어도 계속 트래픽을 보낸다. 여기까지가 L4 수준 TCP 연결 확인이다. HTTP 응답까지 확인하려면 다음 절의 option httpchk가 필요하다.
설정을 고쳤으면 반영 전에 문법부터 검사한다. 이 습관 하나로 오타 때문에 서비스가 안 뜨는 사고를 막는다.
haproxy -c -f /etc/haproxy/haproxy.cfg # Configuration file is valid sudo systemctl reload haproxy
reload는 기존 연결을 유지한 채 설정만 갈아끼운다. restart는 연결이 끊기니 운영 중엔 reload를 쓴다.
3. balance 알고리즘, 무엇을 언제 쓰나
balance는 backend 안에서 요청을 어느 server로 보낼지 정하는 규칙이다. 자주 쓰는 것만 추린다.
roundrobin
순서대로 돌아가며 배정한다. 요청 하나하나의 처리 비용이 비슷한 정적/일반 웹 트래픽의 기본값이다. server 가중치(weight)도 반영한다.
leastconn
현재 활성 연결이 가장 적은 server로 보낸다. 요청마다 처리 시간이 들쭉날쭉한 API, DB를 물고 오래 붙어 있는 연결에 맞다. 한쪽 노드에 무거운 요청이 몰려도 나머지가 받아준다.
source
클라이언트 IP를 해시해 같은 IP는 늘 같은 server로 보낸다. 쿠키 없이 IP 기준으로 세션을 고정하고 싶을 때 쓴다. 다만 NAT 뒤에 있는 다수 사용자가 한 IP로 묶이면 분배가 고르지 않다.
uri
요청 URI를 해시한다. 같은 경로 요청이 같은 server로 가므로 캐시 적중률을 높이려는 캐싱 backend에 유용하다.
권장은 이렇다. 상태 없는 웹 요청은 roundrobin, 요청 비용 편차가 큰 API 서버는 leastconn을 기본으로 잡는다. 세션 고정이 꼭 필요하면 IP 기반 source보다 다음 절의 쿠키 방식이 더 정밀하다.
4. option httpchk 헬스체크 - 기본 요청은 OPTIONS다
헬스체크는 backend server가 살아 있는지 주기적으로 확인해, 응답이 없거나 비정상이면 분배 대상에서 자동으로 빼는 기능이다. option httpchk를 걸면 TCP 연결만 보던 검사가 HTTP 응답까지 확인하는 L7 검사로 올라간다.
여기서 반드시 짚어야 할 기본 동작이 있다. option httpchk를 인자 없이 켜면 HAProxy는 GET이 아니라 OPTIONS 요청을 / 경로로 보낸다. 응답 상태 코드가 2xx 또는 3xx면 정상으로 판정한다. 공식 문서도 "Checks send an OPTIONS request to the URL / by default"라고 못박고 있다.
이 기본값을 모르면 함정에 걸린다. 애플리케이션이 /에 대한 OPTIONS를 처리하지 않아 405를 내거나, 프론트에 둔 WAF/프록시가 OPTIONS를 막거나, 접근 로그 필터를 GET 기준으로 잡아두면 멀쩡한 노드가 DOWN으로 빠진다. 그래서 실무에선 메서드와 경로를 명시하는 쪽을 권한다.
backend api_back
balance leastconn
option httpchk GET /healthz
http-check expect status 200
server api1 10.0.1.21:8080 check inter 3s fall 3 rise 2
server api2 10.0.1.22:8080 check inter 3s fall 3 rise 2
option httpchk GET /healthz로 메서드와 경로를 바꾸고, http-check expect status 200으로 성공 조건을 200 하나로 좁혔다. Spring Boot라면 /actuator/health를 경로로 쓰면 그대로 맞아떨어진다. server 줄의 옵션은 이렇게 읽는다.
inter 3s- 3초 간격으로 검사.fall 3- 연속 3번 실패하면 DOWN 처리.rise 2- 연속 2번 성공하면 다시 UP.
헬스 엔드포인트는 가볍게 두는 게 좋다. DB 커넥션 검사까지 무겁게 물려두면 DB가 잠깐 느려질 때 전 노드가 동시에 DOWN으로 빠져 로드밸런서가 보낼 곳을 잃는다.
5. sticky 세션 - cookie insert로 노드 고정
서버 로컬에 세션을 들고 있는 애플리케이션(전형적인 톰캣 HttpSession)은 매 요청이 같은 노드로 가야 로그인 상태가 유지된다. HAProxy는 cookie 지시자로 backend server마다 식별 쿠키를 심어 이 고정을 처리한다.
backend web_back
balance roundrobin
cookie SRVID insert indirect nocache
server web1 10.0.1.11:8080 check cookie web1
server web2 10.0.1.12:8080 check cookie web2
동작을 뜯어보면 이렇다.
cookie SRVID insert- HAProxy가SRVID라는 쿠키를 응답에 넣는다.indirect- 이 쿠키는 backend server로 다시 전달하지 않는다(애플리케이션은 존재를 모른다).nocache- 중간 캐시가 이 쿠키 붙은 응답을 캐싱하지 못하게 막는다.- server 줄의
cookie web1- 각 노드에 부여할 쿠키 값. 클라이언트가SRVID=web1을 들고 오면 web1로만 보낸다.
다만 sticky 세션은 확장의 발목을 잡는다. 특정 노드에 세션이 몰리면 분배가 기울고, 그 노드가 죽으면 거기 붙어 있던 사용자 세션이 통째로 날아간다. 그래서 가능하면 세션을 Redis 같은 외부 저장소로 빼고(Spring Session 등) HAProxy는 roundrobin/leastconn으로 고정 없이 굴리는 구성을 권한다. 세션 외부화가 당장 어려운 레거시에서만 cookie insert를 임시 방편으로 쓰는 게 맞다.
6. stats 대시보드로 트래픽과 장애 노드 보기
HAProxy는 내장 상태 페이지를 제공한다. 어느 노드가 UP/DOWN인지, 각 backend로 세션이 얼마나 가는지, 헬스체크 실패 횟수가 얼마인지 브라우저에서 바로 확인할 수 있다. listen 블록으로 별도 포트에 올린다.
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:CHANGE_ME
reload 후 http://서버IP:8404/stats로 접속하면 backend별 표가 뜬다. 인증(stats auth) 없이 열지 말고, 8404 포트는 방화벽/보안그룹에서 관리자 IP로만 제한한다. 공개 노출은 서버 내부 구조를 그대로 드러내는 것과 같다.
표에서 눈여겨볼 열은 이렇다.
Status- UP(초록)/DOWN(빨강). DOWN이면 옆에 마지막 헬스체크 실패 사유(예:L7STS/405)가 뜬다. 405가 보이면 4절에서 말한 OPTIONS 문제일 확률이 높다.Sessions- 노드별 현재/누적 세션 수. balance가 고르게 먹는지 여기서 확인한다.Bytes- 인/아웃 전송량.Check- 헬스체크 결과 코드.L7OK/200이면 정상.
운영 중 특정 노드만 잠깐 빼고 싶을 때는 stats 페이지에서 직접 조작하는 대신 소켓 명령이나 설정으로 처리하는 편이 이력 관리에 낫다. 스크립트로 상태만 뽑아보려면 admin 소켓의 show stat을 쓴다.
echo "show stat" | sudo socat stdio /run/haproxy/admin.sock | cut -d, -f1,2,18 # pxname,svname,status # web_back,web1,UP # web_back,web2,DOWN
이렇게 하면 모니터링 도구나 알림 스크립트에서 노드 상태를 파싱해 쓸 수 있다.
7. 정리
HAProxy L7 구성은 frontend로 받고 backend로 나누는 두 블록이 핵심이다. 분배는 웹이면 roundrobin, 비용 편차가 큰 API면 leastconn을 기본으로 잡는다. 헬스체크는 option httpchk의 기본이 OPTIONS /라는 점을 전제로, 메서드와 경로를 명시하고 http-check expect status로 성공 조건을 좁혀두면 오탐이 준다. 세션 고정은 cookie insert로 가능하지만 확장을 막으니 외부 세션 저장소를 먼저 검토한다. 마지막으로 stats 페이지는 인증과 포트 제한을 걸어 올려두면 장애 노드 판별이 빨라진다.