왜 maxconn이 병목이 되는가
HAProxy는 프론트엔드로 들어온 요청을 백엔드 서버로 분배한다. 이때 두 계층 모두 동시 커넥션 상한(maxconn)을 가진다. 트래픽이 상한을 넘으면 HAProxy는 요청을 즉시 거부하지 않고 큐에 쌓는다. 문제는 이 큐잉이 눈에 잘 보이지 않는다는 점이다. 백엔드 스레드 풀이 이미 포화 상태인데 프론트엔드 maxconn만 크게 잡아두면, 커넥션은 계속 수락되지만 백엔드 큐에서 대기하다 타임아웃으로 죽는다. 사용자 입장에서는 "간헐적 5xx"로만 보이고, 실제 원인인 큐 대기는 로그에 드러나지 않아 원인 파악이 늦어진다.
세 계층의 maxconn 구분
maxconn은 위치에 따라 의미가 다르다. 이 구분을 헷갈리면 튜닝이 어긋난다.
| 위치 | 의미 | 초과 시 동작 |
|---|---|---|
| global maxconn | 프로세스 전체 동시 커넥션 | OS 소켓 수락 중단 |
| frontend maxconn | 해당 프론트엔드 상한 | 수락 지연(백프레셔) |
| server maxconn | 서버당 동시 커넥션 | 백엔드 큐에 대기 |
핵심은 server maxconn이다. 이 값이 실제 애플리케이션이 소화 가능한 동시 처리량과 맞아야 한다. 백엔드 앱이 워커 100개라면 server maxconn을 200으로 잡는 순간 100개는 항상 앱 내부 큐에서 대기한다.
기본 설정 예시
global
maxconn 20000 # 프로세스 전체 상한
nbthread 4
defaults
mode http
timeout connect 3s
timeout client 30s
timeout server 30s
timeout queue 5s # 큐 대기 상한 — 이 시간 넘으면 503
frontend web
bind :80
maxconn 18000 # global보다 낮게
default_backend app
backend app
balance leastconn
# server당 동시 100, 큐 최대 대기는 timeout queue 적용
server app1 10.0.0.11:8080 check maxconn 100
server app2 10.0.0.12:8080 check maxconn 100
timeout queue가 없으면 큐 대기가 무한정 길어져 timeout server까지 끌려간다. 큐는 짧게 끊고 빠르게 503을 돌려주는 편이 사용자 체감과 재시도 정책 모두에 유리하다.
적정 server maxconn 산정
Little's Law로 추정한다. 필요한 동시성 = 목표 처리량(RPS) × 평균 응답시간(초). 예를 들어 서버당 2000 RPS를 평균 40ms로 처리한다면 동시성은 2000 × 0.04 = 80이다. 여기에 여유 20%를 더해 maxconn 96 정도가 출발점이다. 앱의 실제 워커/커넥션풀 크기(예: gunicorn workers, DB pool)를 넘지 않게 맞춘다. HAProxy에서만 크게 열면 병목이 앱 안쪽으로 밀려나 관측이 더 어려워질 뿐이다.
큐 상태 관측
튜닝의 전제는 측정이다. Runtime API로 현재 큐 깊이와 세션을 실시간 확인한다.
#!/usr/bin/env bash
# 소켓으로 백엔드별 큐/세션 상태 조회
socat stdio /var/run/haproxy.sock <<< "show stat" \
| awk -F, 'NR==1 || $2 ~ /app[0-9]/ {print $1,$2,"qcur="$3,"scur="$5,"smax="$6}'
# 실시간 큐가 쌓이는지 감시
watch -n1 'echo "show stat" | socat stdio /var/run/haproxy.sock \
| awk -F, "\$2~/app/{print \$2, \$3}"'
여기서 qcur(현재 큐)가 지속적으로 0보다 크면 server maxconn 또는 백엔드 용량이 부족하다는 신호다. scur이 smax=maxconn에 붙어 있는데 qcur이 커진다면 정확히 그 서버가 포화 상태다.
흔한 실수와 주의점
- frontend를 global보다 크게 잡으면 무의미하다. 항상 global ≥ 모든 frontend 합.
- timeout queue 누락: 큐가 timeout server까지 버티며 스레드를 오래 점유한다.
- maxconn만 늘려 503 회피: 실제로는 지연이 응답시간에 숨을 뿐, 근본 처리량은 그대로다. 앱 용량을 먼저 늘려야 한다.
- ulimit-n 미조정: global maxconn을 올렸는데 파일 디스크립터 한계에 걸리면 조용히 커넥션이 실패한다.
maxconn × 2 + 여유로 잡는다.
정리
maxconn 튜닝의 목적은 커넥션 수를 최대로 늘리는 것이 아니라, 앱이 소화할 수 있는 만큼만 받고 나머지는 짧은 큐로 흡수하거나 빠르게 거절하는 것이다. server maxconn을 앱 실제 동시성에 맞추고, timeout queue로 대기를 제한하며, qcur을 상시 관측하는 세 가지만 지켜도 "간헐적 5xx"의 상당 부분은 원인이 명확해진다.