본문으로 건너뛰기

TCP가 복구해도 서비스는 느려질 수 있다

TCP가 손실된 바이트를 다시 보냈다고 해서 기다린 시간까지 사라지지는 않습니다. 애플리케이션에서는 이 시간이 소켓 읽기와 HTTP 응답 지연으로 나타날 수 있습니다.

복구 중에도 전송량은 제한된다

손실 구간을 찾았다고 모두 한꺼번에 재전송하는 것은 아닙니다. RFC 9937의 PRR(Proportional Rate Reduction)은 ACK로 전달이 확인된 양과 혼잡 제어가 정한 ssthresh를 기준으로 빠른 복구 중 전송량을 조절합니다. 손실 감지와 전송량 조절은 서로 다른 역할입니다.

빈 구간은 애플리케이션의 대기 시간이 된다

RFC 9293은 TCP가 애플리케이션에 신뢰할 수 있고 순서가 보장되는 바이트 스트림을 제공한다고 설명합니다. 수신 측 TCP는 순서가 뒤집혀 도착한 세그먼트를 버퍼에 보관할 수 있지만, 앞에 비어 있는 바이트 구간을 건너뛰어 일반 TCP 스트림으로 전달할 수는 없습니다.

예를 들어 다음과 같이 세 구간을 보냈다고 가정하겠습니다.

text
송신: [0..999] [1000..1999] [2000..2999]
수신: [0..999] [2000..2999]
↑ 아직 도착하지 않은 구간

마지막 구간이 먼저 도착해도 애플리케이션은 1000..1999가 복구될 때까지 그 뒤의 바이트를 순서대로 읽을 수 없습니다. 이 대기는 같은 TCP 스트림 안에서 앞의 빈 구간이 뒤의 데이터를 막는 현상입니다.

소켓 API에 따라 블로킹 read는 데이터나 타임아웃을 기다리고, 논블로킹·비동기 API는 준비 상태나 이벤트를 돌려줍니다. 이미 읽을 수 있는 바이트가 있으면 일부만 반환할 수 있고, TCP에는 메시지 경계가 없어 HTTP 응답 하나가 여러 read로 나뉠 수 있습니다.

HTTP 응답 시간에서도 손실 복구 지연은 여러 요소 중 하나입니다. 요청을 보내는 구간에서 손실이 났다면 서버가 요청을 늦게 받습니다. 응답 구간의 손실이라면 클라이언트가 본문을 완성해서 읽는 시점이 늦어질 수 있습니다. 여기에 서버 처리 시간, 연결 수립, TLS, 큐 대기와 같은 시간이 함께 포함됩니다. 응답 시간이 길다는 사실만으로 TCP 손실을 원인으로 정할 수는 없습니다.

TCP 재전송과 애플리케이션 재시도

TCP 재전송은 같은 연결 안에서 확인되지 않은 바이트 구간을 다시 보내는 동작입니다. 애플리케이션은 대개 이 과정을 직접 알지 못합니다.

애플리케이션 재시도는 타임아웃 뒤 HTTP 요청 같은 논리적인 작업을 다시 수행합니다. 타임아웃이 끝나도 이미 보낸 요청이나 서버 처리가 자동으로 취소되지는 않으므로 첫 요청과 재시도가 겹칠 수 있습니다. HTTP/2 스트림을 취소하더라도 이미 TCP에 전달되어 송신 버퍼에 남은 미확인 바이트의 재전송이 자동으로 중단되는 것은 아닙니다. TCP는 HTTP/2의 스트림 경계를 알지 못합니다.

쓰기 요청을 재시도할 때는 특히 이 차이를 봐야 합니다. 클라이언트가 응답을 받지 못했더라도 서버는 요청을 처리했을 수 있습니다. 같은 결제나 주문 생성 요청을 그대로 다시 보내면 작업이 두 번 실행될 가능성이 있습니다. RFC 9110의 멱등성 규칙은 요청 자체가 멱등하게 처리된다는 근거가 있거나 원래 요청이 적용되지 않았음을 확인한 경우가 아니라면, 비멱등 메서드를 자동으로 재시도하지 말라고 권고합니다. 필요한 경우 요청마다 멱등성 키를 보내고 서버가 처리 결과를 보관하도록 설계할 수 있습니다.

애플리케이션 타임아웃과 TCP의 RTO는 별개입니다. 타임아웃을 너무 짧게 두면 복구될 요청까지 재시도해 서버 부하와 중복 처리 가능성이 커집니다.

p99를 해석할 때 확인할 것

손실 복구가 오래 걸린 일부 요청은 p99를 밀어 올릴 수 있지만, 손실률만으로 증가 폭을 계산할 수는 없습니다. RTT, 손실 위치, 전송량, 복구 방식, 애플리케이션 타임아웃이 함께 영향을 줍니다.

원인을 확인하려면 요청 ID나 트레이스와 TCP 연결의 4-tuple을 연결하고, 응답 시간과 재전송, RTO, Seq·ACK를 함께 봐야 합니다.