RFC 9002 QUIC 손실 탐지 2021년 5월
Iyengar & Swett 표준 트랙 [페이지]
스트림:
인터넷 엔지니어링 태스크 포스 (IETF)
RFC:
9002
카테고리:
표준 트랙
게시:
ISSN:
2070-1721
저자:
J. Iyengar, 편집자
Fastly
I. Swett, 편집자
Google

RFC 9002

QUIC 손실 탐지 및 혼잡 제어

초록

이 문서는 QUIC을 위한 손실 탐지 및 혼잡 제어 메커니즘을 설명한다.

이 메모의 상태

이는 인터넷 표준 트랙 문서이다.

이 문서는 인터넷 엔지니어링 태스크 포스 (IETF)의 산출물이다. 이는 IETF 커뮤니티의 합의를 나타낸다. 이 문서는 공개 검토를 받았으며 인터넷 엔지니어링 운영 그룹(IESG)에 의해 발행 승인을 받았다. 인터넷 표준에 대한 추가 정보는 RFC 7841의 섹션 2에서 확인할 수 있다.

이 문서의 현재 상태, 모든 정오표, 그리고 이에 대한 피드백 제공 방법에 관한 정보는 https://www.rfc-editor.org/info/rfc9002에서 얻을 수 있다.

목차

1. 소개

QUIC은 [QUIC-TRANSPORT]에 설명된 안전한 범용 전송 프로토콜이다. 이 문서는 QUIC을 위한 손실 탐지 및 혼잡 제어 메커니즘을 설명한다.

2. 규칙 및 정의

이 문서에서 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", 그리고 "OPTIONAL"이라는 핵심어는 여기에 표시된 것처럼 모두 대문자로 나타날 때, 그리고 오직 그럴 때에만 BCP 14 [RFC2119] [RFC8174]에 설명된 대로 해석되어야 한다.

이 문서에서 사용되는 용어의 정의:

ACK 유발 프레임:

ACK, PADDING, CONNECTION_CLOSE를 제외한 모든 프레임은 ACK 유발로 간주된다.

ACK 유발 패킷:

ACK 유발 프레임을 포함하는 패킷은 최대 확인 응답 지연 내에 수신자로부터 ACK를 유발하며 ACK 유발 패킷이라고 한다.

전송 중 패킷:

패킷은 ACK 유발이거나 PADDING 프레임을 포함하고, 전송되었지만 확인 응답되지 않았으며, 손실로 선언되지 않았고, 이전 키와 함께 폐기되지 않은 경우 전송 중으로 간주된다.

3. QUIC 전송 메커니즘의 설계

QUIC의 모든 전송은 패킷 수준 헤더와 함께 전송되며, 이 헤더는 암호화 수준을 나타내고 패킷 시퀀스 번호(아래에서는 패킷 번호라고 함)를 포함한다. 암호화 수준은 섹션 12.3 of [QUIC-TRANSPORT]에 설명된 패킷 번호 공간을 나타낸다. 패킷 번호는 연결의 수명 동안 하나의 패킷 번호 공간 내에서 반복되지 않는다. 패킷 번호는 한 공간 내에서 단조 증가하는 순서로 전송되어 모호성을 방지한다. 일부 패킷 번호가 전혀 사용되지 않아 의도적인 간격을 남기는 것은 허용된다.

이 설계는 전송과 재전송을 구분할 필요를 없애며, TCP 손실 탐지 메커니즘에 대한 QUIC의 해석에서 상당한 복잡성을 제거한다.

QUIC 패킷은 서로 다른 유형의 여러 프레임을 포함할 수 있다. 복구 메커니즘은 신뢰성 있는 전달이 필요한 데이터와 프레임이 필요에 따라 확인 응답되거나 손실로 선언되어 새 패킷으로 전송되도록 보장한다. 패킷에 포함된 프레임의 유형은 복구 및 혼잡 제어 로직에 영향을 준다:

4. QUIC와 TCP 간의 관련 차이점

TCP의 손실 탐지 및 혼잡 제어에 익숙한 독자는 여기의 알고리즘이 잘 알려진 TCP 알고리즘과 유사함을 알 수 있다. 그러나 QUIC와 TCP 간의 프로토콜 차이는 알고리즘 차이에 영향을 준다. 이러한 프로토콜 차이는 아래에 간략히 설명되어 있다.

4.1. 별도의 패킷 번호 공간

QUIC은 각 암호화 수준에 대해 별도의 패킷 번호 공간을 사용하지만, 0-RTT와 모든 세대의 1-RTT 키는 같은 패킷 번호 공간을 사용한다. 별도의 패킷 번호 공간은 한 암호화 수준으로 전송된 패킷의 확인 응답이 다른 암호화 수준으로 전송된 패킷의 불필요한 재전송을 유발하지 않도록 보장한다. 혼잡 제어와 왕복 시간(RTT) 측정은 패킷 번호 공간 전반에서 통합된다.

4.2. 단조 증가하는 패킷 번호

TCP는 송신자에서의 전송 순서와 수신자에서의 전달 순서를 혼동하여 재전송 모호성 문제 [RETRANSMISSION]를 초래한다. QUIC은 전송 순서와 전달 순서를 분리한다: 패킷 번호는 전송 순서를 나타내고, 전달 순서는 STREAM 프레임의 스트림 오프셋으로 결정된다.

QUIC의 패킷 번호는 패킷 번호 공간 내에서 엄격하게 증가하며 전송 순서를 직접 인코딩한다. 더 높은 패킷 번호는 해당 패킷이 나중에 전송되었음을 의미하고, 더 낮은 패킷 번호는 해당 패킷이 더 일찍 전송되었음을 의미한다. ACK 유발 프레임을 포함하는 패킷이 손실로 탐지되면, QUIC은 필요한 프레임을 새 패킷 번호를 가진 새 패킷에 포함하여, ACK가 수신될 때 어떤 패킷이 확인 응답되었는지에 대한 모호성을 제거한다. 그 결과 더 정확한 RTT 측정을 수행할 수 있고, 불필요한 재전송은 쉽게 탐지되며, Fast Retransmit과 같은 메커니즘은 패킷 번호만을 기반으로 보편적으로 적용될 수 있다.

이 설계 지점은 QUIC의 손실 탐지 메커니즘을 크게 단순화한다. 대부분의 TCP 메커니즘은 TCP 시퀀스 번호를 기반으로 전송 순서를 암시적으로 추론하려고 한다. 이는 특히 TCP 타임스탬프를 사용할 수 없을 때 사소하지 않은 작업이다.

4.3. 더 명확한 손실 에포크

QUIC은 패킷이 손실될 때 손실 에포크를 시작한다. 손실 에포크는 에포크 시작 후 전송된 임의의 패킷이 확인 응답될 때 끝난다. TCP는 시퀀스 번호 공간의 간격이 채워질 때까지 기다리므로, 세그먼트가 연속으로 여러 번 손실되면 손실 에포크가 여러 왕복 시간 동안 끝나지 않을 수 있다. 둘 다 혼잡 윈도우를 에포크당 한 번만 줄여야 하므로, QUIC은 손실을 겪는 각 왕복 시간마다 한 번 줄이는 반면, TCP는 여러 왕복 시간에 걸쳐 한 번만 줄일 수 있다.

4.4. 번복 없음

QUIC ACK 프레임은 TCP 선택적 확인 응답(SACK) [RFC2018]의 정보와 유사한 정보를 포함한다. 그러나 QUIC은 패킷 확인 응답이 번복되는 것을 허용하지 않아, 양측의 구현을 크게 단순화하고 송신자의 메모리 부담을 줄인다.

4.5. 더 많은 ACK 범위

QUIC은 TCP의 세 개 SACK 범위와 달리 많은 ACK 범위를 지원한다. 손실이 많은 환경에서 이는 복구 속도를 높이고, 불필요한 재전송을 줄이며, 타임아웃에 의존하지 않고 전진을 보장한다.

4.6. 지연된 확인 응답에 대한 명시적 보정

QUIC 엔드포인트는 패킷이 수신된 시점과 해당 확인 응답이 전송된 시점 사이에 발생한 지연을 측정하여, 피어가 더 정확한 RTT 추정치를 유지할 수 있게 한다. [QUIC-TRANSPORT]의 섹션 13.2를 참조한다.

4.7. 프로브 타임아웃이 RTO와 TLP를 대체함

QUIC은 TCP의 재전송 타임아웃(RTO) 계산에 기반한 타이머를 사용하는 프로브 타임아웃(PTO; 섹션 6.2 참조)을 사용한다. [RFC6298]를 참조한다. QUIC의 PTO는 고정된 최소 타임아웃을 사용하는 대신 피어의 최대 예상 확인 응답 지연을 포함한다.

TCP의 RACK-TLP 손실 탐지 알고리즘 [RFC8985]과 유사하게, QUIC은 PTO가 만료될 때 혼잡 윈도우를 축소하지 않는다. 꼬리의 단일 패킷 손실은 지속적 혼잡을 나타내지 않기 때문이다. 대신 QUIC은 지속적 혼잡이 선언될 때 혼잡 윈도우를 축소한다. 섹션 7.6을 참조한다. 이렇게 함으로써 QUIC은 불필요한 혼잡 윈도우 감소를 피하며, Forward RTO-Recovery(F-RTO) [RFC5682]와 같은 보정 메커니즘의 필요를 없앤다. QUIC은 PTO 만료 시 혼잡 윈도우를 축소하지 않으므로, QUIC 송신자는 사용 가능한 혼잡 윈도우가 남아 있다면 PTO 만료 후에도 더 많은 전송 중 패킷을 전송하는 데 제한받지 않는다. 이는 송신자가 애플리케이션 제한 상태이고 PTO 타이머가 만료될 때 발생한다. 이는 애플리케이션 제한 상태에서는 TCP의 RTO 메커니즘보다 더 공격적이지만, 애플리케이션 제한 상태가 아닐 때는 동일하다.

QUIC은 타이머가 만료될 때마다 프로브 패킷이 일시적으로 혼잡 윈도우를 초과하도록 허용한다.

4.8. 최소 혼잡 윈도우는 두 패킷

TCP는 한 패킷의 최소 혼잡 윈도우를 사용한다. 그러나 그 단일 패킷의 손실은 송신자가 복구를 위해 PTO(섹션 6.2)를 기다려야 함을 의미하며, 이는 RTT보다 훨씬 길 수 있다. 단일 ACK 유발 패킷을 전송하는 것도 수신자가 확인 응답을 지연할 때 추가 지연이 발생할 가능성을 높인다.

따라서 QUIC은 최소 혼잡 윈도우를 두 패킷으로 할 것을 권장한다. 이는 네트워크 부하를 증가시키지만, 송신자가 지속적 혼잡(섹션 6.2) 하에서 여전히 전송률을 지수적으로 줄이므로 안전한 것으로 간주된다.

4.9. 핸드셰이크 패킷은 특별하지 않음

TCP는 SYN 또는 SYN-ACK 패킷의 손실을 지속적 혼잡으로 처리하고 혼잡 윈도우를 한 패킷으로 줄인다. [RFC5681]을 참조한다. QUIC은 핸드셰이크 데이터를 포함하는 패킷의 손실을 다른 손실과 동일하게 처리한다.

5. 왕복 시간 추정

상위 수준에서, 엔드포인트는 패킷이 전송된 시점부터 확인 응답될 때까지의 시간을 RTT 샘플로 측정한다. 엔드포인트는 RTT 샘플과 피어가 보고한 호스트 지연([QUIC-TRANSPORT]의 섹션 13.2 참조)을 사용하여 네트워크 경로 RTT의 통계적 설명을 생성한다. 엔드포인트는 각 경로에 대해 다음 세 값을 계산한다: 일정 기간 동안의 최솟값 (min_rtt), 지수 가중 이동 평균(smoothed_rtt), 그리고 관측된 RTT 샘플의 평균 편차(이 문서의 나머지 부분에서는 "변동"이라고 함) (rttvar).

5.1. RTT 샘플 생성

엔드포인트는 다음 두 조건을 충족하는 ACK 프레임을 수신할 때 RTT 샘플을 생성한다:

  • 가장 큰 확인 응답된 패킷 번호가 새로 확인 응답되었고,
  • 새로 확인 응답된 패킷 중 적어도 하나가 ACK 유발이어야 한다.

RTT 샘플 latest_rtt는 가장 큰 확인 응답된 패킷이 전송된 이후 경과한 시간으로 생성된다:

latest_rtt = ack_time - send_time_of_largest_acked

RTT 샘플은 수신된 ACK 프레임에서 가장 큰 확인 응답된 패킷만을 사용하여 생성된다. 이는 피어가 ACK 프레임에서 가장 큰 확인 응답된 패킷에 대해서만 확인 응답 지연을 보고하기 때문이다. 보고된 확인 응답 지연은 RTT 샘플 측정에는 사용되지 않지만, smoothed_rtt와 rttvar의 후속 계산에서 RTT 샘플을 조정하는 데 사용된다(섹션 5.3).

단일 패킷에 대해 여러 RTT 샘플을 생성하지 않도록, ACK 프레임은 가장 큰 확인 응답된 패킷을 새로 확인 응답하지 않는 경우 RTT 추정치를 갱신하는 데 사용되어서는 안 된다(SHOULD NOT).

적어도 하나의 ACK 유발 패킷을 새로 확인 응답하지 않는 ACK 프레임을 수신할 때 RTT 샘플을 생성해서는 안 된다(MUST NOT). 피어는 일반적으로 ACK 유발이 아닌 패킷만 수신했을 때 ACK 프레임을 보내지 않는다. 따라서 ACK 유발이 아닌 패킷에 대한 확인 응답만 포함하는 ACK 프레임은 임의로 큰 ACK Delay 값을 포함할 수 있다. 이러한 ACK 프레임을 무시하면 후속 smoothed_rtt 및 rttvar 계산의 복잡성을 피할 수 있다.

송신자는 RTT 내에서 여러 ACK 프레임이 수신될 때 RTT당 여러 RTT 샘플을 생성할 수 있다. [RFC6298]에서 제안된 것처럼, 그렇게 하면 smoothed_rtt와 rttvar에 충분한 이력이 남지 않을 수 있다. RTT 추정치가 충분한 이력을 유지하도록 보장하는 것은 열린 연구 문제이다.

5.2. min_rtt 추정

min_rtt는 특정 네트워크 경로에 대해 일정 기간 동안 관측된 최소 RTT에 대한 송신자의 추정치이다. 이 문서에서 min_rtt는 손실 탐지가 믿기 어려울 정도로 작은 RTT 샘플을 거부하는 데 사용된다.

min_rtt는 첫 번째 RTT 샘플에서 latest_rtt로 설정되어야 한다(MUST). 다른 모든 샘플에서는 min_rtt와 latest_rtt(섹션 5.1) 중 더 작은 값으로 설정되어야 한다(MUST).

엔드포인트는 min_rtt를 계산할 때 로컬에서 관측한 시간만 사용하며 피어가 보고한 확인 응답 지연으로 조정하지 않는다. 이렇게 하면 엔드포인트가 전적으로 자신이 관측한 것을 기반으로 smoothed_rtt의 하한을 설정할 수 있고(섹션 5.3 참조), 피어가 잘못 보고한 지연으로 인한 잠재적 과소평가를 제한할 수 있다.

네트워크 경로의 RTT는 시간이 지남에 따라 변할 수 있다. 경로의 실제 RTT가 감소하면 min_rtt는 첫 번째 낮은 샘플에서 즉시 적응한다. 그러나 경로의 실제 RTT가 증가하면 min_rtt는 이에 적응하지 않아, 새 RTT보다 작은 향후 RTT 샘플이 smoothed_rtt에 포함될 수 있다.

엔드포인트는 지속적 혼잡이 확립된 후 min_rtt를 최신 RTT 샘플로 설정해야 한다(SHOULD). 이는 RTT가 증가할 때 지속적 혼잡을 반복적으로 선언하는 것을 방지한다. 또한 연결이 파괴적인 네트워크 이벤트 후 min_rtt 및 smoothed_rtt 추정치를 재설정할 수 있게 한다. 섹션 5.3을 참조한다.

엔드포인트는 트래픽 양이 적고 낮은 확인 응답 지연을 가진 확인 응답이 수신되는 경우처럼 연결의 다른 시점에 min_rtt를 다시 확립할 수 있다(MAY). 구현은 경로의 실제 최소 RTT가 자주 관측될 수 없으므로 min_rtt 값을 너무 자주 갱신해서는 안 된다(SHOULD NOT).

5.3. smoothed_rtt 및 rttvar 추정

smoothed_rtt는 엔드포인트 RTT 샘플의 지수 가중 이동 평균이며, rttvar는 평균 변동을 사용하여 RTT 샘플의 변동을 추정한다.

smoothed_rtt의 계산은 확인 응답 지연으로 조정한 뒤의 RTT 샘플을 사용한다. 이러한 지연은 [QUIC-TRANSPORT]의 섹션 19.3에 설명된 것처럼 ACK 프레임의 ACK Delay 필드에서 디코딩된다.

피어는 핸드셰이크 중 피어의 max_ack_delay보다 큰 확인 응답 지연을 보고할 수 있다([QUIC-TRANSPORT]의 섹션 13.2.1). 이를 고려하기 위해, 엔드포인트는 [QUIC-TLS]의 섹션 4.1.2에 정의된 것처럼 핸드셰이크가 확인될 때까지 max_ack_delay를 무시해야 한다(SHOULD). 이러한 큰 확인 응답 지연이 발생하면 반복되지 않고 핸드셰이크에 한정될 가능성이 높다. 따라서 엔드포인트는 이를 max_ack_delay로 제한하지 않고 사용할 수 있어, RTT 추정치의 불필요한 증가를 피할 수 있다.

큰 확인 응답 지연은 피어의 확인 응답 지연 보고 또는 엔드포인트의 min_rtt 추정치에 오류가 있는 경우 smoothed_rtt를 상당히 부풀릴 수 있음에 유의한다. 따라서 핸드셰이크 확인 전에, 엔드포인트는 확인 응답 지연으로 RTT 샘플을 조정한 결과가 min_rtt보다 작아지면 RTT 샘플을 무시할 수 있다(MAY).

핸드셰이크가 확인된 후, 피어가 보고한 확인 응답 지연이 피어의 max_ack_delay보다 큰 경우 이는 피어의 스케줄러 지연이나 이전 확인 응답 손실 같은 의도하지 않았지만 잠재적으로 반복될 수 있는 지연으로 간주된다. 초과 지연은 비준수 수신자로 인해 발생할 수도 있다. 따라서 이러한 추가 지연은 사실상 경로 지연의 일부로 간주되어 RTT 추정치에 포함된다.

따라서 피어가 보고한 확인 응답 지연을 사용하여 RTT 샘플을 조정할 때 엔드포인트는:

  • Initial 패킷에 대해서는 확인 응답 지연을 무시할 수 있다(MAY). 이러한 확인 응답은 피어에 의해 지연되지 않기 때문이다([QUIC-TRANSPORT]의 섹션 13.2.1);
  • 핸드셰이크가 확인될 때까지 피어의 max_ack_delay를 무시해야 한다(SHOULD);
  • 핸드셰이크가 확인된 후에는 확인 응답 지연과 피어의 max_ack_delay 중 더 작은 값을 사용해야 한다(MUST); 그리고
  • 결과 값이 min_rtt보다 작아지는 경우 RTT 샘플에서 확인 응답 지연을 빼서는 안 된다(MUST NOT). 이는 잘못 보고하는 피어로 인해 smoothed_rtt가 과소평가되는 것을 제한한다.

또한 엔드포인트는 해당 복호화 키를 즉시 사용할 수 없는 경우 확인 응답 처리를 지연할 수 있다. 예를 들어 클라이언트는 1-RTT 패킷 보호 키를 아직 사용할 수 없기 때문에 복호화할 수 없는 0-RTT 패킷에 대한 확인 응답을 받을 수 있다. 이러한 경우 엔드포인트는 핸드셰이크가 확인될 때까지 해당 로컬 지연을 RTT 샘플에서 빼야 한다(SHOULD).

[RFC6298]와 유사하게, smoothed_rtt와 rttvar는 다음과 같이 계산된다.

엔드포인트는 연결 설정 중 및 연결 마이그레이션 중 추정기가 재설정될 때 RTT 추정기를 초기화한다. [QUIC-TRANSPORT]의 섹션 9.4를 참조한다. 새 경로에 대해 RTT 샘플이 사용 가능해지기 전 또는 추정기가 재설정될 때, 추정기는 초기 RTT를 사용하여 초기화된다. 섹션 6.2.2를 참조한다.

smoothed_rtt와 rttvar는 다음과 같이 초기화되며, 여기서 kInitialRtt는 초기 RTT 값을 포함한다:

smoothed_rtt = kInitialRtt
rttvar = kInitialRtt / 2

네트워크 경로의 RTT 샘플은 latest_rtt에 기록된다. 섹션 5.1을 참조한다. 초기화 후 첫 번째 RTT 샘플에서 추정기는 해당 샘플을 사용하여 재설정된다. 이는 추정기가 과거 샘플의 이력을 유지하지 않도록 보장한다. 다른 경로에서 전송된 패킷은 [QUIC-TRANSPORT]의 섹션 9.4에 설명된 것처럼 현재 경로의 RTT 샘플에 기여하지 않는다.

초기화 후 첫 번째 RTT 샘플에서 smoothed_rtt와 rttvar는 다음과 같이 설정된다:

smoothed_rtt = latest_rtt
rttvar = latest_rtt / 2

후속 RTT 샘플에서 smoothed_rtt와 rttvar는 다음과 같이 변화한다:

ack_delay = decoded acknowledgment delay from ACK frame
if (handshake confirmed):
  ack_delay = min(ack_delay, max_ack_delay)
adjusted_rtt = latest_rtt
if (latest_rtt >= min_rtt + ack_delay):
  adjusted_rtt = latest_rtt - ack_delay
smoothed_rtt = 7/8 * smoothed_rtt + 1/8 * adjusted_rtt
rttvar_sample = abs(smoothed_rtt - adjusted_rtt)
rttvar = 3/4 * rttvar + 1/4 * rttvar_sample

6. 손실 탐지

QUIC 송신자는 확인 응답을 사용하여 손실된 패킷을 탐지하고, PTO를 사용하여 확인 응답이 수신되도록 보장한다. 섹션 6.2를 참조한다. 이 섹션은 이러한 알고리즘에 대한 설명을 제공한다.

패킷이 손실되면 QUIC 전송은 해당 손실에서 복구해야 한다. 예를 들어 데이터를 재전송하거나, 갱신된 프레임을 보내거나, 프레임을 폐기할 수 있다. 자세한 내용은 [QUIC-TRANSPORT]의 섹션 13.3을 참조한다.

손실 탐지는 RTT 측정 및 혼잡 제어와 달리 패킷 번호 공간별로 분리된다. RTT와 혼잡 제어는 경로의 속성인 반면, 손실 탐지는 키 가용성에도 의존하기 때문이다.

6.1. 확인 응답 기반 탐지

확인 응답 기반 손실 탐지는 TCP의 Fast Retransmit [RFC5681], Early Retransmit [RFC5827], Forward Acknowledgment [FACK], SACK 손실 복구 [RFC6675], 그리고 RACK-TLP [RFC8985]의 정신을 구현한다. 이 섹션은 이러한 알고리즘이 QUIC에서 어떻게 구현되는지에 대한 개요를 제공한다.

패킷은 다음 조건을 모두 충족하면 손실로 선언된다:

  • 패킷이 확인 응답되지 않았고, 전송 중이며, 확인 응답된 패킷보다 먼저 전송되었다.
  • 패킷이 확인 응답된 패킷보다 kPacketThreshold 패킷만큼 먼저 전송되었거나 (섹션 6.1.1), 충분히 오래전에 전송되었다 (섹션 6.1.2).

확인 응답은 나중에 전송된 패킷이 전달되었음을 나타내며, 패킷 및 시간 임계값은 패킷 재정렬에 대한 일정한 허용 범위를 제공한다.

패킷을 잘못 손실로 선언하면 불필요한 재전송이 발생하고, 손실 탐지 시 혼잡 제어기가 수행하는 동작으로 인해 성능이 저하될 수 있다. 구현은 잘못된 재전송을 탐지하고 패킷 또는 시간 재정렬 임계값을 증가시켜 향후 잘못된 재전송과 손실 이벤트를 줄일 수 있다. 적응형 시간 임계값을 가진 구현은 복구 지연을 최소화하기 위해 더 작은 초기 재정렬 임계값으로 시작하도록 선택할 수 있다(MAY).

6.1.1. 패킷 임계값

패킷 재정렬 임계값 (kPacketThreshold)의 권장(RECOMMENDED) 초기값은 TCP 손실 탐지의 모범 사례 [RFC5681] [RFC6675]에 기반하여 3이다. TCP와 유사성을 유지하기 위해 구현은 패킷 임계값으로 3보다 작은 값을 사용해서는 안 된다(SHOULD NOT). [RFC5681]을 참조한다.

일부 네트워크는 더 높은 수준의 패킷 재정렬을 보일 수 있어 송신자가 잘못된 손실을 탐지하게 할 수 있다. 또한 TCP 패킷을 관측하고 재정렬할 수 있는 네트워크 요소는 QUIC에 대해서는 그렇게 할 수 없고, QUIC 패킷 번호도 암호화되므로, 패킷 재정렬은 TCP보다 QUIC에서 더 흔할 수 있다. RACK [RFC8985]과 같이 잘못된 손실 탐지 후 재정렬 임계값을 증가시키는 알고리즘은 TCP에서 유용성이 입증되었으며 QUIC에서도 적어도 그만큼 유용할 것으로 예상된다.

6.1.2. 시간 임계값

같은 패킷 번호 공간 내의 더 나중 패킷이 확인 응답되면, 엔드포인트는 더 이른 패킷이 임계값만큼 과거에 전송된 경우 손실로 선언해야 한다(SHOULD). 패킷을 너무 일찍 손실로 선언하지 않도록, 이 시간 임계값은 kGranularity 상수가 나타내는 로컬 타이머 입도 이상으로 설정되어야 한다(MUST). 시간 임계값은 다음과 같다:

max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity)

가장 큰 확인 응답된 패킷보다 이전에 전송된 패킷을 아직 손실로 선언할 수 없다면, 남은 시간에 대해 타이머를 설정해야 한다(SHOULD).

max(smoothed_rtt, latest_rtt)를 사용하면 다음 두 경우로부터 보호된다:

  • latest RTT 샘플이 smoothed RTT보다 낮은 경우. 이는 확인 응답이 더 짧은 경로를 거친 재정렬 때문일 수 있다;
  • latest RTT 샘플이 smoothed RTT보다 높은 경우. 이는 실제 RTT가 지속적으로 증가했지만 smoothed RTT가 아직 따라잡지 못했기 때문일 수 있다.

RTT 배수로 표현되는 권장(RECOMMENDED) 시간 임계값 (kTimeThreshold)은 9/8이다. 타이머 입도 (kGranularity)의 권장(RECOMMENDED) 값은 1 밀리초이다.

구현은 절대 임계값, 이전 연결의 임계값, 적응형 임계값, 또는 RTT 변동의 포함을 실험할 수 있다(MAY). 더 작은 임계값은 재정렬 복원력을 줄이고 잘못된 재전송을 증가시키며, 더 큰 임계값은 손실 탐지 지연을 증가시킨다.

6.2. 프로브 타임아웃

프로브 타임아웃(PTO)은 ACK 유발 패킷이 예상 시간 내에 확인 응답되지 않거나 서버가 클라이언트 주소를 검증하지 않았을 수 있을 때 하나 또는 두 개의 프로브 데이터그램 전송을 트리거한다. PTO는 연결이 꼬리 패킷 또는 확인 응답의 손실에서 복구할 수 있게 한다.

손실 탐지와 마찬가지로 PTO는 패킷 번호 공간별이다. 즉, PTO 값은 패킷 번호 공간별로 계산된다.

PTO 타이머 만료 이벤트는 패킷 손실을 나타내지 않으며, 이전의 확인 응답되지 않은 패킷을 손실로 표시하게 해서는 안 된다(MUST NOT). 패킷을 새로 확인 응답하는 확인 응답이 수신되면, 손실 탐지는 패킷 및 시간 임계값 메커니즘이 지시하는 대로 진행된다. 섹션 6.1을 참조한다.

QUIC에서 사용되는 PTO 알고리즘은 TCP의 Tail Loss Probe [RFC8985], RTO [RFC5681], 그리고 F-RTO 알고리즘 [RFC5682]의 신뢰성 기능을 구현한다. 타임아웃 계산은 TCP의 RTO 기간 [RFC6298]에 기반한다.

6.2.1. PTO 계산

ACK 유발 패킷이 전송될 때, 송신자는 다음과 같이 PTO 기간에 대한 타이머를 예약한다:

PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay

PTO 기간은 송신자가 전송된 패킷의 확인 응답을 기다려야 하는 시간의 양이다. 이 시간 기간에는 추정된 네트워크 RTT(smoothed_rtt), 추정치의 변동(4*rttvar), 그리고 수신자가 확인 응답 전송을 지연할 수 있는 최대 시간을 고려하기 위한 max_ack_delay가 포함된다.

PTO가 Initial 또는 Handshake 패킷 번호 공간에 대해 설정될 때, PTO 기간 계산에서 max_ack_delay는 0으로 설정된다. 피어가 이러한 패킷을 의도적으로 지연하지 않을 것으로 예상되기 때문이다. [QUIC-TRANSPORT]의 섹션 13.2.1을 참조한다.

PTO 기간은 타이머가 즉시 만료되는 것을 피하기 위해 적어도 kGranularity 이상이어야 한다(MUST).

여러 패킷 번호 공간의 ACK 유발 패킷이 전송 중일 때, 타이머는 Initial 및 Handshake 패킷 번호 공간 중 더 이른 값으로 설정되어야 한다(MUST).

엔드포인트는 핸드셰이크가 확인될 때까지 Application Data 패킷 번호 공간에 대해 PTO 타이머를 설정해서는 안 된다(MUST NOT). 이렇게 하면 피어가 아직 해당 패킷을 처리할 키를 갖고 있지 않거나, 엔드포인트가 아직 해당 패킷의 확인 응답을 처리할 키를 갖고 있지 않을 때 엔드포인트가 패킷의 정보를 재전송하는 것을 방지한다. 예를 들어 클라이언트가 서버에 0-RTT 패킷을 보낼 때 이러한 일이 발생할 수 있다. 클라이언트는 서버가 이를 복호화할 수 있을지 알지 못한 채 전송한다. 마찬가지로 서버가 클라이언트가 서버 인증서를 검증했고 따라서 이러한 1-RTT 패킷을 읽을 수 있음을 확인하기 전에 1-RTT 패킷을 전송할 때 이러한 일이 발생할 수 있다.

송신자는 ACK 유발 패킷이 전송되거나 확인 응답될 때마다, 또는 Initial 또는 Handshake 키가 폐기될 때마다 PTO 타이머를 다시 시작해야 한다(SHOULD) ([QUIC-TLS]의 섹션 4.9). 이는 PTO가 항상 최신 RTT 추정치와 패킷 번호 공간 전반의 올바른 패킷을 기반으로 설정되도록 보장한다.

PTO 타이머가 만료되면 PTO 백오프가 증가해야 하며(MUST), 그 결과 PTO 기간은 현재 값의 두 배로 설정된다. PTO 백오프 계수는 확인 응답이 수신될 때 재설정되지만, 다음 경우는 예외이다. 서버는 핸드셰이크 중 패킷에 응답하는 데 평소보다 더 오래 걸릴 수 있다. 이러한 서버를 반복적인 클라이언트 프로브로부터 보호하기 위해, 클라이언트가 서버가 클라이언트 주소 검증을 끝냈다고 아직 확신하지 못하는 경우 PTO 백오프는 재설정되지 않는다. 즉, 클라이언트는 Initial 패킷의 확인 응답을 수신해도 PTO 백오프 계수를 재설정하지 않는다.

송신자 전송률의 이러한 지수적 감소는 중요하다. 연속적인 PTO는 심각한 혼잡으로 인한 패킷 또는 확인 응답 손실 때문에 발생할 수 있기 때문이다. 여러 패킷 번호 공간에 ACK 유발 패킷이 전송 중인 경우에도, 네트워크의 과도한 부하를 방지하기 위해 PTO의 지수적 증가가 모든 공간에 적용된다. 예를 들어 Initial 패킷 번호 공간의 타임아웃은 Handshake 패킷 번호 공간의 타임아웃 길이를 두 배로 만든다.

연속적인 PTO가 만료되는 전체 시간 길이는 idle timeout에 의해 제한된다.

시간 임계값 손실 탐지를 위해 타이머가 설정되어 있는 경우 PTO 타이머를 설정해서는 안 된다(MUST NOT). 섹션 6.1.2를 참조한다. 시간 임계값 손실 탐지를 위해 설정된 타이머는 대부분의 경우 PTO 타이머보다 더 일찍 만료되며, 데이터를 잘못 재전송할 가능성이 더 낮다.

6.2.2. 핸드셰이크와 새 경로

동일한 네트워크를 통한 재개된 연결은 이전 연결의 최종 smoothed RTT 값을 재개된 연결의 초기 RTT로 사용할 수 있다(MAY). 이전 RTT를 사용할 수 없는 경우, 초기 RTT는 333밀리초로 설정해야 한다(SHOULD). 이는 TCP의 초기 RTO에 권장된 것처럼 핸드셰이크가 1초의 PTO로 시작되게 한다. [RFC6298]의 섹션 2를 참조한다.

연결은 PATH_CHALLENGE 전송과 PATH_RESPONSE 수신 사이의 지연을 새 경로에 대한 초기 RTT(부록 A.2의 kInitialRtt 참조)를 설정하는 데 사용할 수 있지만(MAY), 그 지연은 RTT 샘플로 간주해서는 안 된다(SHOULD NOT).

Initial 키와 Handshake 키가 폐기되면(섹션 6.4 참조), 모든 Initial 패킷과 Handshake 패킷은 더 이상 확인 응답될 수 없으므로 전송 중 바이트에서 제거된다. Initial 또는 Handshake 키가 폐기될 때, PTO 및 손실 탐지 타이머는 재설정되어야 한다(MUST). 키 폐기는 전진을 나타내며, 손실 탐지 타이머가 이제 폐기된 패킷 번호 공간에 대해 설정되어 있었을 수 있기 때문이다.

6.2.2.1. 주소 검증 전

서버가 경로에서 클라이언트 주소를 검증할 때까지, 서버가 전송할 수 있는 데이터의 양은 수신한 데이터 양의 세 배로 제한된다. 이는 [QUIC-TRANSPORT]의 섹션 8.1에 지정되어 있다. 추가 데이터를 전송할 수 없는 경우, PTO에서 전송된 패킷은 증폭 방지 한도에 포함되므로 서버의 PTO 타이머는 클라이언트로부터 데이터그램이 수신될 때까지 설정되어서는 안 된다(MUST NOT).

서버가 클라이언트로부터 데이터그램을 수신하면, 증폭 한도가 증가하고 서버는 PTO 타이머를 재설정한다. 이때 PTO 타이머가 과거의 시간으로 설정되면 즉시 실행된다. 이렇게 하면 핸드셰이크 완료에 중요한 패킷보다 먼저 새 1-RTT 패킷을 전송하는 것을 피할 수 있다. 특히 0-RTT가 수락되었지만 서버가 클라이언트 주소 검증에 실패한 경우 이러한 일이 발생할 수 있다.

서버는 클라이언트로부터 더 많은 데이터그램을 수신할 때까지 차단될 수 있으므로, 서버가 주소 검증을 완료했다고 확신할 때까지 서버의 차단을 해제하기 위해 패킷을 보내는 것은 클라이언트의 책임이다 ([QUIC-TRANSPORT]의 섹션 8 참조). 즉, 클라이언트는 자신의 Handshake 패킷에 대한 확인 응답을 하나도 받지 못했고 핸드셰이크가 확인되지 않은 경우([QUIC-TLS]의 섹션 4.1.2 참조), 전송 중인 패킷이 없더라도 PTO 타이머를 설정해야 한다(MUST). PTO가 발생하면 클라이언트는 Handshake 키가 있는 경우 Handshake 패킷을 보내야 하며(MUST), 그렇지 않으면 적어도 1200바이트의 페이로드를 가진 UDP 데이터그램에 Initial 패킷을 보내야 한다(MUST).

6.2.3. 핸드셰이크 완료 가속

서버가 중복 CRYPTO 데이터를 포함하는 Initial 패킷을 수신하면, 클라이언트가 서버가 Initial 패킷으로 보낸 모든 CRYPTO 데이터를 받지 못했거나, 클라이언트의 추정 RTT가 너무 작다고 가정할 수 있다. 클라이언트가 Handshake 키를 얻기 전에 Handshake 또는 1-RTT 패킷을 수신하면, 서버의 Initial 패킷 중 일부 또는 전부가 손실되었다고 가정할 수 있다.

이러한 조건에서 핸드셰이크 완료를 가속하기 위해, 엔드포인트는 연결당 제한된 횟수 동안, [QUIC-TRANSPORT]의 섹션 8.1의 주소 검증 한도에 따르는 범위에서 PTO 만료보다 일찍 확인 응답되지 않은 CRYPTO 데이터를 포함하는 패킷을 보낼 수 있다(MAY). 각 연결에 대해 최대 한 번만 그렇게 해도 단일 패킷 손실에서 빠르게 복구하기에 충분하다. 처리할 수 없는 패킷 수신에 대한 응답으로 항상 패킷을 재전송하는 엔드포인트는 무한한 패킷 교환을 만들 위험이 있다.

엔드포인트는 또한 병합된 패킷([QUIC-TRANSPORT]의 섹션 12.2 참조)을 사용하여 각 데이터그램이 적어도 하나의 확인 응답을 유발하도록 보장할 수 있다. 예를 들어 클라이언트는 PING 및 PADDING 프레임을 포함하는 Initial 패킷을 0-RTT 데이터 패킷과 병합할 수 있고, 서버는 PING 프레임을 포함하는 Initial 패킷을 첫 번째 비행의 하나 이상의 패킷과 병합할 수 있다.

6.2.4. 프로브 패킷 전송

PTO 타이머가 만료되면, 송신자는 해당 패킷 번호 공간에서 적어도 하나의 ACK 유발 패킷을 프로브로 보내야 한다(MUST). 엔드포인트는 단일 손실 데이터그램으로 인한 비용이 큰 연속 PTO 만료를 피하거나 여러 패킷 번호 공간의 데이터를 전송하기 위해 ACK 유발 패킷을 포함하는 최대 두 개의 전체 크기 데이터그램을 보낼 수 있다(MAY). PTO에서 전송되는 모든 프로브 패킷은 ACK 유발이어야 한다(MUST).

타이머가 만료된 패킷 번호 공간에서 데이터를 보내는 것 외에도, 송신자는 전송 중인 데이터가 있는 다른 패킷 번호 공간에서 ACK 유발 패킷을 보내야 하며(SHOULD), 가능하면 패킷을 병합해야 한다. 이는 서버가 Initial 및 Handshake 데이터를 모두 전송 중이거나, 클라이언트가 Handshake 및 Application Data를 모두 전송 중인 경우 특히 유용하다. 피어가 두 패킷 번호 공간 중 하나에 대해서만 수신 키를 가지고 있을 수 있기 때문이다.

송신자가 PTO에서 더 빠른 확인 응답을 유발하려면, 확인 응답 지연을 제거하기 위해 패킷 번호 하나를 건너뛸 수 있다.

엔드포인트는 PTO 만료 시 전송되는 패킷에 새 데이터를 포함해야 한다(SHOULD). 새 데이터를 보낼 수 없는 경우 이전에 전송된 데이터를 보낼 수 있다(MAY). 구현은 애플리케이션의 우선순위에 따라 새 데이터 또는 재전송 데이터를 보내는 것을 포함하여, 프로브 패킷의 내용을 결정하기 위한 대체 전략을 사용할 수 있다(MAY).

송신자에게 보낼 새 데이터나 이전에 보낸 데이터가 없을 수 있다. 예를 들어 다음 이벤트 순서를 고려한다: 새 애플리케이션 데이터가 STREAM 프레임으로 전송되고, 손실된 것으로 간주된 뒤 새 패킷으로 재전송되며, 이후 원래 전송이 확인 응답된다. 보낼 데이터가 없을 때, 송신자는 단일 패킷에 PING 또는 다른 ACK 유발 프레임을 보내 PTO 타이머를 다시 설정해야 한다(SHOULD).

또는 ACK 유발 패킷을 보내는 대신, 송신자는 아직 전송 중인 모든 패킷을 손실로 표시할 수 있다(MAY). 이렇게 하면 추가 패킷 전송을 피할 수 있지만, 손실이 너무 공격적으로 선언되어 혼잡 제어기에 의한 불필요한 전송률 감소가 발생할 위험이 증가한다.

연속적인 PTO 기간은 지수적으로 증가하며, 그 결과 네트워크에서 패킷이 계속 삭제될수록 연결 복구 지연도 지수적으로 증가한다. PTO 만료 시 두 패킷을 보내면 패킷 삭제에 대한 복원력이 증가하여 연속적인 PTO 이벤트의 확률이 줄어든다.

PTO 타이머가 여러 번 만료되고 새 데이터를 보낼 수 없는 경우, 구현은 매번 같은 페이로드를 보낼지 다른 페이로드를 보낼지 선택해야 한다. 같은 페이로드를 보내는 것이 더 간단할 수 있으며 가장 높은 우선순위의 프레임이 먼저 도착하도록 보장한다. 매번 다른 페이로드를 보내면 잘못된 재전송 가능성이 줄어든다.

6.3. Retry 패킷 처리

Retry 패킷은 클라이언트가 다른 Initial 패킷을 보내게 하여 사실상 연결 프로세스를 다시 시작한다. Retry 패킷은 Initial 패킷이 수신되었지만 처리되지 않았음을 나타낸다. Retry 패킷은 패킷이 처리되었음을 나타내거나 패킷 번호를 지정하지 않으므로 확인 응답으로 처리될 수 없다.

Retry 패킷을 수신한 클라이언트는 대기 중인 모든 타이머를 재설정하는 것을 포함하여 혼잡 제어 및 손실 복구 상태를 재설정한다. 다른 연결 상태, 특히 암호화 핸드셰이크 메시지는 유지된다. [QUIC-TRANSPORT]의 섹션 17.2.5를 참조한다.

클라이언트는 첫 번째 Initial 패킷이 전송된 시점부터 Retry 또는 Version Negotiation 패킷이 수신될 때까지의 시간 기간을 서버에 대한 RTT 추정치로 계산할 수 있다(MAY). 클라이언트는 이 값을 초기 RTT 추정치의 기본값 대신 사용할 수 있다(MAY).

6.4. 키 및 패킷 상태 폐기

Initial 및 Handshake 패킷 보호 키가 폐기되면 ([QUIC-TLS]의 섹션 4.9 참조), 해당 키로 전송된 모든 패킷은 그 확인 응답을 처리할 수 없으므로 더 이상 확인 응답될 수 없다. 송신자는 해당 패킷과 관련된 모든 복구 상태를 폐기해야 하며(MUST), 전송 중 바이트 수에서 이를 제거해야 한다(MUST).

엔드포인트는 Handshake 패킷 교환을 시작하면 Initial 패킷의 송수신을 중지한다. [QUIC-TRANSPORT]의 섹션 17.2.2.1을 참조한다. 이 시점에서 전송 중인 모든 Initial 패킷의 복구 상태는 폐기된다.

0-RTT가 거부되면, 전송 중인 모든 0-RTT 패킷의 복구 상태가 폐기된다.

서버가 0-RTT를 수락하지만 Initial 패킷보다 먼저 도착한 0-RTT 패킷을 버퍼링하지 않는 경우, 초기 0-RTT 패킷은 손실로 선언되지만, 이는 드물 것으로 예상된다.

키는 해당 키로 암호화된 패킷이 확인 응답되었거나 손실로 선언된 후 어느 시점에 폐기될 것으로 예상된다. 그러나 Initial 및 Handshake 비밀값은 Handshake 및 1-RTT 키를 클라이언트와 서버 모두 사용할 수 있음이 입증되는 즉시 폐기된다. [QUIC-TLS]의 섹션 4.9.1을 참조한다.

7. 혼잡 제어

이 문서는 TCP NewReno [RFC6582]와 유사한 QUIC용 송신자 측 혼잡 제어기를 지정한다.

QUIC이 혼잡 제어를 위해 제공하는 신호는 일반적이며, 다양한 송신자 측 알고리즘을 지원하도록 설계되었다. 송신자는 CUBIC [RFC8312]과 같은 다른 알고리즘을 일방적으로 선택하여 사용할 수 있다.

송신자가 이 문서에 지정된 것과 다른 제어기를 사용하는 경우, 선택된 제어기는 섹션 3.1 of [RFC8085]에 지정된 혼잡 제어 지침을 준수해야 한다(MUST).

TCP와 유사하게, ACK 프레임만 포함하는 패킷은 전송 중 바이트에 포함되지 않으며 혼잡 제어를 받지 않는다. TCP와 달리, QUIC은 이러한 패킷의 손실을 탐지할 수 있으며, 이 정보를 사용하여 혼잡 제어기나 전송되는 ACK 전용 패킷의 전송률을 조정할 수 있다(MAY). 그러나 이 문서는 이를 수행하기 위한 메커니즘을 설명하지 않는다.

혼잡 제어기는 경로별로 존재하므로, 다른 경로에서 전송된 패킷은 섹션 9.4 of [QUIC-TRANSPORT]에 설명된 것처럼 현재 경로의 혼잡 제어기를 변경하지 않는다.

이 문서의 알고리즘은 제어기의 혼잡 윈도우를 바이트 단위로 지정하고 사용한다.

엔드포인트는 해당 패킷이 bytes_in_flight(부록 B.2 참조)를 혼잡 윈도우보다 크게 만들 경우 패킷을 전송해서는 안 된다(MUST NOT). 단, 패킷이 PTO 타이머 만료 시(섹션 6.2 참조) 전송되거나 복구에 진입할 때(섹션 7.3.2 참조) 전송되는 경우는 예외이다.

7.1. 명시적 혼잡 알림

경로가 명시적 혼잡 알림(ECN) [RFC3168] [RFC8311]을 지원하는 것으로 검증된 경우, QUIC은 IP 헤더의 Congestion Experienced (CE) 코드포인트를 혼잡의 신호로 처리한다. 이 문서는 피어가 보고한 ECN-CE 카운트가 증가할 때 엔드포인트의 응답을 지정한다. 섹션 13.4.2 of [QUIC-TRANSPORT]을 참조한다.

7.2. 초기 및 최소 혼잡 윈도우

QUIC은 모든 연결을 혼잡 윈도우가 초기값으로 설정된 느린 시작으로 시작한다. 엔드포인트는 최대 데이터그램 크기 (max_datagram_size)의 10배를 초기 혼잡 윈도우로 사용해야 하며(SHOULD), 동시에 윈도우를 14,720바이트 또는 최대 데이터그램 크기의 두 배 중 더 큰 값으로 제한해야 한다. 이는 [RFC6928]의 분석과 권장 사항을 따르며, TCP의 20바이트 오버헤드에 비해 UDP의 더 작은 8바이트 오버헤드를 고려하기 위해 바이트 한도를 증가시킨다.

연결 중 최대 데이터그램 크기가 변경되면, 초기 혼잡 윈도우는 새 크기로 다시 계산되어야 한다(SHOULD). 핸드셰이크를 완료하기 위해 최대 데이터그램 크기가 감소한 경우, 혼잡 윈도우는 새 초기 혼잡 윈도우로 설정되어야 한다(SHOULD).

클라이언트 주소를 검증하기 전에, 서버는 섹션 8.1 of [QUIC-TRANSPORT]에 지정된 증폭 방지 한도에 의해 추가로 제한될 수 있다. 증폭 방지 한도는 혼잡 윈도우가 완전히 활용되는 것을 막고 따라서 혼잡 윈도우의 증가를 늦출 수 있지만, 혼잡 윈도우에 직접 영향을 주지는 않는다.

최소 혼잡 윈도우는 손실, 피어가 보고한 ECN-CE 카운트의 증가, 또는 지속적 혼잡에 대한 응답으로 혼잡 윈도우가 도달할 수 있는 가장 작은 값이다. 권장(RECOMMENDED) 값은 2 * max_datagram_size이다.

7.3. 혼잡 제어 상태

이 문서에 설명된 NewReno 혼잡 제어기는 그림 1에 표시된 세 가지 뚜렷한 상태를 가진다.

                 New path or      +------------+
            persistent congestion |   Slow     |
        (O)---------------------->|   Start    |
                                  +------------+
                                        |
                                Loss or |
                        ECN-CE increase |
                                        v
 +------------+     Loss or       +------------+
 | Congestion |  ECN-CE increase  |  Recovery  |
 | Avoidance  |------------------>|   Period   |
 +------------+                   +------------+
           ^                            |
           |                            |
           +----------------------------+
              Acknowledgment of packet
                sent during recovery
그림 1: 혼잡 제어 상태와 전이

이러한 상태와 그 사이의 전이는 후속 섹션에서 설명한다.

7.3.1. 느린 시작

NewReno 송신자는 혼잡 윈도우가 느린 시작 임계값보다 낮은 모든 시점에 느린 시작 상태에 있다. 송신자는 느린 시작 임계값이 무한한 값으로 초기화되기 때문에 느린 시작에서 시작한다.

송신자가 느린 시작 상태에 있는 동안, 각 확인 응답이 처리될 때 혼잡 윈도우는 확인 응답된 바이트 수만큼 증가한다. 이는 혼잡 윈도우의 지수적 증가를 초래한다.

송신자는 패킷이 손실되거나 피어가 보고한 ECN-CE 카운트가 증가할 때 느린 시작을 종료하고 복구 기간에 들어가야 한다(MUST).

송신자는 혼잡 윈도우가 느린 시작 임계값보다 작을 때마다 느린 시작에 다시 진입한다. 이는 지속적 혼잡이 선언된 후에만 발생한다.

7.3.2. 복구

NewReno 송신자는 패킷 손실을 탐지하거나 피어가 보고한 ECN-CE 카운트가 증가할 때 복구 기간에 들어간다. 이미 복구 기간에 있는 송신자는 그 상태를 유지하며 다시 진입하지 않는다.

복구 기간에 들어갈 때, 송신자는 손실이 탐지된 시점의 혼잡 윈도우 값의 절반으로 느린 시작 임계값을 설정해야 한다(MUST). 혼잡 윈도우는 복구 기간을 종료하기 전에 느린 시작 임계값의 감소된 값으로 설정되어야 한다(MUST).

구현은 복구 기간에 들어가는 즉시 혼잡 윈도우를 줄이거나(MAY), Proportional Rate Reduction [PRR]과 같은 다른 메커니즘을 사용하여 혼잡 윈도우를 더 점진적으로 줄일 수 있다. 혼잡 윈도우가 즉시 줄어드는 경우, 감소 전에 단일 패킷을 전송할 수 있다. 이는 손실된 패킷의 데이터가 재전송되는 경우 손실 복구를 빠르게 하며, 섹션 5 of [RFC6675]에 설명된 TCP와 유사하다.

복구 기간의 목적은 왕복 시간당 한 번으로 혼잡 윈도우 감소를 제한하는 것이다. 따라서 복구 기간 동안에는 혼잡 윈도우가 새로운 손실이나 ECN-CE 카운트 증가에 응답하여 변경되지 않는다.

복구 기간 중 전송된 패킷이 확인 응답되면 복구 기간이 종료되고 송신자는 혼잡 회피 상태에 들어간다. 이는 복구를 시작한 손실 세그먼트가 확인 응답될 때 종료되는 TCP의 복구 정의와 약간 다르다 [RFC5681].

7.3.3. 혼잡 회피

NewReno 송신자는 혼잡 윈도우가 느린 시작 임계값 이상이고 복구 기간에 있지 않은 모든 시점에 혼잡 회피 상태에 있다.

혼잡 회피 상태의 송신자는 Additive Increase Multiplicative Decrease (AIMD) 접근 방식을 사용하며, 이는 확인 응답된 각 혼잡 윈도우마다 혼잡 윈도우 증가를 최대 하나의 최대 데이터그램 크기로 제한해야 한다(MUST).

송신자는 패킷이 손실되거나 피어가 보고한 ECN-CE 카운트가 증가할 때 혼잡 회피를 종료하고 복구 기간에 들어간다.

7.4. 복호화할 수 없는 패킷의 손실 무시

핸드셰이크 중에는 패킷이 도착할 때 일부 패킷 보호 키를 사용할 수 없을 수 있으며, 수신자는 해당 패킷을 버리도록 선택할 수 있다. 특히 Handshake 및 0-RTT 패킷은 Initial 패킷이 도착할 때까지 처리할 수 없으며, 1-RTT 패킷은 핸드셰이크가 완료될 때까지 처리할 수 없다. 엔드포인트는 피어가 해당 패킷을 처리할 패킷 보호 키를 갖기 전에 도착했을 수 있는 Handshake, 0-RTT, 1-RTT 패킷의 손실을 무시할 수 있다(MAY). 엔드포인트는 주어진 패킷 번호 공간에서 가장 이른 확인 응답된 패킷 이후에 전송된 패킷의 손실을 무시해서는 안 된다(MUST NOT).

7.5. 프로브 타임아웃

프로브 패킷은 혼잡 제어기에 의해 차단되어서는 안 된다(MUST NOT). 그러나 송신자는 이러한 패킷을 추가로 전송 중인 것으로 계산해야 한다(MUST). 이러한 패킷은 패킷 손실을 확립하지 않고 네트워크 부하를 추가하기 때문이다. 프로브 패킷 전송은 손실 또는 패킷 전달을 확립하는 확인 응답이 수신될 때까지 송신자의 전송 중 바이트가 혼잡 윈도우를 초과하게 할 수 있음에 유의한다.

7.6. 지속적 혼잡

송신자가 충분히 긴 기간 동안 전송된 모든 패킷의 손실을 확립하면, 네트워크는 지속적 혼잡을 겪고 있는 것으로 간주된다.

7.6.1. 지속 시간

지속적 혼잡 지속 시간은 다음과 같이 계산된다:

(smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay) *
    kPersistentCongestionThreshold

섹션 6.2의 PTO 계산과 달리, 이 지속 시간은 손실이 확립된 패킷 번호 공간과 관계없이 max_ack_delay를 포함한다.

이 지속 시간은 송신자가 지속적 혼잡을 확립하기 전에, PTO 만료에 대한 응답으로 전송되는 일부 패킷을 포함하여, Tail Loss Probes [RFC8985] 및 RTO [RFC5681]를 사용하는 TCP와 같은 수만큼 패킷을 보낼 수 있게 한다.

kPersistentCongestionThreshold 값이 클수록 송신자는 네트워크의 지속적 혼잡에 덜 민감해지며, 이는 혼잡한 네트워크로 공격적인 전송을 초래할 수 있다. 너무 작은 값은 송신자가 불필요하게 지속적 혼잡을 선언하게 하여 송신자의 처리량 감소를 초래할 수 있다.

kPersistentCongestionThreshold의 권장(RECOMMENDED) 값은 3이며, 이는 TCP 송신자가 두 번의 TLP 후 RTO를 선언하는 것과 거의 동등한 동작을 초래한다.

이 설계는 지속적 혼잡을 확립하기 위해 연속적인 PTO 이벤트를 사용하지 않는다. 애플리케이션 패턴이 PTO 만료에 영향을 주기 때문이다. 예를 들어, 그 사이에 무음 기간을 두고 소량의 데이터를 전송하는 송신자는 전송할 때마다 PTO 타이머를 다시 시작하여, 확인 응답이 수신되지 않는 경우에도 PTO 타이머가 오랫동안 만료되지 않게 할 수 있다. 지속 시간을 사용하면 송신자는 PTO 만료에 의존하지 않고 지속적 혼잡을 확립할 수 있다.

7.6.2. 지속적 혼잡 확립

송신자는 확인 응답을 수신한 후, ACK 유발 패킷 두 개가 손실로 선언되고 다음 조건을 만족하면 지속적 혼잡을 확립한다:

  • 모든 패킷 번호 공간에 걸쳐, 이 두 패킷의 전송 시간 사이에 전송된 패킷 중 어느 것도 확인 응답되지 않았다;
  • 이 두 패킷의 전송 시간 사이의 지속 시간이 지속적 혼잡 지속 시간(섹션 7.6.1)을 초과한다; 그리고
  • 이 두 패킷이 전송될 때 이전 RTT 샘플이 존재했다.

이 두 패킷은 ACK 유발이어야 한다(MUST). 수신자는 최대 확인 응답 지연 내에 ACK 유발 패킷만 확인 응답하도록 요구되기 때문이다. 섹션 13.2 of [QUIC-TRANSPORT]를 참조한다.

지속적 혼잡 기간은 적어도 하나의 RTT 샘플이 있을 때까지 시작되어서는 안 된다(SHOULD NOT). 첫 번째 RTT 샘플 전에, 송신자는 초기 RTT(섹션 6.2.2)를 기반으로 PTO 타이머를 설정하며, 이는 실제 RTT보다 상당히 클 수 있다. 이전 RTT 샘플을 요구하면 송신자가 잠재적으로 너무 적은 프로브로 지속적 혼잡을 확립하는 것을 방지한다.

네트워크 혼잡은 패킷 번호 공간의 영향을 받지 않으므로, 지속적 혼잡은 패킷 번호 공간 전반에서 전송된 패킷을 고려해야 한다(SHOULD). 모든 패킷 번호 공간에 대한 상태가 없는 송신자 또는 패킷 번호 공간 전반의 전송 시간을 비교할 수 없는 구현은 확인 응답된 패킷 번호 공간의 상태만 사용할 수 있다(MAY). 이는 지속적 혼잡을 잘못 선언하게 할 수 있지만, 지속적 혼잡 탐지 실패로 이어지지는 않는다.

지속적 혼잡이 선언되면, 송신자의 혼잡 윈도우는 TCP 송신자가 RTO에 응답하는 것과 유사하게 최소 혼잡 윈도우(kMinimumWindow)로 줄어들어야 한다(MUST) [RFC5681].

7.6.3.

다음 예는 송신자가 지속적 혼잡을 어떻게 확립할 수 있는지 보여준다. 다음을 가정한다:

smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay = 2
kPersistentCongestionThreshold = 3

다음 이벤트 순서를 고려한다:

표 1
시간 동작
t=0 패킷 #1 전송(애플리케이션 데이터)
t=1 패킷 #2 전송(애플리케이션 데이터)
t=1.2 #1의 확인 응답 수신
t=2 패킷 #3 전송(애플리케이션 데이터)
t=3 패킷 #4 전송(애플리케이션 데이터)
t=4 패킷 #5 전송(애플리케이션 데이터)
t=5 패킷 #6 전송(애플리케이션 데이터)
t=6 패킷 #7 전송(애플리케이션 데이터)
t=8 패킷 #8 전송(PTO 1)
t=12 패킷 #9 전송(PTO 2)
t=12.2 #9의 확인 응답 수신

패킷 9에 대한 확인 응답이 t = 12.2에 수신되면 패킷 2부터 8까지가 손실로 선언된다.

혼잡 기간은 가장 오래된 손실 패킷과 가장 새로운 손실 패킷 사이의 시간으로 계산된다: 8 - 1 = 7. 지속적 혼잡 지속 시간은 2 * 3 = 6이다. 임계값에 도달했고 가장 오래된 손실 패킷과 가장 새로운 손실 패킷 사이의 패킷 중 어느 것도 확인 응답되지 않았으므로, 네트워크는 지속적 혼잡을 겪은 것으로 간주된다.

이 예는 PTO 만료를 보여주지만, 지속적 혼잡을 확립하는 데 PTO 만료가 필요한 것은 아니다.

7.7. 페이싱

송신자는 혼잡 제어기의 입력을 기반으로 모든 전송 중 패킷의 전송 속도를 조절해야 한다(SHOULD).

여러 패킷을 그 사이에 지연 없이 네트워크로 전송하면 단기적인 혼잡과 손실을 유발할 수 있는 패킷 버스트가 생성된다. 송신자는 페이싱을 사용하거나 이러한 버스트를 제한해야 한다(MUST). 송신자는 버스트를 초기 혼잡 윈도우로 제한해야 한다(SHOULD). 섹션 7.2를 참조한다. 수신자까지의 네트워크 경로가 더 큰 버스트를 흡수할 수 있음을 아는 송신자는 더 높은 한도를 사용할 수 있다(MAY).

구현은 혼잡 제어기가 페이서와 잘 작동하도록 설계하는 데 주의를 기울여야 한다. 예를 들어, 페이서는 혼잡 제어기를 감싸고 혼잡 윈도우의 가용성을 제어할 수도 있고, 혼잡 제어기가 전달한 패킷을 일정한 속도로 내보낼 수도 있다.

ACK 프레임의 적시 전달은 효율적인 손실 복구에 중요하다. 피어로의 전달이 지연되는 것을 피하기 위해, ACK 프레임만 포함하는 패킷은 따라서 페이싱되어서는 안 된다(SHOULD).

엔드포인트는 원하는 방식으로 페이싱을 구현할 수 있다. 완벽하게 페이싱된 송신자는 패킷을 시간에 따라 정확히 고르게 분산한다. 이 문서의 것과 같은 윈도우 기반 혼잡 제어기의 경우, 해당 전송률은 RTT 동안 혼잡 윈도우의 평균을 구해 계산할 수 있다. congestion_window가 바이트 단위일 때, 시간당 바이트 단위의 전송률로 표현하면:

rate = N * congestion_window / smoothed_rtt

또는 시간 단위의 패킷 간 간격으로 표현하면:

interval = ( smoothed_rtt * packet_size / congestion_window ) / N

N에 작지만 적어도 1인 값(예: 1.25)을 사용하면 RTT 변동이 혼잡 윈도우의 과소 활용을 초래하지 않도록 보장한다.

패킷화, 스케줄링 지연, 계산 효율성과 같은 실제 고려 사항은 송신자가 RTT보다 훨씬 짧은 시간 구간 동안 이 전송률에서 벗어나게 할 수 있다.

페이싱을 위한 한 가지 가능한 구현 전략은 leaky bucket 알고리즘을 사용한다. 여기서 "bucket"의 용량은 최대 버스트 크기로 제한되고, "bucket"이 채워지는 전송률은 위 함수로 결정된다.

7.8. 혼잡 윈도우 과소 활용

전송 중 바이트가 혼잡 윈도우보다 작고 전송이 페이싱에 의해 제한되지 않는 경우, 혼잡 윈도우는 과소 활용된다. 이는 애플리케이션 데이터가 부족하거나 흐름 제어 한도 때문에 발생할 수 있다. 이러한 경우, 혼잡 윈도우는 느린 시작이나 혼잡 회피 중 어느 경우에도 증가되어서는 안 된다(SHOULD NOT).

패킷을 페이싱하는 송신자(섹션 7.7 참조)는 이 지연 때문에 패킷 전송이 지연되고 혼잡 윈도우를 완전히 활용하지 못할 수 있다. 송신자는 페이싱 지연이 없었다면 혼잡 윈도우를 완전히 활용했을 경우 자신을 애플리케이션 제한 상태로 간주해서는 안 된다(SHOULD NOT).

송신자는 과소 활용 기간 후 혼잡 윈도우를 갱신하기 위한 대체 메커니즘을 구현할 수 있다(MAY). 예를 들어 [RFC7661]에서 TCP에 대해 제안된 메커니즘이 있다.

8. 보안 고려 사항

8.1. 손실 및 혼잡 신호

손실 탐지와 혼잡 제어는 본질적으로 지연, 손실, ECN 마킹과 같은 인증되지 않은 엔터티로부터의 신호 소비를 포함한다. 공격자는 패킷을 삭제하거나, 경로 지연을 전략적으로 변경하거나, ECN 코드포인트를 변경함으로써 이러한 신호를 조작하여 엔드포인트가 전송률을 낮추게 할 수 있다.

8.2. 트래픽 분석

ACK 프레임만 운반하는 패킷은 패킷 크기를 관찰하여 휴리스틱하게 식별될 수 있다. 확인 응답 패턴은 링크 특성이나 애플리케이션 동작에 대한 정보를 노출할 수 있다. 누출되는 정보를 줄이기 위해, 엔드포인트는 확인 응답을 다른 프레임과 묶거나, 성능에 대한 잠재적 비용을 감수하고 PADDING 프레임을 사용할 수 있다.

8.3. ECN 마킹 오보고

수신자는 송신자의 혼잡 응답을 변경하기 위해 ECN 마킹을 잘못 보고할 수 있다. ECN-CE 마킹 보고를 억제하면 송신자가 전송률을 증가시킬 수 있다. 이러한 증가는 혼잡과 손실을 초래할 수 있다.

송신자는 자신이 전송하는 가끔의 패킷에 ECN-CE 마킹을 하여 보고 억제를 탐지할 수 있다. ECN-CE 마킹으로 전송된 패킷이 확인 응답될 때 CE 마킹된 것으로 보고되지 않으면, 송신자는 해당 경로에서 이후 전송되는 패킷에 ECN-Capable Transport (ECT) 코드포인트를 설정하지 않음으로써 해당 경로의 ECN을 비활성화할 수 있다 [RFC3168].

추가 ECN-CE 마킹을 보고하면 송신자는 전송률을 낮추게 되며, 이는 축소된 연결 흐름 제어 한도를 광고하는 것과 효과가 유사하므로 그렇게 해서 얻는 이점은 없다.

엔드포인트는 사용할 혼잡 제어기를 선택한다. 혼잡 제어기는 ECN-CE 보고에 응답하여 전송률을 낮추지만, 그 응답은 달라질 수 있다. 마킹은 손실과 동등하게 처리될 수 있지만 [RFC3168], [RFC8511] 또는 [RFC8311]과 같은 다른 응답도 지정될 수 있다.

9. 참고 문헌

9.1. 규범적 참고 문헌

[QUIC-TLS]
Thomson, M., Ed. and S. Turner, Ed., "TLS를 사용하여 QUIC 보안 확보", RFC 9001, DOI 10.17487/RFC9001, , <https://www.rfc-editor.org/info/rfc9001>.
[QUIC-TRANSPORT]
Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: UDP 기반 다중화 및 보안 전송", RFC 9000, DOI 10.17487/RFC9000, , <https://www.rfc-editor.org/info/rfc9000>.
[RFC2119]
Bradner, S., "RFC에서 요구 수준을 나타내는 데 사용하는 핵심어", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC3168]
Ramakrishnan, K., Floyd, S., and D. Black, "IP에 명시적 혼잡 알림(ECN) 추가", RFC 3168, DOI 10.17487/RFC3168, , <https://www.rfc-editor.org/info/rfc3168>.
[RFC8085]
Eggert, L., Fairhurst, G., and G. Shepherd, "UDP 사용 지침", BCP 145, RFC 8085, DOI 10.17487/RFC8085, , <https://www.rfc-editor.org/info/rfc8085>.
[RFC8174]
Leiba, B., "RFC 2119 핵심어의 대문자와 소문자 모호성", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.

9.2. 정보성 참고 문헌

[FACK]
Mathis, M. and J. Mahdavi, "전방 확인 응답: TCP 혼잡 제어 개선", ACM SIGCOMM Computer Communication Review, DOI 10.1145/248157.248181, , <https://doi.org/10.1145/248157.248181>.
[PRR]
Mathis, M., Dukkipati, N., and Y. Cheng, "TCP를 위한 Proportional Rate Reduction", RFC 6937, DOI 10.17487/RFC6937, , <https://www.rfc-editor.org/info/rfc6937>.
[RETRANSMISSION]
Karn, P. and C. Partridge, "신뢰성 있는 전송 프로토콜에서 왕복 시간 추정 개선", ACM Transactions on Computer Systems, DOI 10.1145/118544.118549, , <https://doi.org/10.1145/118544.118549>.
[RFC2018]
Mathis, M., Mahdavi, J., Floyd, S., and A. Romanow, "TCP 선택적 확인 응답 옵션", RFC 2018, DOI 10.17487/RFC2018, , <https://www.rfc-editor.org/info/rfc2018>.
[RFC3465]
Allman, M., "적절한 바이트 계산(ABC)을 사용한 TCP 혼잡 제어", RFC 3465, DOI 10.17487/RFC3465, , <https://www.rfc-editor.org/info/rfc3465>.
[RFC5681]
Allman, M., Paxson, V., and E. Blanton, "TCP 혼잡 제어", RFC 5681, DOI 10.17487/RFC5681, , <https://www.rfc-editor.org/info/rfc5681>.
[RFC5682]
Sarolahti, P., Kojo, M., Yamamoto, K., and M. Hata, "Forward RTO-Recovery (F-RTO): TCP에서 잘못된 재전송 타임아웃을 탐지하기 위한 알고리즘", RFC 5682, DOI 10.17487/RFC5682, , <https://www.rfc-editor.org/info/rfc5682>.
[RFC5827]
Allman, M., Avrachenkov, K., Ayesta, U., Blanton, J., and P. Hurtig, "TCP 및 Stream Control Transmission Protocol (SCTP)을 위한 Early Retransmit", RFC 5827, DOI 10.17487/RFC5827, , <https://www.rfc-editor.org/info/rfc5827>.
[RFC6298]
Paxson, V., Allman, M., Chu, J., and M. Sargent, "TCP의 재전송 타이머 계산", RFC 6298, DOI 10.17487/RFC6298, , <https://www.rfc-editor.org/info/rfc6298>.
[RFC6582]
Henderson, T., Floyd, S., Gurtov, A., and Y. Nishida, "TCP 빠른 복구 알고리즘에 대한 NewReno 수정", RFC 6582, DOI 10.17487/RFC6582, , <https://www.rfc-editor.org/info/rfc6582>.
[RFC6675]
Blanton, E., Allman, M., Wang, L., Jarvinen, I., Kojo, M., and Y. Nishida, "TCP를 위한 선택적 확인 응답(SACK) 기반 보수적 손실 복구 알고리즘", RFC 6675, DOI 10.17487/RFC6675, , <https://www.rfc-editor.org/info/rfc6675>.
[RFC6928]
Chu, J., Dukkipati, N., Cheng, Y., and M. Mathis, "TCP의 초기 윈도우 증가", RFC 6928, DOI 10.17487/RFC6928, , <https://www.rfc-editor.org/info/rfc6928>.
[RFC7661]
Fairhurst, G., Sathiaseelan, A., and R. Secchi, "전송률 제한 트래픽을 지원하도록 TCP 갱신", RFC 7661, DOI 10.17487/RFC7661, , <https://www.rfc-editor.org/info/rfc7661>.
[RFC8311]
Black, D., "명시적 혼잡 알림(ECN) 실험에 대한 제한 완화", RFC 8311, DOI 10.17487/RFC8311, , <https://www.rfc-editor.org/info/rfc8311>.
[RFC8312]
Rhee, I., Xu, L., Ha, S., Zimmermann, A., Eggert, L., and R. Scheffenegger, "빠른 장거리 네트워크를 위한 CUBIC", RFC 8312, DOI 10.17487/RFC8312, , <https://www.rfc-editor.org/info/rfc8312>.
[RFC8511]
Khademi, N., Welzl, M., Armitage, G., and G. Fairhurst, "ECN을 사용한 TCP 대체 백오프(ABE)", RFC 8511, DOI 10.17487/RFC8511, , <https://www.rfc-editor.org/info/rfc8511>.
[RFC8985]
Cheng, Y., Cardwell, N., Dukkipati, N., and P. Jha, "TCP용 RACK-TLP 손실 탐지 알고리즘", RFC 8985, DOI 10.17487/RFC8985, , <https://www.rfc-editor.org/info/rfc8985>.

부록 A. 손실 복구 의사코드

이제 섹션 6에 설명된 손실 탐지 메커니즘의 예시 구현을 설명한다.

이 섹션의 의사코드 부분은 코드 구성 요소로 라이선스가 부여된다. 저작권 고지를 참조한다.

A.1. 전송된 패킷 추적

혼잡 제어를 올바르게 구현하기 위해, QUIC 송신자는 모든 ack-eliciting 패킷을 해당 패킷이 확인 응답되거나 손실될 때까지 추적한다. 구현은 패킷 번호 및 암호화 컨텍스트별로 이 정보에 접근할 수 있어야 하며, 손실 복구와 혼잡 제어를 위해 패킷별 필드 (부록 A.1.1)를 저장할 수 있을 것으로 예상된다.

패킷이 손실로 선언된 후에도, 엔드포인트는 패킷 재정렬을 허용하기 위해 일정 시간 동안 그 상태를 유지할 수 있다. [QUIC-TRANSPORT]의 섹션 13.3을 참조한다. 이를 통해 송신자는 잘못된 재전송을 탐지할 수 있다.

전송된 패킷은 각 패킷 번호 공간별로 추적되며, ACK 처리는 단일 공간에만 적용된다.

A.1.1. 전송된 패킷 필드

packet_number:

전송된 패킷의 패킷 번호.

ack_eliciting:

패킷이 ack-eliciting인지 여부를 나타내는 Boolean. true이면 확인 응답이 수신될 것으로 예상되지만, 피어는 이를 포함하는 ACK 프레임의 전송을 최대 max_ack_delay까지 지연할 수 있다.

in_flight:

패킷이 전송 중 바이트에 포함되는지 여부를 나타내는 Boolean.

sent_bytes:

패킷에서 전송된 바이트 수. UDP 또는 IP 오버헤드는 포함하지 않지만 QUIC 프레이밍 오버헤드는 포함한다.

time_sent:

패킷이 전송된 시간.

A.2. 관심 상수

손실 복구에 사용되는 상수는 RFC, 논문, 그리고 일반적인 관행의 조합에 기반한다.

kPacketThreshold:

패킷 임계값 손실 탐지가 패킷을 손실로 간주하기 전의 최대 패킷 단위 재정렬. 섹션 6.1.1에서 권장하는 값은 3이다.

kTimeThreshold:

시간 임계값 손실 탐지가 패킷을 손실로 간주하기 전의 최대 시간 단위 재정렬. RTT 배수로 지정된다. 섹션 6.1.2에서 권장하는 값은 9/8이다.

kGranularity:

타이머 입도. 이는 시스템 의존적인 값이며, 섹션 6.1.2는 1 ms 값을 권장한다.

kInitialRtt:

RTT 샘플을 얻기 전에 사용되는 RTT. 섹션 6.2.2에서 권장하는 값은 333 ms이다.

kPacketNumberSpace:

세 개의 패킷 번호 공간을 열거하는 enum:

enum kPacketNumberSpace {
  Initial,
  Handshake,
  ApplicationData,
}

A.3. 관심 변수

혼잡 제어 메커니즘을 구현하는 데 필요한 변수는 이 섹션에 설명되어 있다.

latest_rtt:

이전에 확인 응답되지 않은 패킷에 대한 확인 응답을 수신할 때 수행된 가장 최근의 RTT 측정값.

smoothed_rtt:

섹션 5.3에 설명된 대로 계산된 연결의 smoothed RTT.

rttvar:

섹션 5.3에 설명된 대로 계산된 RTT 변동.

min_rtt:

섹션 5.2에 설명된 대로, 확인 응답 지연을 무시하고 일정 기간 동안 관측된 최소 RTT.

first_rtt_sample:

첫 번째 RTT 샘플을 얻은 시간.

max_ack_delay:

동명의 전송 매개변수([QUIC-TRANSPORT]의 섹션 18.2)에 정의된 것처럼, 수신자가 Application Data 패킷 번호 공간의 패킷에 대한 확인 응답을 지연하려는 최대 시간. 수신된 ACK 프레임의 실제 ack_delay는 늦은 타이머, 재정렬 또는 손실로 인해 더 클 수 있음에 유의한다.

loss_detection_timer:

손실 탐지에 사용되는 다중 모드 타이머.

pto_count:

확인 응답을 수신하지 않고 PTO가 전송된 횟수.

time_of_last_ack_eliciting_packet[kPacketNumberSpace]:

가장 최근의 ack-eliciting 패킷이 전송된 시간.

largest_acked_packet[kPacketNumberSpace]:

지금까지 해당 패킷 번호 공간에서 확인 응답된 가장 큰 패킷 번호.

loss_time[kPacketNumberSpace]:

해당 패킷 번호 공간의 다음 패킷이 시간상 재정렬 윈도우를 초과하여 손실로 간주될 수 있는 시간.

sent_packets[kPacketNumberSpace]:

패킷 번호 공간의 패킷 번호를 그에 대한 정보와 연결한 것. 위의 부록 A.1에서 자세히 설명되어 있다.

A.4. 초기화

연결 시작 시, 손실 탐지 변수를 다음과 같이 초기화한다:

loss_detection_timer.reset()
pto_count = 0
latest_rtt = 0
smoothed_rtt = kInitialRtt
rttvar = kInitialRtt / 2
min_rtt = 0
first_rtt_sample = 0
for pn_space in [ Initial, Handshake, ApplicationData ]:
  largest_acked_packet[pn_space] = infinite
  time_of_last_ack_eliciting_packet[pn_space] = 0
  loss_time[pn_space] = 0

A.5. 패킷 전송 시

패킷이 전송된 후, 패킷에 대한 정보가 저장된다. OnPacketSent에 대한 매개변수는 위의 부록 A.1.1에서 자세히 설명되어 있다.

OnPacketSent의 의사코드는 다음과 같다:

OnPacketSent(packet_number, pn_space, ack_eliciting,
             in_flight, sent_bytes):
  sent_packets[pn_space][packet_number].packet_number =
                                           packet_number
  sent_packets[pn_space][packet_number].time_sent = now()
  sent_packets[pn_space][packet_number].ack_eliciting =
                                           ack_eliciting
  sent_packets[pn_space][packet_number].in_flight = in_flight
  sent_packets[pn_space][packet_number].sent_bytes = sent_bytes
  if (in_flight):
    if (ack_eliciting):
      time_of_last_ack_eliciting_packet[pn_space] = now()
    OnPacketSentCC(sent_bytes)
    SetLossDetectionTimer()

A.6. 데이터그램 수신 시

서버가 증폭 방지 한도에 의해 차단된 경우, 데이터그램을 수신하면 해당 데이터그램의 패킷 중 어떤 것도 성공적으로 처리되지 않더라도 차단이 해제된다. 이러한 경우 PTO 타이머를 다시 설정해야 한다.

OnDatagramReceived의 의사코드는 다음과 같다:

OnDatagramReceived(datagram):
  // If this datagram unblocks the server, arm the
  // PTO timer to avoid deadlock.
  if (server was at anti-amplification limit):
    SetLossDetectionTimer()
    if loss_detection_timer.timeout < now():
      // Execute PTO if it would have expired
      // while the amplification limit applied.
      OnLossDetectionTimeout()

A.7. 확인 응답 수신 시

ACK 프레임이 수신되면, 임의 개수의 패킷을 새로 확인 응답할 수 있다.

OnAckReceived 및 UpdateRtt의 의사코드는 다음과 같다:

IncludesAckEliciting(packets):
  for packet in packets:
    if (packet.ack_eliciting):
      return true
  return false

OnAckReceived(ack, pn_space):
  if (largest_acked_packet[pn_space] == infinite):
    largest_acked_packet[pn_space] = ack.largest_acked
  else:
    largest_acked_packet[pn_space] =
        max(largest_acked_packet[pn_space], ack.largest_acked)

  // DetectAndRemoveAckedPackets finds packets that are newly
  // acknowledged and removes them from sent_packets.
  newly_acked_packets =
      DetectAndRemoveAckedPackets(ack, pn_space)
  // Nothing to do if there are no newly acked packets.
  if (newly_acked_packets.empty()):
    return

  // Update the RTT if the largest acknowledged is newly acked
  // and at least one ack-eliciting was newly acked.
  if (newly_acked_packets.largest().packet_number ==
          ack.largest_acked &&
      IncludesAckEliciting(newly_acked_packets)):
    latest_rtt =
      now() - newly_acked_packets.largest().time_sent
    UpdateRtt(ack.ack_delay)

  // Process ECN information if present.
  if (ACK frame contains ECN information):
      ProcessECN(ack, pn_space)

  lost_packets = DetectAndRemoveLostPackets(pn_space)
  if (!lost_packets.empty()):
    OnPacketsLost(lost_packets)
  OnPacketsAcked(newly_acked_packets)

  // Reset pto_count unless the client is unsure if
  // the server has validated the client's address.
  if (PeerCompletedAddressValidation()):
    pto_count = 0
  SetLossDetectionTimer()


UpdateRtt(ack_delay):
  if (first_rtt_sample == 0):
    min_rtt = latest_rtt
    smoothed_rtt = latest_rtt
    rttvar = latest_rtt / 2
    first_rtt_sample = now()
    return

  // min_rtt ignores acknowledgment delay.
  min_rtt = min(min_rtt, latest_rtt)
  // Limit ack_delay by max_ack_delay after handshake
  // confirmation.
  if (handshake confirmed):
    ack_delay = min(ack_delay, max_ack_delay)

  // Adjust for acknowledgment delay if plausible.
  adjusted_rtt = latest_rtt
  if (latest_rtt >= min_rtt + ack_delay):
    adjusted_rtt = latest_rtt - ack_delay

  rttvar = 3/4 * rttvar + 1/4 * abs(smoothed_rtt - adjusted_rtt)
  smoothed_rtt = 7/8 * smoothed_rtt + 1/8 * adjusted_rtt

A.8. 손실 탐지 타이머 설정

QUIC 손실 탐지는 모든 타임아웃 손실 탐지를 위해 단일 타이머를 사용한다. 타이머의 지속 시간은 아래의 패킷 및 타이머 이벤트에서 설정되는 타이머 모드에 기반한다. 아래에 정의된 SetLossDetectionTimer 함수는 단일 타이머가 설정되는 방식을 보여준다.

이 알고리즘은 특히 타이머가 늦게 깨어나는 경우 타이머가 과거로 설정되는 결과를 낳을 수 있다. 과거로 설정된 타이머는 즉시 실행된다.

SetLossDetectionTimer의 의사코드는 다음과 같다("^" 연산자는 지수 연산을 나타낸다):

GetLossTimeAndSpace():
  time = loss_time[Initial]
  space = Initial
  for pn_space in [ Handshake, ApplicationData ]:
    if (time == 0 || loss_time[pn_space] < time):
      time = loss_time[pn_space];
      space = pn_space
  return time, space

GetPtoTimeAndSpace():
  duration = (smoothed_rtt + max(4 * rttvar, kGranularity))
      * (2 ^ pto_count)
  // Anti-deadlock PTO starts from the current time
  if (no ack-eliciting packets in flight):
    assert(!PeerCompletedAddressValidation())
    if (has handshake keys):
      return (now() + duration), Handshake
    else:
      return (now() + duration), Initial
  pto_timeout = infinite
  pto_space = Initial
  for space in [ Initial, Handshake, ApplicationData ]:
    if (no ack-eliciting packets in flight in space):
        continue;
    if (space == ApplicationData):
      // Skip Application Data until handshake confirmed.
      if (handshake is not confirmed):
        return pto_timeout, pto_space
      // Include max_ack_delay and backoff for Application Data.
      duration += max_ack_delay * (2 ^ pto_count)

    t = time_of_last_ack_eliciting_packet[space] + duration
    if (t < pto_timeout):
      pto_timeout = t
      pto_space = space
  return pto_timeout, pto_space

PeerCompletedAddressValidation():
  // Assume clients validate the server's address implicitly.
  if (endpoint is server):
    return true
  // Servers complete address validation when a
  // protected packet is received.
  return has received Handshake ACK ||
       handshake confirmed

SetLossDetectionTimer():
  earliest_loss_time, _ = GetLossTimeAndSpace()
  if (earliest_loss_time != 0):
    // Time threshold loss detection.
    loss_detection_timer.update(earliest_loss_time)
    return

  if (server is at anti-amplification limit):
    // The server's timer is not set if nothing can be sent.
    loss_detection_timer.cancel()
    return

  if (no ack-eliciting packets in flight &&
      PeerCompletedAddressValidation()):
    // There is nothing to detect lost, so no timer is set.
    // However, the client needs to arm the timer if the
    // server might be blocked by the anti-amplification limit.
    loss_detection_timer.cancel()
    return

  timeout, _ = GetPtoTimeAndSpace()
  loss_detection_timer.update(timeout)

A.9. 타임아웃 시

손실 탐지 타이머가 만료되면, 타이머의 모드가 수행할 동작을 결정한다.

OnLossDetectionTimeout의 의사코드는 다음과 같다:

OnLossDetectionTimeout():
  earliest_loss_time, pn_space = GetLossTimeAndSpace()
  if (earliest_loss_time != 0):
    // Time threshold loss Detection
    lost_packets = DetectAndRemoveLostPackets(pn_space)
    assert(!lost_packets.empty())
    OnPacketsLost(lost_packets)
    SetLossDetectionTimer()
    return

  if (no ack-eliciting packets in flight):
    assert(!PeerCompletedAddressValidation())
    // Client sends an anti-deadlock packet: Initial is padded
    // to earn more anti-amplification credit,
    // a Handshake packet proves address ownership.
    if (has Handshake keys):
      SendOneAckElicitingHandshakePacket()
    else:
      SendOneAckElicitingPaddedInitialPacket()
  else:
    // PTO. Send new data if available, else retransmit old data.
    // If neither is available, send a single PING frame.
    _, pn_space = GetPtoTimeAndSpace()
    SendOneOrTwoAckElicitingPackets(pn_space)

  pto_count++
  SetLossDetectionTimer()

A.10. 손실된 패킷 탐지

DetectAndRemoveLostPackets는 ACK가 수신되거나 시간 임계값 손실 탐지 타이머가 만료될 때마다 호출된다. 이 함수는 해당 패킷 번호 공간의 sent_packets에 대해 동작하며 새로 손실로 탐지된 패킷 목록을 반환한다.

DetectAndRemoveLostPackets의 의사코드는 다음과 같다:

DetectAndRemoveLostPackets(pn_space):
  assert(largest_acked_packet[pn_space] != infinite)
  loss_time[pn_space] = 0
  lost_packets = []
  loss_delay = kTimeThreshold * max(latest_rtt, smoothed_rtt)

  // Minimum time of kGranularity before packets are deemed lost.
  loss_delay = max(loss_delay, kGranularity)

  // Packets sent before this time are deemed lost.
  lost_send_time = now() - loss_delay

  foreach unacked in sent_packets[pn_space]:
    if (unacked.packet_number > largest_acked_packet[pn_space]):
      continue

    // Mark packet as lost, or set time when it should be marked.
    // Note: The use of kPacketThreshold here assumes that there
    // were no sender-induced gaps in the packet number space.
    if (unacked.time_sent <= lost_send_time ||
        largest_acked_packet[pn_space] >=
          unacked.packet_number + kPacketThreshold):
      sent_packets[pn_space].remove(unacked.packet_number)
      lost_packets.insert(unacked)
    else:
      if (loss_time[pn_space] == 0):
        loss_time[pn_space] = unacked.time_sent + loss_delay
      else:
        loss_time[pn_space] = min(loss_time[pn_space],
                                  unacked.time_sent + loss_delay)
  return lost_packets

A.11. Initial 또는 Handshake 키를 폐기할 때

Initial 또는 Handshake 키가 폐기되면, 해당 공간의 패킷이 폐기되고 손실 탐지 상태가 갱신된다.

OnPacketNumberSpaceDiscarded의 의사코드는 다음과 같다:

OnPacketNumberSpaceDiscarded(pn_space):
  assert(pn_space != ApplicationData)
  RemoveFromBytesInFlight(sent_packets[pn_space])
  sent_packets[pn_space].clear()
  // Reset the loss detection and PTO timer
  time_of_last_ack_eliciting_packet[pn_space] = 0
  loss_time[pn_space] = 0
  pto_count = 0
  SetLossDetectionTimer()

부록 B. 혼잡 제어 의사코드

이제 섹션 7에 설명된 혼잡 제어기의 예시 구현을 설명한다.

이 섹션의 의사코드 부분은 코드 구성 요소로 라이선스가 부여된다. 저작권 고지를 참조한다.

B.1. 관심 상수

혼잡 제어에 사용되는 상수는 RFC, 논문, 그리고 일반적인 관행의 조합에 기반한다.

kInitialWindow:

섹션 7.2에 설명된 전송 중 초기 바이트의 기본 한도.

kMinimumWindow:

섹션 7.2에 설명된 바이트 단위의 최소 혼잡 윈도우.

kLossReductionFactor:

새로운 손실 이벤트가 탐지될 때 혼잡 윈도우를 줄이기 위해 적용되는 스케일링 계수. 섹션 7은 0.5 값을 권장한다.

kPersistentCongestionThreshold:

지속적 혼잡이 확립되기 위한 기간으로, PTO 배수로 지정된다. 섹션 7.6은 3 값을 권장한다.

B.2. 관심 변수

혼잡 제어 메커니즘을 구현하는 데 필요한 변수는 이 섹션에 설명되어 있다.

max_datagram_size:

송신자의 현재 최대 페이로드 크기. 이는 UDP 또는 IP 오버헤드를 포함하지 않는다. 최대 데이터그램 크기는 혼잡 윈도우 계산에 사용된다. 엔드포인트는 Path Maximum Transmission Unit (PMTU; [QUIC-TRANSPORT]의 섹션 14.2 참조)에 기반해 이 변수의 값을 설정하며, 최소값은 1200바이트이다.

ecn_ce_counters[kPacketNumberSpace]:

ACK 프레임에서 피어가 해당 패킷 번호 공간에 대해 보고한 ECN-CE 카운터의 가장 높은 값. 이 값은 보고된 ECN-CE 카운터의 증가를 탐지하는 데 사용된다.

bytes_in_flight:

적어도 하나의 ack-eliciting 또는 PADDING 프레임을 포함하고, 아직 확인 응답되지 않았거나 손실로 선언되지 않은 모든 전송된 패킷 크기의 바이트 합계. 크기에는 IP 또는 UDP 오버헤드는 포함되지 않지만, QUIC 헤더와 Authenticated Encryption with Associated Data (AEAD) 오버헤드는 포함된다. ACK 프레임만 포함하는 패킷은 혼잡 제어가 혼잡 피드백을 방해하지 않도록 bytes_in_flight에 포함되지 않는다.

congestion_window:

전송 중으로 허용되는 최대 바이트 수.

congestion_recovery_start_time:

손실 또는 ECN 탐지로 인해 현재 복구 기간이 시작된 시간. 이 시간 이후에 전송된 패킷이 확인 응답되면, QUIC은 혼잡 복구를 종료한다.

ssthresh:

바이트 단위의 느린 시작 임계값. 혼잡 윈도우가 ssthresh보다 낮으면 모드는 느린 시작이고, 윈도우는 확인 응답된 바이트 수만큼 증가한다.

혼잡 제어 의사코드는 손실 복구 의사코드의 변수 중 일부에도 접근한다.

B.3. 초기화

연결 시작 시, 혼잡 제어 변수를 다음과 같이 초기화한다:

congestion_window = kInitialWindow
bytes_in_flight = 0
congestion_recovery_start_time = 0
ssthresh = infinite
for pn_space in [ Initial, Handshake, ApplicationData ]:
  ecn_ce_counters[pn_space] = 0

B.4. 패킷 전송 시

패킷이 전송되고 non-ACK 프레임을 포함할 때마다, 해당 패킷은 bytes_in_flight를 증가시킨다.

OnPacketSentCC(sent_bytes):
  bytes_in_flight += sent_bytes

B.5. 패킷 확인 응답 시

이는 손실 탐지의 OnAckReceived에서 호출되며, sent_packets에서 새로 acked_packets를 제공받는다.

혼잡 회피에서 congestion_window에 정수 표현을 사용하는 구현자는 나눗셈에 주의해야 하며 [RFC3465]의 섹션 2.1에 제안된 대체 접근 방식을 사용할 수 있다.

InCongestionRecovery(sent_time):
  return sent_time <= congestion_recovery_start_time

OnPacketsAcked(acked_packets):
  for acked_packet in acked_packets:
    OnPacketAcked(acked_packet)

OnPacketAcked(acked_packet):
  if (!acked_packet.in_flight):
    return;
  // Remove from bytes_in_flight.
  bytes_in_flight -= acked_packet.sent_bytes
  // Do not increase congestion_window if application
  // limited or flow control limited.
  if (IsAppOrFlowControlLimited())
    return
  // Do not increase congestion window in recovery period.
  if (InCongestionRecovery(acked_packet.time_sent)):
    return
  if (congestion_window < ssthresh):
    // Slow start.
    congestion_window += acked_packet.sent_bytes
  else:
    // Congestion avoidance.
    congestion_window +=
      max_datagram_size * acked_packet.sent_bytes
      / congestion_window

B.6. 새 혼잡 이벤트 시

이는 새 혼잡 이벤트가 탐지될 때 ProcessECN 및 OnPacketsLost에서 호출된다. 이미 복구 중이 아니라면, 이는 복구 기간을 시작하고 느린 시작 임계값과 혼잡 윈도우를 즉시 줄인다.

OnCongestionEvent(sent_time):
  // No reaction if already in a recovery period.
  if (InCongestionRecovery(sent_time)):
    return

  // Enter recovery period.
  congestion_recovery_start_time = now()
  ssthresh = congestion_window * kLossReductionFactor
  congestion_window = max(ssthresh, kMinimumWindow)
  // A packet can be sent to speed up loss recovery.
  MaybeSendOnePacket()

B.7. ECN 정보 처리

이는 ECN 섹션이 있는 ACK 프레임이 피어로부터 수신될 때 호출된다.

ProcessECN(ack, pn_space):
  // If the ECN-CE counter reported by the peer has increased,
  // this could be a new congestion event.
  if (ack.ce_counter > ecn_ce_counters[pn_space]):
    ecn_ce_counters[pn_space] = ack.ce_counter
    sent_time = sent_packets[ack.largest_acked].time_sent
    OnCongestionEvent(sent_time)

B.8. 패킷 손실 시

이는 DetectAndRemoveLostPackets가 패킷을 손실로 판단할 때 호출된다.

OnPacketsLost(lost_packets):
  sent_time_of_last_loss = 0
  // Remove lost packets from bytes_in_flight.
  for lost_packet in lost_packets:
    if lost_packet.in_flight:
      bytes_in_flight -= lost_packet.sent_bytes
      sent_time_of_last_loss =
        max(sent_time_of_last_loss, lost_packet.time_sent)
  // Congestion event if in-flight packets were lost
  if (sent_time_of_last_loss != 0):
    OnCongestionEvent(sent_time_of_last_loss)

  // Reset the congestion window if the loss of these
  // packets indicates persistent congestion.
  // Only consider packets sent after getting an RTT sample.
  if (first_rtt_sample == 0):
    return
  pc_lost = []
  for lost in lost_packets:
    if lost.time_sent > first_rtt_sample:
      pc_lost.insert(lost)
  if (InPersistentCongestion(pc_lost)):
    congestion_window = kMinimumWindow
    congestion_recovery_start_time = 0

B.9. 폐기된 패킷을 전송 중 바이트에서 제거

Initial 또는 Handshake 키가 폐기되면, 해당 공간에서 전송된 패킷은 더 이상 전송 중 바이트에 포함되지 않는다.

RemoveFromBytesInFlight의 의사코드는 다음과 같다:

RemoveFromBytesInFlight(discarded_packets):
  // Remove any unacknowledged packets from flight.
  foreach packet in discarded_packets:
    if packet.in_flight
      bytes_in_flight -= size

기여자

IETF QUIC 워킹 그룹은 많은 사람들로부터 엄청난 지원을 받았다. 다음 사람들은 이 문서에 실질적인 기여를 제공했다:

저자 주소

Jana Iyengar (편집자)
Fastly
Ian Swett (편집자)
Google