Tomcat & nginx 운영 · 로그와 장애 진단 · 이론
502 에 타임아웃을 늘리는 사람이 되지 않기
한 줄 요약
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/health5번의 결과 해석이 핵심이다.
- 업스트림 직접 호출이 정상 → 문제는 프록시와 업스트림 사이(설정, 헤더, 버퍼, 타임아웃)
- 업스트림 직접 호출도 실패 → 프록시는 무죄. 애플리케이션이나 DB 를 봐야 한다
-H 'Host: ...' 를 빼면 이 비교는 무의미하다. 가상 호스트 라우팅이 걸린 경우
Host 가 다르면 아예 다른 앱으로 가기 때문이다.
4번의 시간 분해도 유용하다.
time_appconnect자체가 길다 → TLS 핸드셰이크. 인증서 체인 검증이나 OCSP 조회 의심time_appconnect와time_starttransfer사이가 벌어진다 → 애플리케이션/DB.
네트워크가 아니다
GC 로그를 읽는 최소한
[2026-08-19T02:14:33.221+0900][12.334s] GC(41) Pause Full (System.gc()) 486M->402M(512M) 812.443ms이 한 줄에서 읽을 것.
Pause Full— Full GC 다. 잦으면 문제다486M->402M(512M)— 회수 전 → 회수 후 (전체 힙). 회수 후가 전체에 근접하면812.443ms— 이 시간 동안 애플리케이션이 멈춘다. 응답시간 튐의 정체가 이것일 때가 많다
메모리가 실제로 부족한 것이다. 회수가 잘 되는데 자주 도는 것과는 다른 문제다
판단 기준은 절대값이 아니라 추세와 패턴이다.
- Full GC 후에도 사용량이 계속 우상향 → 메모리 누수 의심 → 힙 덤프
- Full GC 는 드문데 Young GC 가 매우 잦음 → 단기 객체 과다 생성 → 코드 문제
- 특정 시각(배치 시간)에만 몰림 → 그 배치가 범인
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 를 받는다. 이때 모니터링 대시보드는 전부 초록색이라 사용자 문의가 유일한 신호다.
그리고 재기동 전에 증거를 남기는 습관이 필요하다. 힙 덤프와 스레드 덤프는 재기동하는 순간 사라지고, 같은 장애가 다시 날 때까지 원인을 알 수 없게 된다.