Skip to main content
계속 실패하면 고장 난 곳을 찾기 쉬울 것 같죠? 사실은 한동안 조용한 뒤 첫 요청만 실패하는 장애가 더 찾기 어려워요.
Proxy, Reverse Proxy, 그리고 Load Balancer에서는 브라우저와 앱 사이에 앞단 프록시가 설 수 있다는 큰 그림을 봤어요. 그리고 Connection reuse, Keep-Alive, Pooling에서는 앞단이 오리진 연결을 매번 새로 열지 않고 pool에 남겨뒀다가 다시 쓸 수 있다는 걸 봤죠. 이번 글은 그중에서도 앞단은 아직 쓸 수 있다고 생각했는데, 오리진은 이미 닫아버린 연결이 실제 장애에서 어떻게 보이는지 따라가 볼게요. 장면은 이래요.
  • 평소 요청은 대부분 200 OK예요.
  • 트래픽이 잠깐 끊긴 뒤 첫 요청에서만 가끔 502 Bad Gateway가 나요.
  • 바로 새로고침하면 정상으로 돌아와요.
  • 실패한 시각의 앱 access log에는 요청이 없을 때가 있어요.
  • 앞단 로그에는 connection reset, upstream prematurely closed, remote close 같은 표현이 보여요.
이 증상만 보면 앱이 가끔 죽거나, 특정 서버가 불안정하거나, 사용자 네트워크가 끊긴 것처럼 느껴져요. 근데요, 실패한 건 새 연결이 아닐 수 있어요.
“앞단이 pool에서 꺼낸 그 연결은 정말 아직 살아 있었을까요?”
여기서는 HTTP/1.1 기반의 앞단-오리진 연결 재사용 장면을 중심으로 봐요. 제품마다 pool 관리, 사전 연결 검사, 재시도, 로그 문구, 502 변환 방식은 달라요. 그래서 특정 로그 한 줄을 정답처럼 외우기보다 idle 시간 뒤 첫 요청, 오리진 로그 유무, 재시도 흔적, FIN/RST 시점을 함께 읽는 데 집중할게요.

먼저 장애 타임라인부터 맞춰볼게요

쇼핑몰 API 앞에 리버스 프록시가 있고, 그 뒤에 오리진 앱 서버가 있다고 해볼게요. 앞단은 사용이 끝난 upstream 연결을 최대 60초 동안 pool에 남겨두고, 오리진은 30초 동안 요청이 없으면 연결을 닫는 설정이에요. 처음 요청은 정상이에요.
이 타임라인에서 중요한 건 10:00:30 근처의 아주 짧은 순간이에요. 오리진의 종료 신호를 앞단이 처리해 pool에서 빼는 일과, 마침 도착한 요청을 그 연결에 배정하는 일이 엇갈릴 수 있어요. 이 그림은 “timeout이 다르면 반드시 502가 난다”는 뜻은 아니에요. 정상 구현은 종료 신호를 감지한 연결을 pool에서 빼요. 다만 연결 종료와 다음 요청 배정이 비슷한 순간에 일어나거나, 중간 장비가 조용히 상태를 지웠거나, 제품별 검사·재시도 정책이 다르면 짧은 경합 구간에서 실패가 드러날 수 있어요. HTTP/1.1 표준도 연결은 의도와 상관없이 언제든 닫힐 수 있고, 서버가 idle 연결을 닫으려는 순간 클라이언트가 새 요청을 보내는 경합이 생길 수 있다고 설명해요. 그래서 persistent connection을 쓰는 양쪽은 비동기 종료를 예상해야 해요.

기본 감각을 실제 장애 신호로 바꿔봐요

연결 재사용 글에서 본 감각을 이번 사례에 옮기면 이렇게 돼요. 여기서 stale connection은 캐시의 오래된 사본을 말하는 게 아니에요. pool에는 남아 있지만 실제 peer 상태와 어긋난 연결을 설명하기 위한 운영 표현에 가까워요.

먼저 읽을 신호 여섯 가지

이 장애는 에러율이 낮아서 한 줄만 보면 우연처럼 지나가기 쉬워요. 아래 신호를 같은 시간축에 모아야 패턴이 보여요. 이 흐름에서 오리진 로그가 없음은 원인 확정이 아니에요. 로그 수집 누락일 수도 있고, 다른 프로세스나 다른 target으로 갔을 수도 있어요. 하지만 request id와 target 주소까지 맞췄는데도 앱 access log가 없다면, 앱의 HTTP 핸들러보다 앞에서 실패했을 가능성이 커져요.

로그는 한 요청의 양쪽 얼굴로 읽어요

아래는 읽기 연습용 예시예요. 특정 제품의 실제 고정 문구는 아니에요. 앞단 access log에는 이렇게 남았다고 해볼게요.
여기서 읽을 신호는 네 가지예요.
  1. status=502라서 앞단이 오류 응답을 만들었을 가능성이 있어요.
  2. upstream=10.0.2.17:8080으로 어느 target이었는지 알 수 있어요.
  3. upstream_connect_time=0.000은 새 연결 비용이 거의 없거나 기존 연결을 사용한 장면일 수 있어요.
  4. before response headers라서 오리진 응답 헤더를 받기 전에 연결이 끝났어요.
같은 시각의 오리진 access log에는 req_a81f도, 해당 path도 없어요.
이때 10:00:31의 성공 요청은 사용자의 새로고침일 수도 있고, 앞단이 새 연결로 다시 보낸 요청일 수도 있어요. 그래서 시간만 보지 말고 request id, forwarded request id, source port, retry attempt 같은 연결 고리를 같이 봐야 해요.
로그의 0, -, 빈 값이 무엇을 뜻하는지는 제품마다 달라요. 연결 재사용 여부를 직접 기록하는 필드가 있다면 그 값을 우선하고, 없으면 connect time, connection id, source port, 패킷을 조합해 추정해야 해요.

패킷에서는 FIN과 RST의 순서를 봐요

로그만으로 애매하면 앞단과 오리진 사이를 짧게 캡처할 수 있어요. 아래 예시는 실제 캡처가 아니라 흐름을 읽기 위한 축약 예시예요.
이 모양이라면 오리진이 연결 종료를 시작한 순간과 앞단의 재사용 시도가 거의 겹쳤고, 같은 4-tuple로 데이터를 보내려다가 reset을 받은 흐름을 의심할 수 있어요. 하지만 현실의 캡처는 항상 이렇게 깔끔하지 않아요.
  • 앞단이 FIN을 받고 즉시 ACK한 뒤 pool에서 제거할 수 있어요.
  • FIN과 새 요청이 거의 동시에 지나가 경합처럼 보일 수 있어요.
  • 중간 NAT나 방화벽이 idle 상태를 먼저 지워서 RST 또는 무응답이 생길 수 있어요.
  • 캡처 위치에 따라 한쪽 패킷만 보일 수 있어요.
  • TLS upstream이면 HTTP path는 안 보이고 TCP/TLS 종료 신호만 보일 수 있어요.
그래서 패킷에서 볼 것은 단순히 RST가 있다가 아니에요. 이 그림에서 실제 장애를 만드는 건 timeout 숫자 두 개만이 아니에요. 종료를 감지하고 pool에서 제거하는 방식, 요청을 이미 얼마나 보냈는지, 재시도를 허용하는지가 같이 결과를 만들어요.

왜 새로고침하면 바로 성공할까요?

첫 요청이 실패하면서 문제 연결이 pool에서 제거됐다고 해볼게요. 다음 요청은 더 이상 그 연결을 꺼낼 수 없으니 새 TCP 연결을 만들어요.
사용자 입장에서는 **“한 번 오류가 났지만 새로고침하니 됐다”**예요. 운영자 입장에서는 더 까다로워요. 재현 버튼을 누르는 순간 두 번째 요청이 되어 정상만 보일 수 있거든요. 그래서 재현할 때는 요청을 빠르게 반복하기보다, 의심되는 idle 시간보다 조금 더 기다린 뒤 첫 요청의 결과를 기록해야 해요.
예를 들어 오리진 timeout이 30초로 의심된다면 5초, 20초, 29초, 31초, 45초처럼 대기 시간을 바꿔 비교할 수 있어요.
주문 생성, 결제, 메시지 발송 같은 요청은 실패 응답을 받았더라도 서버에서 처리됐을 가능성을 완전히 배제하기 어려워요. 재현은 부작용 없는 전용 endpoint나 읽기 요청으로 하고, 운영 트래픽에서 재시도 정책을 바꿀 때는 중복 처리 위험도 함께 봐야 해요.

재시도는 502를 숨길 수도, 중복 처리를 만들 수도 있어요

앞단이 재사용 연결 실패를 감지한 뒤 새 연결로 자동 재시도하면 사용자는 200만 볼 수 있어요. 그렇다고 장애가 없는 건 아니에요.
이때 사용자 오류율은 낮아도 내부에서는 다음 신호가 늘 수 있어요.
  • upstream retry count
  • remote reset count
  • 새 연결 생성률
  • 첫 시도 실패율
  • tail latency
반대로 재시도하지 않으면 사용자가 간헐적 502를 직접 봐요. 문제는 요청이 어디까지 처리됐는지 모호할 때예요. GET처럼 같은 요청을 반복해도 의도한 효과가 달라지지 않는 메서드는 자동 재시도를 설계하기 상대적으로 쉬워요. 하지만 POST 같은 비멱등 요청은 첫 시도가 오리진에 적용됐는지 확신할 수 없다면 자동 재시도가 중복 주문이나 중복 결제로 이어질 수 있어요. RFC 9110의 멱등 메서드 설명도 자동 재시도가 가능한 이유와 비멱등 요청을 조심해야 하는 이유를 구분해요.
재시도가 성공하면 가용성은 좋아지지만, timeout mismatch나 연결 종료 경합이 해결된 건 아니에요. retry success와 함께 첫 시도 reset, 새 연결 생성률, 추가 지연을 계속 봐야 해요.

복구는 timeout 하나를 무작정 늘리는 일이 아니에요

이 장애를 만나면 흔히 “keep-alive를 더 길게 하자” 또는 “더 짧게 하자”로 바로 가요. 그런데 어느 쪽 설정을 바꾸는지, 중간 장비가 있는지, 연결 수가 얼마나 늘어나는지를 같이 봐야 해요. 큰 방향은 재사용하는 쪽이 peer보다 오래된 연결을 계속 후보로 들고 있지 않도록 정렬하고, 종료 신호를 제때 처리하며, 안전한 재시도와 관측을 갖추는 것이에요.
1

Keep-alive mismatch 가설을 세워요

오래 쉬었던 연결을 재사용한 직후 reset이나 502가 생기는지 확인해요. 증상이 idle 시간과 무관하다면 다른 원인도 열어 둬요.
2

실제 실패 구간을 확정해요

client, proxy, origin 중 누가 연결을 재사용했고 누가 먼저 닫았는지 packet과 log로 확인해요. 502를 반환한 장비와 연결을 종료한 장비는 다를 수 있어요.
3

모든 timeout 값을 수집해요

앞단, origin, load balancer, NAT의 keep-alive와 idle timeout을 한 표에 놓아요. 이름이 비슷한 connect·read timeout과 섞지 않아요.
4

종료와 재사용의 경합을 재현해요

idle 경계 바로 전과 후에 같은 연결을 재사용해 실패 조건을 반복해요. 우연한 네트워크 오류가 아니라 timeout 순서에서 생긴 문제인지 증명해요.
5

Timeout과 pool 정책을 맞춰요

재사용하는 쪽이 상대보다 먼저 연결을 폐기하도록 수명을 조정해요. pool의 validation과 eviction 정책도 실제 트래픽 간격에 맞춰요.
6

안전한 재시도 범위를 정해요

멱등한 요청만 자동 재시도할지, body 전송 전 실패만 허용할지 결정해요. 재시도가 502를 숨기면서 중복 처리를 만들지 않아야 해요.
7

Idle 경계 전후를 다시 검증해요

조정한 timeout보다 짧고 긴 대기 시간을 모두 시험해요. 한 번의 성공보다 반복 실행에서 reset이 사라지는지가 중요해요.
8

연결과 오류 지표를 관찰해요

reset, retry, 502, active·idle 연결 수를 함께 봐요. 오류만 줄고 새 연결 폭증이나 latency 증가가 생기지 않았는지도 확인해요.
설정을 바꿀 때는 아래를 같이 확인해요. NGINX처럼 upstream keepalive cache의 최대 idle 연결 수와 idle timeout을 따로 두는 구현도 있고, Envoy처럼 HTTP connection idle timeout과 request/stream timeout을 구분하는 구현도 있어요. 같은 timeout이라는 이름이어도 적용 대상이 다르므로, 반드시 제품 문서에서 어느 연결과 어느 상태에 적용되는지 확인해야 해요.

잘못 읽기 쉬운 함정 일곱 가지

하나, 간헐적 502를 모두 keep-alive 문제로 보기.
502는 upstream 연결 거부, TLS 실패, 잘못된 HTTP 응답, 프로세스 종료 같은 여러 원인으로 생겨요. idle 뒤 첫 요청이라는 시간 패턴과 연결 종료 신호가 같이 있어야 의심이 강해져요.
둘, timeout 값이 다르면 반드시 장애가 난다고 보기.
종료 신호를 정상적으로 감지해 pool에서 제거하면 문제없이 새 연결을 열 수 있어요. 숫자 차이는 단서이고, 실제 실패는 종료 감지와 요청 배정의 경합까지 봐야 해요.
셋, 앱 access log가 없으니 앱 서버는 무조건 정상이라고 보기.
요청이 HTTP 파서까지 못 갔을 수 있지만, 서버 프로세스 재시작이나 listener 종료가 연결을 끊었을 수도 있어요. 시스템 로그와 배포 시각도 확인해야 해요.
넷, RST 하나만 보고 오리진 잘못이라고 단정하기.
RST는 이미 닫힌 연결에 데이터가 왔을 때나 중간 장비 정책 등 여러 장면에서 보일 수 있어요. 이전 FIN, 마지막 데이터, 캡처 위치를 같이 봐야 해요.
다섯, 사용자 502가 없으니 문제가 없다고 보기.
프록시 재시도가 실패를 숨겼을 수 있어요. 첫 시도 reset과 retry count가 늘면 지연과 새 연결 비용은 남아요.
여섯, 모든 요청을 자동 재시도하기.
비멱등 요청은 첫 시도가 실제로 처리됐는지 모호할 수 있어요. 재시도 안전성은 메서드 이름만이 아니라 애플리케이션의 idempotency 설계까지 봐야 해요.
일곱, HTTP keep-alive와 TCP keepalive를 같은 설정으로 보기.
HTTP 연결 재사용을 위한 idle 정책과 TCP 수준의 liveness probe는 다른 메커니즘이에요. TCP keepalive를 켰다고 HTTP pool의 timeout mismatch가 자동으로 사라지지는 않아요.

복구 뒤에는 같은 idle 경계를 다시 통과해봐요

설정 반영 직후 연속 요청이 모두 성공했다고 끝내면 안 돼요. 이 장애는 기다린 뒤 첫 요청에서 나타났으니까요. 검증도 같은 모양이어야 해요.
그리고 성공 여부만 보지 말고 아래 지표를 같이 봐요. 이렇게 해야 “502만 안 보이게 만든 것”과 “연결 수명 정책을 실제로 맞춘 것”을 구분할 수 있어요.

자, 정리해볼까요?

  • 한동안 조용한 뒤 첫 요청만 502가 난다면 idle 연결 재사용 실패를 후보에 넣어볼 수 있어요.
  • 앞단은 연결을 재사용할 수 있다고 생각하지만, 오리진이나 중간 장비는 이미 그 연결을 닫았을 수 있어요.
  • 진단할 때는 idle 간격, 앞단 upstream 오류, 오리진 access log, 재시도, connect time, FIN/RST를 같은 시간축에 놓아야 해요.
  • timeout 값이 다르다는 사실만으로 원인이 확정되지는 않아요. 종료 감지, pool 제거, 요청 배정의 경합까지 확인해야 해요.
  • 자동 재시도는 사용자 502를 숨길 수 있지만, 비멱등 요청에서는 중복 처리 위험을 만들 수 있어요.
  • 복구 뒤에는 연속 요청이 아니라 문제가 났던 idle 경계 뒤 첫 요청을 다시 검증해야 해요.

이어서 보면 좋은 글