LabHub

Tomcat & nginx 운영 · 로그와 장애 진단 · 이론

502 에 타임아웃을 늘리는 사람이 되지 않기

LabHub 에서 이어서 보기

한 줄 요약

502 는 "유효하지 않은 응답을 받았다" 이고 504 가 "시간 안에 응답하지 않았다" 이며, 이 둘을 섞으면 타임아웃만 늘리다가 몇 시간을 태운다.

왜 이게 문제인가

장애 대응에서 시간을 가장 많이 잡아먹는 것은 원인을 못 찾는 것이 아니라 틀린 원인을 확신하는 것이다. 502 를 "백엔드가 죽었다" 로 단정하면 백엔드를 재기동하고, 안 되면 타임아웃을 늘리고, 그래도 안 되면 서버를 늘린다. 그동안 진짜 원인인 응답 헤더 크기나 커넥션 조기 종료는 손도 대지 않는다.

숫자가 있으면 이 확신이 근거로 바뀐다. 액세스 로그에서 URL 별 응답시간 상위와 5xx 분포를 뽑고, nginx 에러 로그에서 connection refused 와 upstream timed out 을 따로 세면 502 와 504 가 저절로 갈린다. 이 두 숫자를 세는 데 걸리는 시간은 1 분이다.

502 와 504 는 다른 이야기다

| 코드 | 의미 | nginx 가 이걸 낼 때 | 흔한 원인 |
| --- | --- | --- | --- |
| 502 Bad Gateway | 업스트림에서 유효하지 않은 응답을 받았거나 연결이 거부됨 | 연결 거부, 응답 파싱 실패, 커넥션 조기 종료 | 백엔드 다운, 백로그 포화, 응답 헤더가 버퍼보다 큼 |
| 504 Gateway Timeout | 업스트림이 정해진 시간 안에 응답하지 않음 | proxy_read_timeout 초과 | 느린 쿼리, 외부 연동 지연, 락 대기 |
| 503 Service Unavailable | 사용 가능한 업스트림이 없음 | 모든 서버가 실패 처리로 제외됨 | 전체 장애, 헬스 판정 오류 |

여기서 가장 자주 틀리는 지점은 이것이다.
502 를 "업스트림이 죽었다"로 단정하는 것.

규범이 말하는 502 는 "게이트웨이가 상류로부터 유효하지 않은 응답을 받았다"이지
"상류가 응답하지 않았다"가 아니다. 응답이 오긴 왔는데 프록시가 처리하지 못한 경우도 502 다.
대표적인 것이 응답 헤더가 프록시 버퍼보다 큰 경우다.

이 상황이 고약한 이유: 업스트림 헬스체크는 계속 정상으로 나온다.
헬스체크 응답은 헤더가 작으니까. 그래서 "서버는 멀쩡한데 특정 사용자만 502"가 된다.
그 특정 사용자는 대개 SSO 로 로그인해서 쿠키가 큰 사용자다.

proxy_buffer_size       16k;proxy_buffers         4 32k;proxy_busy_buffers_size 64k;

그리고 반드시 기억할 것: 502 가 나는데 타임아웃을 늘리는 것은 아무 효과가 없다.
타임아웃은 504 의 이야기다. 이 둘을 섞으면 몇 시간을 낭비한다.

진단 사다리 — curl 다섯 단계

증상이 애매할 때 이 순서로 좁힌다. 특히 5번이 탐색 공간을 절반으로 줄인다.

# 1. 이름이 풀리는가getent hosts api.example.com# 2. 포트가 열려 있는가 (ping 은 이 환경에서 안 되니 쓰지 않는다)curl -s -o /dev/null -w '%{http_code}\n' --connect-timeout 3 http://api.example.com/# 3. 상태코드와 헤더curl -sSI https://api.example.com/health# 4. 시간 분해curl -s -o /dev/null -w 'dns=%{time_namelookup} conn=%{time_connect} \tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://api.example.com/# 5. ★ 프록시를 건너뛰고 업스트림에 직접, 원래 Host 헤더를 유지한 채curl -sSI -H 'Host: api.example.com' http://127.0.0.1:8080/health

5번의 결과 해석이 핵심이다.

-H 'Host: ...' 를 빼면 이 비교는 무의미하다. 가상 호스트 라우팅이 걸린 경우
Host 가 다르면 아예 다른 앱으로 가기 때문이다.

4번의 시간 분해도 유용하다.

GC 로그를 읽는 최소한

[2026-08-19T02:14:33.221+0900][12.334s] GC(41) Pause Full (System.gc()) 486M->402M(512M) 812.443ms

이 한 줄에서 읽을 것.

판단 기준은 절대값이 아니라 추세와 패턴이다.

OOM 이 났을 때

java.lang.OutOfMemoryError 는 종류에 따라 조치가 완전히 다르다.

| 메시지 | 뜻 | 첫 조치 |
| --- | --- | --- |
| Java heap space | 힙 부족 또는 누수 | 힙 덤프 분석. 무작정 -Xmx 올리면 누수는 그대로 |
| Metaspace | 클래스 메타데이터 부족 | 반복 배포로 클래스로더 누수 의심. 재기동 후 관찰 |
| GC overhead limit exceeded | GC 에 시간 대부분을 쓰는데 회수가 거의 안 됨 | 사실상 누수. 힙 덤프 |
| unable to create native thread | 스레드 생성 실패 | 힙이 아니라 OS 한계/스레드 누수. 스레드 덤프 |

마지막 줄이 특히 헷갈린다. OOM 인데 힙 문제가 아닌 경우다.
-Xmx 를 올리면 오히려 나빠질 수 있다(힙이 커지면 스레드용 메모리가 줄어든다).

그리고 덤프는 터지는 순간에만 받을 수 있다.
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<경로> 를 미리 넣어 두지 않으면
"재현되면 그때 보죠"가 되고, 재현은 대개 안 된다.

재기동 전에 증거를 남긴다

장애 대응의 원칙은 복구 우선이다. 다만 재기동 버튼을 누르기 전 1~2분이면
이 셋은 확보할 수 있다.

PID=$(pgrep -f tomcat | head -1)jcmd $PID Thread.print            > /tmp/threads_$(date +%H%M%S).txtjcmd $PID GC.heap_info            > /tmp/heap_$(date +%H%M%S).txtcp /opt/tomcat/logs/catalina.out    /tmp/catalina_$(date +%H%M%S).out

이 1~2분을 아끼면 원인을 영원히 못 찾는다.
"일단 재기동했더니 됐어요"가 세 번 반복되면 네 번째엔 재기동으로도 안 되고,
그때는 증거가 하나도 없다.

로그에서 실제로 뽑아야 하는 것

액세스 로그에서 즉시 뽑아야 할 네 가지.

1. 상태코드 분포 — 5xx 가 언제부터 늘었는가
2. 응답시간 상위 URL — 어디가 느린가
3. 업스트림별 오류 분포 — 특정 서버만 문제인가
4. 분당 요청 수 — 트래픽 급증이 원인인가, 결과인가

4번이 중요하다. 장애 때 요청이 늘어난 것이 원인일 수도 있지만,
사용자가 안 되니까 새로고침을 반복해서 늘어난 결과일 때도 많다.
시작 시각을 정확히 보면 구분된다.

현장에서 만나는 모습

가장 고약한 형태는 "서버는 멀쩡한데 특정 사용자만 502" 다. 헬스체크 응답은 헤더가 작아서 늘 통과하고, 세션 쿠키가 큰 일부 사용자만 프록시 버퍼를 넘겨 502 를 받는다. 이때 모니터링 대시보드는 전부 초록색이라 사용자 문의가 유일한 신호다.

그리고 재기동 전에 증거를 남기는 습관이 필요하다. 힙 덤프와 스레드 덤프는 재기동하는 순간 사라지고, 같은 장애가 다시 날 때까지 원인을 알 수 없게 된다.