cloudflare

‘idle’이 유휴 상태가 아닐 때: 리눅스 커널 최적화가 QUIC 버그가 된 과정 (새 탭에서 열림)

CUBIC의 혼잡 윈도우(cwnd)가 심각한 손실 이후 최솟값에 고정되어 회복하지 못하는 QUIC 버그가 발견됐다. 손실이 2초 후 완전히 사라졌는데도 quiche의 CUBIC은 회복 상태와 혼잡 회피 상태를 RTT 주기로 반복하며 전송량을 늘리지 못했다. 원인은 Linux TCP CUBIC의 유휴 상태 최적화를 QUIC에 적용하는 과정에서 bytes_in_flight == 0 상황이 잘못 해석된 데서 비롯됐으며, 결국 매우 작은 수정으로 문제가 해결됐다.

CUBIC과 혼잡 윈도우의 역할

  • 혼잡 제어 알고리즘(CCA)은 송신자가 네트워크에 동시에 전송할 수 있는 데이터의 상한인 cwnd를 조절한다.
  • cwnd가 크면 한 번에 더 많은 데이터를 전송하고, 작으면 전송 속도가 제한된다.
  • 손실 기반 알고리즘인 CUBIC은 다음과 같은 전제를 따른다.
    • 패킷 손실이 없으면 네트워크 여유가 있다고 보고 전송률을 증가시킨다.
    • 패킷 손실이 발생하면 네트워크 용량을 초과했다고 판단해 전송률을 줄인다.
  • CUBIC은 Linux의 기본 TCP 혼잡 제어기이며, Cloudflare의 QUIC 구현인 quiche에서도 기본값으로 사용된다.

재현 조건과 이상 증상

  • 문제는 초반에 심각한 패킷 손실이 발생한 뒤 회복하는 통합 테스트에서 발견됐다.
  • 테스트 조건:
    • localhost에서 실행되는 HTTP/3 클라이언트와 서버
    • RTT 10ms
    • 10MB 파일 다운로드
    • CUBIC 사용
    • 연결 시작 후 2초 동안 무작위 30% 패킷 손실
    • 이후 패킷 손실 제거
    • 10초 타임아웃
  • 정상이라면 손실 구간에서 cwnd가 감소한 뒤, 손실이 사라지면 다시 증가해 4~5초 안에 다운로드를 완료해야 한다.
  • 그러나 100회 반복 시 약 60~61%가 10초 안에 완료되지 못했다.

손실이 없는데도 회복하지 못한 CUBIC

  • 2초 이후 패킷 손실은 완전히 사라졌지만 bytes_in_flightcwnd는 계속 최솟값에 머물렀다.
  • CUBIC은 약 6.7초 동안 회복 상태와 혼잡 회피 상태를 999회 반복했다.
  • 상태 전환 주기는 약 14ms로, 연결의 RTT 10ms와 비슷했다.
  • cwnd는 2700바이트, 즉 최대 세그먼트 크기의 패킷 2개 수준에 고정됐다.
  • 다운로드 서버 입장에서는 클라이언트의 ACK가 도착할 때마다 전송 중인 바이트가 0이 되고, 다시 두 패킷을 보내는 과정이 반복됐다.
  • 이 ACK 기반의 전송 리듬이 CUBIC의 상태 전환을 매 RTT마다 잘못 촉발한 것으로 분석됐다.

Reno 비교를 통한 CUBIC 특화 문제 확인

  • 동일한 테스트를 Reno로 실행한 결과 100% 성공했다.
  • Reno는 손실이 끝난 뒤 정상적으로 cwnd를 증가시키고 다운로드를 완료했다.
  • 따라서 네트워크 시뮬레이션이나 QUIC 전반의 문제가 아니라 CUBIC 구현의 특정 로직에 문제가 있음을 확인했다.

Linux TCP의 유휴 상태 최적화

  • 문제의 출발점은 2017년 Linux 커널에 적용된 TCP CUBIC 최적화였다.
  • 기존 구현에서는 CUBIC의 시간 기준점인 epoch가 연결 시작 시점과 손실 발생 시점에만 갱신됐다.
  • 애플리케이션이 오랫동안 유휴 상태에 있다가 다시 전송하면 현재 시각 - epoch_start 값이 지나치게 커질 수 있었다.
  • 그 결과 CUBIC의 목표 전송률과 증가 기울기가 비정상적으로 커지고, ca->cnt가 지나치게 작아질 수 있었다.
  • Linux는 이를 완화하기 위해 ca->cnt에 최솟값 2를 적용했으며, 특히 slow_start_after_idle이 비활성화된 경우 유휴 후 위험한 cwnd 증가가 발생할 수 있었다.
  • 이 최적화는 TCP에서 애플리케이션이 전송을 제한한 구간을 혼잡으로 오인하지 않도록 하기 위한 것이었지만, QUIC 구현에 이식되는 과정에서 bytes_in_flight == 0인 상황이 예상과 다르게 작동했다.

문제의 본질

  • CUBIC은 실제 패킷 손실이 없더라도 전송 중인 바이트가 0이 되는 순간을 유휴 또는 특수한 상태로 처리한다.
  • 하지만 이 테스트에서는 연결이 유휴한 것이 아니라, cwnd가 두 패킷으로 제한되어 ACK를 받은 뒤 잠시 bytes_in_flight가 0이 되는 상황이 반복됐다.
  • 결과적으로 CUBIC의 유휴 상태 처리와 회복 상태 전환이 ACK 클록과 결합되어 매 RTT마다 잘못된 상태 변화를 일으켰다.
  • 그 결과 혼잡 윈도우가 최소값에서 탈출하지 못하고, 손실이 사라진 뒤에도 전송 속도가 회복되지 않았다.

실용적인 시사점

  • 혼잡 제어 테스트는 정상적인 성장 구간뿐 아니라, cwnd가 최솟값으로 떨어진 뒤 회복하는 시나리오도 반드시 포함해야 한다.
  • bytes_in_flight == 0은 실제 애플리케이션 유휴 상태와 ACK 직후의 일시적인 상태를 구분하지 않으면 오판의 원인이 될 수 있다.
  • TCP 최적화 코드를 QUIC에 이식할 때는 전송 모델과 ACK 처리 방식의 차이를 검증해야 한다.
  • 이 사례는 상태 전환 횟수와 RTT 상관관계를 관찰하는 qlog 기반 계측이 혼잡 제어 버그를 찾는 데 효과적임을 보여준다.