대규모 트래픽에서 체감 성능은 애플리케이션보다 역프록시 레이어가 먼저 한계에 닿습니다. 워커 병렬도, 버퍼 크기, 큐 운영 정책이 조금만 어긋나도 p95 지연과 에러율이 튑니다. 아래 글은 같은 소제목을 유지하며, 운영에서 바로 적용할 수 있도록 한 단계 더 자세히 풀어쓴 확장판입니다. 굵은 글씨나 장식은 빼고 필요한 내용만 담았습니다.
1) 워커: CPU에 맞춘 병렬도와 수락 정책
워커 수는 물리 코어 수를 기준으로 시작하고, 코어가 많을수록 워커 하나에 과도한 연결을 싣지 않도록 worker_connections, maxconn을 함께 보정합니다. 재시도 폭주나 스파이크 상황에서는 한 워커가 너무 많은 accept를 수행하면 컨텍스트 스위치가 늘어납니다. NGINX의 multi_accept는 과도한 일괄 수락을 만들 수 있으므로 기본적으로 끄고, HAProxy의 tune.maxaccept는 소규모 배치로 두어 급격한 수락 폭주를 방지합니다. Envoy는 워커 수가 기본적으로 코어에 맞춰지므로 오케스트레이션 레벨에서 CPU 핀과 IRQ 분배를 맞추면 캐시 적중률이 좋아집니다. 컨테이너라면 리소스 제한과 워커 수가 어긋나지 않게 요청합니다.
2) 버퍼: 과소와 과대의 비용을 모두 피하기
버퍼를 작게 잡으면 업스트림 응답이 조금만 커져도 디스크 스풀로 흘러가고, 너무 크게 잡으면 연결 수가 많을 때 RAM이 급증합니다. NGINX의 proxy_buffer_size, proxy_buffers, proxy_busy_buffers_size는 서비스의 평균 응답 크기와 헤더 크기를 측정한 후 25% 여유를 둔 값에서 시작합니다. 이미지 업로드나 대용량 다운로드가 섞이는 서비스는 temp file을 금지하기보다 SSD IOPS와 함께 관리하는 편이 안전합니다. Envoy의 per_connection_buffer_limit_bytes를 과소로 두면 스트림이 자주 블로킹되므로 32k에서 64k 사이로 시작해 워커당 연결 수와 곱했을 때 총 메모리가 장비 RAM의 안전 한도를 넘지 않도록 계산합니다. HAProxy의 tune.bufsize는 16k에서 64k 사이를 권장하고, 클라이언트와 서버 소켓 버퍼는 지연과 대역폭의 곱을 고려해 상향하되 모니터링을 동반합니다.
3) 큐와 백프레셔: 기다리게 하지 말고 일찍 실패시키기
큐는 무한 대기 장소가 아니라 순간적인 피크를 흡수하는 완충지입니다. timeout queue를 짧게 두고 초과분은 즉시 실패로 돌려 상류의 재시도를 조기에 멈추는 편이 전체 안정성에 유리합니다. NGINX의 proxy_next_upstream은 오류와 타임아웃 정도만 대상으로 제한하고 횟수와 총 시간을 작게 잡습니다. Envoy는 circuit breaker의 max_pending_requests와 max_requests로 서비스별 대기 한도를 명확히 하되, overload manager로 메모리나 CPU 임계에 도달하면 keepalive 해제를 통해 유휴 연결을 신속히 회수합니다. HAProxy는 leastconn으로 공정성을 높이고 서버별 maxconn을 명시해 특정 노드 쏠림을 막습니다. 큐를 길게 가져가면 p99는 악화되고 사용자는 느림으로 체감하니 지연보다 가용성을 우선하는 일부 배치 작업이 아니라면 짧은 큐가 맞습니다.
4) Keep-Alive와 HTTP2·HTTP3: 연결 재사용이 곧 성능
새 연결을 자주 만드는 서비스는 Time-Wait, 포트 고갈, 핸드셰이크 CPU 낭비가 겹칩니다. keepalive_timeout과 keepalive_requests를 합리적으로 두고, 업스트림 풀에 keepalive를 반드시 설정합니다. HTTP2는 같은 연결에서 여러 스트림을 동시에 처리하므로 new conn/s를 큰 폭으로 낮춥니다. 다만 stream 동시성 수를 과도하게 키우면 한 백엔드에 부하가 몰릴 수 있어 128 내외부터 시작해 조정합니다. HTTP3는 손실 환경에서 지연 꼬리를 줄여주지만, 경계 장비와의 호환성이 전제되어야 하므로 점진 도입과 관측이 필수입니다.
5) 타임아웃 정렬: 끊을 땐 위에서 먼저
클라이언트, 역프록시, 백엔드의 타임아웃이 엇갈리면 중간에서 예고 없이 끊기고 상류는 모른 채 재시도를 반복합니다. 원칙은 클라이언트가 가장 짧고, 역프록시가 그보다 길고, 백엔드가 가장 깁니다. connect, send, read, request 전체 시간, stream idle, keepalive 시간 등 같은 성격의 타임아웃을 계층별로 짝지어 표로 관리하면 장애 분석이 쉬워집니다. 숫자는 작게 시작해 p95 기준으로 올리는 것이지, 아래 계층을 무조건 크게만 두는 것은 아닙니다. 실제 트래픽의 지연 분포와 재시도 패턴을 함께 봐야 합니다.
6) 제품별 추천 시작값
NGINX는 worker_processes를 auto, worker_connections 8192 정도로 시작하고 FD 한도를 넉넉히 둡니다. proxy_buffers 16 16k와 proxy_busy_buffers_size 256k, keepalive 256과 keepalive_requests 1000, connect 2초, read와 send 15초 조합이면 대부분의 웹 API에서 안전한 출발점입니다. Envoy는 워커 수를 코어에 맞추고 per_connection_buffer_limit_bytes 32k에서 64k, circuit breaker는 연결 2만, 대기 1만, 요청 2만으로 시작합니다. request timeout과 stream idle timeout을 15초 수준으로 맞추고 overload manager를 반드시 켭니다. HAProxy는 nbthread를 코어수로, maxconn은 RAM과 FD 한도에 맞추어 20만 수준에서 시작하되 백엔드 maxconn을 명시합니다. tune.bufsize 32k, connect 2초, queue 2초, client와 server 30초가 무난합니다.
7) 관측 대시보드: 빠름을 수치로 증명
지연과 성공률은 p50, p95, p99, TTFB로 나누어 보고 4xx, 5xx, 타임아웃을 분리합니다. 큐 지표는 엔진엑스의 writing, waiting, 엔보이의 pending, 하프로의 timeout queue 히트를 수집합니다. 연결 지표는 active와 idle, new conn/s, keepalive 재사용률을 함께 봅니다. 버퍼와 메모리는 per connection buffer hit과 temp file 사용률을 체크하고, 오버로드 이벤트와 백엔드 maxconn 히트 여부를 경보로 올립니다. 이 모든 지표를 배포, 캠페인, 스케일 이벤트 타임라인과 겹치면 원인과 결과가 선명해집니다.
8) 단계별 적용 체크리스트
베이스라인을 동일 요일과 시간대 기준으로 24시간 이상 수집합니다. 워커와 FD 한도를 먼저 넓히고 new conn/s, 메모리 변화를 확인합니다. 버퍼는 두 배씩 올리지 말고 25퍼센트 단위로 점진 조정합니다. 큐 한도와 타임아웃을 정하고 초과는 즉시 실패로 되돌립니다. keepalive와 HTTP2를 활성화하되 스트림 동시성은 보수적으로 시작합니다. 타임아웃은 계층별로 정렬하고, 변경은 한 번에 하나만 적용해 전후 p95와 에러율을 비교합니다.
9) 자주 묻는 질문
값을 그대로 복사해도 되느냐는 질문이 많지만 트래픽 패턴과 페이로드 크기, 백엔드 성능에 따라 최적값은 달라집니다. 버퍼를 크게 잡으면 빨라지느냐고 묻는다면 연결 수가 많은 환경에서는 메모리가 먼저 고갈됩니다. 큐를 길게 두면 안정적이냐는 질문에는 짧은 큐와 빠른 실패가 대부분의 사용자 체감과 전체 성공률을 더 좋게 만든다고 답할 수 있습니다. 과한 리트라이는 하류를 공격하는 것과 같으니 상한과 시간 제한을 반드시 걸어야 합니다.
마무리
워커, 버퍼, 큐는 따로 조정하는 것이 아니라 함께 설계해야 합니다. 워커로 동시성 바닥을 올리고, 버퍼로 스풀과 메모리 균형을 맞추며, 큐 정책으로 폭주 전파를 초기에 차단하면 같은 인프라에서도 p95 지연은 내려가고 성공률은 올라갑니다. 오늘은 워커와 FD, 타임아웃 정렬부터 점검하고, 내일은 버퍼와 큐, 연결 재사용률을 단계적으로 조정해 보세요. 역프록시 튜닝만으로도 사용자는 충분히 빠르다고 느끼게 됩니다.