实时通信 — WebSocket、gRPC 流式调用与 WebRTC
队头阻塞还留在哪里 — HTTP/3 与 QUIC
한국어 원문으로 표시합니다.
한 줄 요약
TCP 는 순서를 지키느라 잃은 세그먼트 하나 뒤의 바이트를 모두 붙잡고, QUIC 은 스트림을 전송 계층으로 내려 잃은 스트림만 기다리게 합니다.
왜 이게 필요했나
앞 모듈의 실습 마지막 단계에서 본 숫자를 떠올려 보세요. 하류로 20,000 바이트가 지난 뒤 800ms 멈추는 중계기를 두었더니, 한 연결에 실린 HTTP/2 의 작은 응답은 900ms 안팎에, 연결을 따로 쓴 HTTP/1.1 의 작은 응답은 수십 ms 에 끝났습니다. 중계기가 흉내 낸 것은 세그먼트 하나의 손실입니다. TCP 는 바이트 흐름을 순서대로만 애플리케이션에 넘겨주므로, 가운데 세그먼트가 재전송을 기다리는 동안 그 뒤에 이미 도착한 바이트도 커널 버퍼에 묶여 있습니다. HTTP/2 는 애플리케이션 수준의 머리 막힘을 없앴지만, 그 아래 TCP 수준의 머리 막힘은 오히려 모든 스트림이 함께 겪게 만들었습니다.
손실이 드문 데이터센터 안에서는 이 비용이 거의 보이지 않습니다. 손실이 잦은 모바일 망과 와이파이, 음성처럼 수십 ms 가 체감되는 서비스에서는 가장 늦은 1% 의 요청이 이 멈춤으로 채워집니다.
어떻게 동작하나
RFC 9000 의 QUIC 은 UDP 위에서 돌지만, TCP 가 하던 일 — 신뢰 전송, 혼잡 제어(RFC 9002), 흐름 제어 — 을 모두 다시 합니다. 달라진 점은 스트림이 전송 계층의 개념이라는 것입니다. 패킷 하나에는 여러 스트림의 프레임이 실릴 수 있지만, 순서 보장은 스트림 안에서만 합니다. 어떤 패킷이 사라지면 그 패킷에 데이터가 있던 스트림만 재전송을 기다리고, 나머지 스트림은 도착한 대로 애플리케이션에 넘겨집니다.
암호화도 바깥에 얹지 않고 안에 넣었습니다. RFC 9001 은 TLS 1.3 핸드셰이크를 QUIC 핸드셰이크에 녹여서, 새 연결은 1-RTT 에, 전에 만난 서버와는 0-RTT 에 데이터를 보낼 수 있게 합니다. 0-RTT 데이터는 재전송 공격에 노출되므로 멱등한 요청에만 써야 합니다. 연결을 IP 와 포트가 아니라 연결 ID 로 알아보기 때문에, 휴대폰이 와이파이에서 LTE 로 넘어가도 연결을 이어 갈 수 있습니다(연결 이전). 음성 통화처럼 길게 이어지는 세션에 반가운 성질입니다.
RFC 9114 의 HTTP/3 은 이 QUIC 위에 HTTP 의미를 올립니다. 헤더 압축도 바뀌었습니다. HPACK 은 헤더 블록이 보낸 순서대로 도착한다고 가정하고 동적 테이블을 고치므로, 스트림이 따로 도착하는 QUIC 에서는 쓸 수 없습니다. 그래서 RFC 9204 의 QPACK 은 테이블 갱신을 별도의 인코더·디코더 스트림으로 보내고, 아직 도착하지 않은 테이블 항목을 가리키느라 기다리는 요청 스트림을 몇 개까지 허용할지를 설정(SETTINGS_QPACK_BLOCKED_STREAMS)으로 합의합니다.
| 층 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 요청 동시성 | 연결 여러 개 | 한 연결의 스트림 | 한 연결의 QUIC 스트림 |
| 손실 하나의 영향 | 그 연결만 | 모든 스트림 | 그 스트림만 |
| 헤더 압축 | 없음 | HPACK | QPACK |
| 암호화 | 선택(TLS) | 사실상 TLS | 항상(TLS 1.3 내장) |
HTTP/3 을 쓸 수 있는지는 서버가 Alt-Svc 머리나 DNS 의 HTTPS 레코드로 알리고, 클라이언트는 그것을 보고 다음 요청부터 UDP 로 시도합니다.
흐름 제어도 두 겹 그대로입니다. QUIC 은 스트림마다, 그리고 연결 전체에 받을 수 있는 최대 바이트 수를 알리고(MAX_STREAM_DATA·MAX_DATA 프레임), 동시에 열 수 있는 스트림 수도 MAX_STREAMS 로 따로 알립니다. HTTP/2 에서 SETTINGS 와 WINDOW_UPDATE 가 하던 일이 전송 계층으로 내려간 것입니다. 그래서 앞 실습에서 본 "연결 창이 닫히면 작은 응답도 기다린다" 는 현상은 HTTP/3 에서도 똑같이 일어납니다. QUIC 이 없앤 것은 손실로 인한 머리 막힘이지, 받는 쪽이 느려서 생기는 막힘이 아닙니다.
현장에서 만나는 모습
HTTP/3 이 늘 이기지는 않습니다. UDP 443 을 막는 사내망과 공용 와이파이가 여전히 있어서 클라이언트는 TCP 로 되돌아갈 준비를 해야 하고, 사용자 공간에서 암호화와 혼잡 제어를 하는 만큼 같은 처리량에 CPU 를 더 씁니다. 로드밸런서와 방화벽이 QUIC 을 이해하지 못하면 연결 이전의 이점도 사라집니다. 그래서 대형 서비스는 두 경로를 모두 열어 두고 지표로 고릅니다.
이 코스의 실습 파드는 NET_ADMIN 권한이 없어 tc netem 으로 진짜 패킷 손실을 넣을 수 없습니다. 그래서 TCP 쪽은 사용자 공간 중계기로 손실의 결과(멈춤)를 흉내 냈고, QUIC 은 이 읽기로 다룹니다. 손실을 흉내 낼 때 무엇을 흉내 냈고 무엇을 흉내 내지 못했는지를 적어 두는 것도 측정의 일부입니다 — 중계기는 재전송 타이머와 혼잡 창 축소까지는 재현하지 않습니다.
다음 퀴즈에서 할 것
TCP 수준과 애플리케이션 수준의 머리 막힘을 구분하고, QUIC 이 어느 쪽을 어떻게 없앴는지, QPACK 이 왜 필요했는지를 확인합니다.