nginx 1.30 업그레이드 후 POST에서만 502가 났던 이유
nginx 1.29.7부터 기본 활성화된 upstream keepalive와 백엔드 idle timeout이 어긋나며 생긴 502를 추적하고 keepalive 0으로 되돌린 기록
지난주에 nginx를 1.29.0에서 1.30.2로 올렸습니다. 보안 패치 목적의 정기 업그레이드였고 설정 파일은 한 줄도 건드리지 않았습니다. 그런데 배포하고 얼마 지나지 않아 502가 드문드문 찍히기 시작했습니다. 재현 조건도 없이, 하필 POST 요청에서만요.
error.log부터
로그를 열어보니 에러가 두 종류 섞여 있었습니다.
2026/07/21 14:03:12 [error] 1123#0: *8421 upstream prematurely closed connection
while reading response header from upstream,
request: "POST /api/orders HTTP/1.1", upstream: "http://127.0.0.1:8080"
2026/07/21 14:05:47 [error] 1123#0: *8460 upstream timed out
(110: Connection timed out) while reading response header from upstream
“upstream timed out” 쪽 504는 proxy_read_timeout 초과라는 별개 축의 문제라 일단 제쳐두고, 502를 만드는 “prematurely closed”에 집중했습니다. 백엔드가 커넥션을 먼저 닫았다는 뜻인데, 정작 백엔드 로그에는 에러가 없었습니다. 요청이 도착한 흔적조차 없는 경우도 있었고요.
keepalive_timeout 65를 만졌다가
keepalive라는 단어가 어른거려서 http 블록의 keepalive_timeout 65부터 건드려봤습니다. 늘려도 줄여도 502 빈도는 그대로였습니다. 여기서 반나절을 헤맸는데, 허무하게도 이 지시어는 클라이언트와 nginx 사이에만 적용되는 값이었습니다. 문제의 구간인 nginx와 백엔드 사이와는 무관합니다.
1.29.7 체인지로그
결국 changelog를 정독하다가 원인을 찾았습니다. 1.29.7부터 upstream keepalive가 기본으로 켜집니다.
| ~1.29.6 | 1.29.7~ | |
|---|---|---|
| upstream 커넥션 | 매 요청 후 close | worker당 32개 캐시 |
| proxy_http_version | 1.0 | 1.1 |
| Connection 헤더 | close | 미전송 |
| 유휴 커넥션 유지 | 없음 | 60초 |
1.29.0에서는 매 요청마다 커넥션을 새로 맺고 끊었는데, 1.30.2는 응답을 받은 커넥션을 60초간 캐시해뒀다가 재사용합니다. 문제는 백엔드의 keep-alive idle timeout이 이 60초보다 짧을 때입니다. 백엔드가 유휴 커넥션을 먼저 닫으면 nginx는 죽은 커넥션인 줄 모르고 다음 요청을 밀어넣고, 그 순간 “prematurely closed”와 함께 502가 됩니다. GET 같은 멱등 요청은 proxy_next_upstream이 조용히 재시도해 주지만 POST, PATCH는 자동 재시도 대상이 아니라서 502가 사용자에게 그대로 나갑니다. POST에서만 보였던 이유가 이거였습니다.
우리 환경은 조건이 더 나빴다
백엔드는 uWSGI HTTP 라우터(http = :8080)를 직접 쓰는 구성인데 http-keepalive 옵션이 없었습니다. 이 상태의 uWSGI는 매 응답 후 Connection: close로 끊습니다. nginx는 붙잡아두려 하고 백엔드는 매번 끊으니 race 창이 상시 열려 있던 셈입니다. 덤으로 harakiri = 120이 proxy_read_timeout 기본값 60초보다 길어서, nginx가 504를 낸 뒤에도 uWSGI 워커는 120초까지 그 요청을 붙들고 있는 불일치도 이참에 발견했습니다.
조치
upstream 블록에 keepalive 0을 넣어 1.29.0 시절 동작으로 되돌렸습니다.
upstream base {
server 127.0.0.1:8080;
keepalive 0;
}
keep-alive를 살리는 방향(uWSGI http-keepalive 활성화, idle timeout을 60초보다 길게)도 검토했지만 접었습니다. 백엔드가 loopback 평문이라 TCP 셋업 절약이 사실상 없고, processes=N에 threads=1인 동기 모델에서는 유휴 커넥션이 워커를 점유하는 쪽이 오히려 손해라고 판단했습니다. 적용 후 502는 사라졌습니다.
이번에 확실해진 건 timeout에 두 축이 있다는 감각입니다. 하나는 요청 처리 시간의 상한(proxy_connect/read/send_timeout, harakiri), 다른 하나는 유휴 커넥션의 수명(upstream keepalive_timeout과 백엔드 idle timeout)입니다. 이번 502는 순전히 후자의 문제였고, 처음에 만졌던 keepalive_timeout 65는 둘 중 어느 쪽도 아니었습니다. 마이너 업그레이드라도 changelog의 기본값 변경, 특히 upstream 관련 항목은 먼저 훑어보자는 교훈이 남았습니다.
