
실제 재현 노트: 1초 제한인데 2.4초 응답은 성공한 이유
2026년 10월 12일 별도 로컬 Docker의 Nginx 1.30.4·Node.js 24.21.0(Linux arm64)에서 실제 HTTP 요청을 시험했습니다. 운영 장애나 성능 벤치마크가 아니라, 응답을 언제 보내는지 통제한 예제 앱과 Nginx의 동작을 비교한 실험입니다. TLS·CDN·로드밸런서·실제 DB는 포함하지 않았습니다.
요청 경로와 응답 시점을 통제했습니다
같은 내부 네트워크의 app:3000을 직접 호출하거나 Nginx를 거쳤습니다. /slow는 1500ms 후 처음 응답하고, /heartbeat는 헤더와 첫 조각을 즉시 보낸 뒤 600ms마다 네 조각을 더 보냅니다. /gap은 헤더·첫 조각을 보낸 뒤 1500ms 동안 다음 조각을 보내지 않습니다. 세 경로는 이번 검증을 위해 직접 작성한 앱 코드입니다.
# 실제 시험 설정의 공통 부분
proxy_pass http://app:3000;
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_buffering off;
proxy_connect_timeout 1s;
proxy_read_timeout 1s;
# 비교 서버 한 곳만 proxy_read_timeout 3s 적용
| 시험 조건 | HTTP | 본문까지 확인한 결과 |
| Nginx /health, 읽기 1초 | 200 | 정상 완결 |
| 앱 /slow 직접 호출 | 200 | 정상 완결, 1511.12ms |
| Nginx /slow, 읽기 1초 | 504 | 헤더 대기 중 시간 초과, 1021.16ms |
| Nginx /slow, 읽기 3초 | 200 | 정상 완결, 1529.45ms |
| Nginx /heartbeat, 읽기 1초 | 200 | 전체 조각 완결, 2423.63ms |
| Nginx /gap, 읽기 1초 | 200 | 첫 조각 뒤 중단, response_aborted |
표의 시간은 한 번씩 보낸 요청에서 Node 클라이언트가 관찰한 값입니다. 컨테이너 실행 환경과 스케줄링에 따라 달라지므로 평균·최대 처리량·보장 시간으로 쓰지 않습니다. 빠른 /health와 느린 /slow의 동시 관찰은 헬스체크 정상만으로 모든 경로의 504를 배제할 수 없음을 보여줍니다.
504뿐 아니라 ‘200인데 끊김’도 확인해야 합니다
/slow 실패 로그에는 while reading response header from upstream이 나왔습니다. 반면 헤더를 이미 보낸 /gap은 Nginx 접근 로그도 200이었지만 오류 로그에는 while reading upstream이 남았고 Node 응답의 aborted 이벤트가 발생했습니다. 클라이언트에는 늦은 두 번째 조각이 도착하지 않았습니다. 상태 코드가 200이라는 사실만으로 스트리밍 응답 성공을 판정하면 이 실패를 놓칩니다.
/heartbeat가 1초 제한 아래에서 2초 넘게 완료된 것은 제한이 전체 요청 시간이 아니라 연속 읽기 사이의 대기에 적용되기 때문입니다. 그렇다고 장애를 숨기기 위해 무의미한 heartbeat를 보내거나 모든 운영 제한을 3초로 바꾸라는 권장은 아닙니다. 실제 업무의 허용 지연, 최종 응답 완결, 앞단의 별도 총시간 제한을 함께 확인해야 합니다.
재현 코드·설정·고정 입력·결과는 아래 첨부 파일에 있습니다. 읽기 제한 1초·3초는 차이를 빨리 관찰하기 위한 시험 값이며 운영 권장값이 아닙니다. 공식 정의는 Nginx proxy_read_timeout에서 확인했습니다(확인일: 2026-10-12). 아래의 기존 진단 절차와 같은 경로 비교 조건은 유지했습니다.
이하 기존 점검 가이드
그림: 504는 단순히 제한 시간을 늘리기보다 어느 구간에서 응답이 멈췄는지 찾는 것이 우선입니다.
Nginx의 504 Gateway Timeout은 프록시가 뒤쪽 서버(upstream)의 응답을 기다리다 제한 시간에 도달했을 때 나타납니다. 설정값만 크게 늘리면 느린 DB 쿼리, 외부 API 대기, 컨테이너 과부하가 가려질 수 있습니다. 아래 순서대로 시간이 어디서 소모되는지 확인하면 수정 범위를 좁힐 수 있습니다.
1. 502와 504를 먼저 구분한다
502는 프록시가 upstream과 연결하거나 유효한 응답을 받는 과정에서 문제가 생길 때 흔합니다. 504는 연결 또는 응답 대기 중 제한 시간 초과가 핵심입니다. 다만 에러 코드만으로 원인을 단정하지 말고 Nginx error log의 문구를 확인해야 합니다. 예를 들어 upstream timed out ... while reading response header from upstream는 응답 헤더를 기다리던 상황을 알려줍니다.
2. 로그의 시각과 URL을 앱 로그에 맞춘다
sudo nginx -t
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/access.log
docker compose ps
docker compose logs --since 10m app
위 명령의 app은 실제 Compose 서비스명으로 바꾸세요. Nginx가 컨테이너 안에서 실행된다면 로그 경로 대신 해당 컨테이너의 stdout을 볼 수도 있습니다. 오류 시각, 요청 경로, upstream 주소를 기록하고 앱 로그에서 같은 시각의 DB 쿼리·외부 API 호출·작업 큐 지연을 찾습니다. 로그에 토큰이나 개인정보가 있다면 외부에 공유하기 전에 제거하세요.
3. 직접 호출과 프록시 호출의 시간을 비교한다
Nginx가 접근하는 네트워크 위치에서 upstream을 직접 호출해야 비교가 의미 있습니다. Docker Compose라면 같은 네트워크의 진단 컨테이너나 Nginx 컨테이너 안에서 서비스 이름과 포트로 호출하세요. 로컬 호스트의 localhost는 컨테이너 안의 localhost와 다릅니다.
curl -sS -o /dev/null -w 'connect=%{time_connect} total=%{time_total}\n' http://app:8080/slow-path
curl -sS -o /dev/null -w 'connect=%{time_connect} total=%{time_total}\n' https://example.com/slow-path
위 두 curl은 같은 읽기 전용 업무 경로 /slow-path를 비교하는 예시입니다. 실제로는 메서드·쿼리·필요한 인증 조건을 같게 맞추고, 직접 호출은 Nginx가 upstream에 접근하는 네트워크 위치에서 실행하세요. 프록시가 경로를 바꾸거나 Host 헤더에 따라 앱이 다르게 동작한다면 upstream이 받은 실제 경로·헤더도 대조해야 합니다. 비밀 토큰을 명령 이력이나 공유 로그에 남기지 마세요.
/health는 연결·기본 상태 확인용으로 별도로 사용할 수 있지만, /health의 빠른 응답과 다른 업무 경로의 느린 응답만으로 프록시 구간의 지연을 분리할 수는 없습니다. 이 예시는 비교 조건을 설명한 것이며 운영 서버에서 측정한 응답 시간 결과가 아닙니다.
헬스체크가 빠르더라도 실제 느린 경로의 DB와 외부 API가 병목일 수 있습니다. 읽기 전용이거나 테스트 가능한 요청으로 재현하고, 본문에 비밀값을 넣지 마세요.
| 관찰 결과 | 우선 의심할 구간 | 다음 점검 |
| 직접 호출도 느림 | 앱·DB·외부 API | 앱 타이밍, DB 쿼리, 작업 큐 |
| 연결 자체가 늦음 | 네트워크·DNS·포트·과부하 | Compose 네트워크, 연결 수, CPU·메모리 |
| 첫 바이트가 늦음 | 앱 처리·응답 헤더 생성 | 요청 핸들러와 의존 서비스 |
| 일부 큰 응답만 실패 | 응답 간 읽기 지연 | 스트리밍 방식과 timeout 설정 |
4. proxy_read_timeout의 뜻을 정확히 이해한다
Nginx 공식 문서에서 proxy_read_timeout은 upstream 응답을 읽는 두 연속 읽기 작업 사이의 제한 시간입니다. 전체 응답을 완성해야 하는 총 시간 제한과 같지 않습니다. 기본값은 공식 문서 기준 60초지만 배포판·설정 파일·상위 블록의 값이 실제 적용값을 결정합니다. 실제 설정은 nginx -T로 확인하세요. 연결 단계의 proxy_connect_timeout, 요청 전송 단계의 proxy_send_timeout과 혼동하지 않아야 합니다.
location /api/ {
proxy_pass http://app:8080;
proxy_connect_timeout 5s;
proxy_read_timeout 90s;
}
이 설정은 예시입니다. 장시간 작업이 정상 요구사항이라면 제한 시간을 조정할 수 있지만, 요청을 비동기 작업으로 분리하고 상태 조회 URL을 제공하는 편이 더 안정적인 경우도 있습니다. 변경 전후 응답 시간과 동시 요청 수를 측정하세요.
5. Docker 운영에서 함께 확인할 항목
- 컨테이너가 실행 중이고 재시작 루프가 없는지 확인합니다.
- upstream 서비스명·포트·네트워크가 실제 설정과 일치하는지 봅니다.
- CPU·메모리 제한과 OOM 기록을 확인합니다.
- DB 연결 풀과 외부 API 제한 시간에 대기 요청이 쌓이는지 측정합니다.
- 설정 변경 후 nginx -t가 통과할 때만 reload합니다.
- 정상 요청과 느린 요청을 모두 재시험합니다.
연결 실패 문구가 나온다면 Nginx 502와 Docker upstream 점검 가이드를, 컨테이너가 계속 재시작된다면 Docker Restarting 루프 점검을 참고하세요.
6. 자주 묻는 질문
Q. proxy_read_timeout을 300초로 늘리면 해결되나요?
정상적인 장시간 요청에는 필요할 수 있지만 병목 자체를 해결하지는 않습니다. 앱 처리 시간과 리소스 사용량부터 측정하세요.
Q. 헬스체크가 정상인데 왜 504가 나나요?
헬스체크는 보통 짧은 경로만 검사합니다. 특정 API의 DB 쿼리나 외부 호출이 느릴 수 있습니다.
Q. Nginx를 reload해도 바로 나아지지 않으면?
앱 자체가 느리거나 프록시 앞에 또 다른 로드밸런서의 제한 시간이 있을 수 있습니다. 요청 경로의 각 계층을 비교하세요.
공식 자료: Nginx proxy 모듈 지시어 · Docker Compose 네트워크 (확인일: 2026-09-24)
'소프트웨어개발' 카테고리의 다른 글
| 기업용 WAF 선택 기준: AWS WAF·Cloudflare 비교와 오탐·비용 체크리스트 (0) | 2026.09.25 |
|---|---|
| AWS S3 403 AccessDenied 해결 순서: IAM·버킷 정책·KMS·공개 차단 점검 (0) | 2026.09.24 |
| 기업용 로그 관리 솔루션 선택법: 수집·보존·검색·마스킹 비용 체크리스트 (0) | 2026.09.24 |