서버가 느린 이유 #4 대역폭은 충분한데 느릴 때: 지연시간, RTT, 재전송

5 분 소요

회선은 1Gbps인데 API 응답이 느립니다. 대역폭 그래프를 보면 사용량은 10%도 안 됩니다. 대역폭이 남는데 왜 느릴까요? 하드웨어 기초 #6에서 대역폭과 지연시간이 서로 다른 축이라는 개념을 잡았는데, 이번 글은 그 구분을 실제 진단에 적용합니다. 결론을 먼저 적으면, 작은 요청이 많은 트래픽의 체감 속도는 대역폭이 아니라 왕복(RTT) 횟수 × 왕복 시간으로 정해집니다.

체감 속도의 공식: RTT × 왕복 횟수 #

서울과 미국 서부 사이의 RTT는 빛의 속도가 정한 물리 한계 때문에 아무리 좋은 회선이라도 130ms 언저리입니다. 이 경로에서 HTTPS 요청 하나가 새 커넥션으로 나가면 왕복이 이렇게 쌓입니다.

단계왕복누적 시간 (RTT 130ms)
TCP 핸드셰이크1 RTT130ms
TLS 1.3 핸드셰이크1 RTT260ms
HTTP 요청/응답1 RTT390ms

실제 데이터는 거의 보내지도 않았는데 400ms에 가까운 시간이 흘렀습니다. 대역폭을 10Gbps로 올려도 이 숫자는 1ms도 줄지 않습니다. 왕복 횟수를 줄이거나(커넥션 재사용, TLS 세션 재개), 왕복 거리를 줄이는(리전 이전, CDN, 엣지) 것만이 처방입니다.

같은 구조가 서비스 내부에서도 반복됩니다. 요청 하나를 처리하며 마이크로서비스 호출 5번, 데이터베이스 쿼리 10번이 순차로 나간다면, 내부 RTT가 1ms라도 왕복 15번이 곱해집니다. 여기에 리전을 잘못 배치해 내부 호출이 리전을 넘으면 곱해지는 단위가 수십 밀리초로 뜁니다. 순차 호출을 병렬로 묶거나 배치로 합치는 것이 대역폭 증설보다 훨씬 큰 효과를 내는 이유입니다.

커넥션 재사용: 왕복을 없애는 가장 손쉬운 방법 #

위 표에서 TCP와 TLS의 2왕복은 커넥션을 재사용하면 통째로 사라집니다. 그래서 점검 순서의 첫 번째는 항상 재사용 여부입니다.

  • HTTP 클라이언트가 keep-alive와 커넥션 풀을 실제로 쓰고 있는지 확인합니다. 요청마다 새 클라이언트 객체를 만드는 코드는 매번 핸드셰이크를 다시 치릅니다.
  • 서버·프록시의 idle timeout이 너무 짧으면 풀에 있던 커넥션이 계속 끊겨 재사용률이 떨어집니다.
  • DNS 조회도 왕복입니다. 캐시 TTL이 0이거나 리졸버가 느리면 요청마다 수십 밀리초가 얹힙니다.

재사용이 되고 있는지는 서버 쪽에서 신규 커넥션 비율로 확인할 수 있습니다. 요청 수와 TCP accept 수가 비슷하다면 재사용이 거의 안 되고 있다는 뜻입니다.

대용량 전송이 대역폭을 못 채울 때: BDP와 윈도우 #

반대로 파일 전송·복제처럼 큰 데이터가 문제라면 대역폭 방향을 봐야 하는데, 여기도 지연시간이 끼어듭니다. TCP는 확인 응답을 받기 전에 보낼 수 있는 양(윈도우)이 정해져 있어서, 한 커넥션의 처리량 상한은 윈도우 크기 ÷ RTT입니다. 경로의 대역폭을 다 채우는 데 필요한 윈도우가 대역폭 × RTT, 즉 BDP(bandwidth-delay product)입니다.

1Gbps × RTT 130ms면 BDP는 약 16MB입니다. 윈도우가 4MB에 머물면 이 경로에서 한 커넥션이 낼 수 있는 최대치는 250Mbps 언저리입니다. 회선이 남는데 전송이 느린 전형적인 구조입니다. 장거리 전송이 느리면 커널의 TCP 버퍼 상한(net.ipv4.tcp_rmem/tcp_wmem)이 BDP보다 작지 않은지 확인하고, 애플리케이션 레벨에서는 병렬 스트림으로 나누는 것이 실무적인 우회입니다.

패킷 손실과 재전송: 소량으로도 파괴적입니다 #

TCP는 손실을 혼잡 신호로 해석해 전송 속도를 즉시 줄입니다. 그래서 손실률 0.1%처럼 작아 보이는 수치도 장거리·고대역 경로의 처리량을 수십 분의 1로 깎을 수 있고, 손실된 패킷의 재전송 타임아웃은 개별 요청에 수백 밀리초의 스파이크를 만듭니다. “평소엔 빠른데 가끔 한 번씩 튄다"는 증상의 흔한 범인입니다.

재전송은 소켓 단위로 직접 확인할 수 있습니다.

ss -ti: 재전송 확인
$ ss -ti dst 10.0.3.7
ESTAB  0  0  10.0.1.21:44712  10.0.3.7:5432
     cubic rto:204 rtt:1.8/0.4 retrans:0/842 bytes_retrans:1218432

retrans의 누적값(842)과 bytes_retrans가 계속 자란다면 이 경로는 손실을 겪고 있습니다. 경로 어디서 새는지는 mtr로 홉별 손실률을 보고 좁힙니다. 원인은 회선 품질만이 아니라 과부하 스위치의 큐 드랍, 잘못된 duplex 협상, MTU 불일치(경로 MTU 탐색 실패로 큰 패킷만 사라지는 경우)처럼 설비 쪽인 경우도 많습니다. “작은 응답은 멀쩡한데 큰 응답만 멈춘다"면 MTU를 의심합니다.

측정 도구 정리: 축을 나눠서 잽니다 #

진단이 헷갈리는 이유는 지연, 대역폭, 손실이 “느리다"라는 한 덩어리로 체감되기 때문입니다. 축을 나눠서 각각 잽니다.

  • RTT와 경로: ping으로 왕복 시간을, mtr로 홉별 지연과 손실을 잽니다. 부하 중과 한가할 때를 비교해 큐잉 지연(버퍼블로트)도 봅니다.
  • 대역폭: iperf3로 경로의 실제 달성 가능 처리량을 잽니다. 회선 스펙이 아니라 실측이 기준입니다.
  • 재전송: ss -ti, 서버 전체로는 nstat의 TcpRetransSegs 추이를 봅니다.
  • 왕복 횟수: 애플리케이션 트레이싱(요청 하나가 만드는 내부 호출 수)을 봅니다. 시스템 도구로는 보이지 않는 축입니다.

진단 순서 정리 #

증상이 “대역폭은 남는데 느리다"일 때, 순서는 이렇습니다.

  1. RTT부터 잽니다: pingmtr를 씁니다. 경로의 물리 거리가 곧 하한입니다.
  2. 왕복 횟수를 셉니다: 새 커넥션 비율(핸드셰이크 낭비)과 요청당 내부 호출 수를 봅니다. 곱셈을 줄이는 것이 최우선 처방입니다.
  3. 재전송 확인: ss -timtr의 홉별 손실로 확인합니다. 있다면 설비와 경로 문제로 방향을 바꿉니다.
  4. 대용량 전송이 문제라면 BDP 계산: 윈도우·버퍼가 BDP에 미치는지 확인합니다.
  5. 그래도 남으면 서버 쪽: 여기서부터는 네트워크가 아니라 상대 서버의 처리 지연입니다. 1편의 진단으로 넘어갑니다.

정리 #

  • 작은 요청 트래픽의 체감 속도는 RTT × 왕복 횟수입니다. 대역폭 증설은 이 곱셈에 아무 효과가 없습니다.
  • 커넥션 재사용(keep-alive, 풀, TLS 세션 재개)은 요청당 2왕복을 공짜로 없애는 첫 번째 처방입니다.
  • 한 커넥션의 처리량 상한은 윈도우 ÷ RTT입니다. 장거리 대용량 전송은 BDP 대비 버퍼를 확인합니다.
  • 0.1%의 패킷 손실도 처리량을 수십 분의 1로 깎을 수 있습니다. ss -ti의 retrans와 mtr로 확인합니다.
  • 지연, 대역폭, 손실, 왕복 횟수는 서로 다른 축입니다. 나눠서 재야 처방이 나옵니다.

다음 편이 시리즈 마지막, 데이터베이스입니다. 잘 돌던 데이터베이스가 느려지는 이유를 인덱스, 락, 커넥션 풀로 추적합니다.

X