왜 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"의 상당 부분은 원인이 명확해진다.