HTTP/3에 대하여

HTTP/3에 대하여

HTTP/2가 나온 지 얼마나 됐다고 벌써 HTTP/3 도입 사례가 들리기 시작할까? 네이버는 검색 서비스에, 토스는 페이먼츠에 HTTP/3을 도입했다고 한다. HTTP/2는 2015년 5월에 릴리즈됐다고 한다. 약 7년 전이고 생각보다는 오래됐지만 HTTP/1.1이 버텨온 기간보다는 짧다. HTTP/3은 무엇을 해결하려고 했고, 어떻게 해결했을지 정리했다.

HTTP/2의 문제

이전에 gRPC를 설명하면서 간단하게 HTTP/2에 관해 설명한 적이 있다. HTTP/2는 과거보다 나아진 현대 네트워크 환경에 맞는 프로토콜이라고 볼 수 있다. 하나의 TCP 물리적인 컨넥션을 사용해 논리적인 스트림을 사용하는 구조이다. 이를 통해 시스템의 물리적인 메모리 사용이라든지 리소스를 훨씬 덜 사용하면서도 HTTP/1의 결점을 수정할 수 있었다. 오늘날 HTTP/2는 인터넷 통신의 약 40%를 차지한다.
조금 줄어드는 건 왜일까
TCP는 신뢰성 있는 데이터 전송을 위해 Handshake 과정이 포함되어있다. 일반적으로 TLS/TCP 스택을 생각해보면 애플리케이션 레이어가 데이터를 주고받기까지 최소 두 번(TCP 연결 & TLS 연결)의 라운드 트립 딜레이가 발생한다. CPU, 네트워크는 점점 빨라지지만 빛의 속도는 변하지 않기 때문에 절대적으로 걸리는 시간을 줄이는 데 한계가 있다.

이러한 TCP의 구조적 문제 말고, TCP 위에서 스트림을 이용해 멀티플렉싱하고 있는 HTTP/2의 문제가 있는데, TCP의 선형적인 데이터 전송 특성으로 인해, 임의의 스트림이 느려지는 것이 다른 스트림에게 영향을 주게 된다는 점이다. 즉, HOL(Head Of Line) Blocking 문제가 발생한다. 만약 패킷 손실률이 2%라고 했을 때 HTTP/2를 사용하는 것 보다 여러 TCP 컨넥션을 사용하는 HTTP/1.1이 더 나은 성능을 보여준다고 한다.

패킷을 100개 보냈을 때 2개가 손실되는 것은 정말 열악한 환경이다. 서버 사이의 통신에서는 이런 상황이 자주 나오지 않을 수 있다. 하지만 클라이언트와 통신하는 환경은 워낙 다양하고 네트워크 품질이 안 좋은 상황이 자주 있다.

HTTP/3

HTTP/3을 단순하게 말하자면 HTTP Over QUIC라고 볼 수 있다. QUIC는 UDP 위에서 구현된 전송 레이어이다. 그래서 이제부터는 HTTP/3의 본체인 QUIC에 대해 얘기할 예정이다.
QUIC & TLS/TCP

위는 논문의 공식적인 그림이다. QUIC는 일종의 전송 프로토콜이다. 첫 번째로 HTTP 메시지를 전달하는 역할을 하는 것이다. 그래서 초기 HTTP/3은 HTTP Over QUIC라고 불렸다. 다른 프로토콜을 QUIC 위에 전송하도록 하는 작업은 공식 버전 1.0 출시 이후 연기되었다고 하는데, 그로부터 3년이 지났으므로 어떤 발전이 있을 수도 있다.

왜 QUIC이어야 했을까?

왜 QUIC라고 하는 전송 프로토콜을 UDP 위에 만들었어야 했을까?

왜 새로운 전송 레이어를 만들지 않았나

왜 TCP, UDP, QUIC같이 전송 계층에 새로운 레이어를 만들지 않았을까? 이는 사실 간단한데, 배포가 굉장히 어렵다. 전송 레이어의 프로토콜이 추가되는 것은 커널의 업데이트가 요구되기도 하고, 모든 중간 미들박스에서 이를 통과시켜줘야 한다. 그냥 단순히 통과시킬 수도 있지만, 보안 측면으로 익숙하지 않은 패킷의 형태가 통과되지 않을 가능성도 있다.

왜 TCP 업데이트 하지 않았나

TCP 동작 방식을 변경하는 것은 사실 위와 유사한 이유로 인해 문제가 발생한다. TCP는 커널에 의해 구현되어 있으므로 OS 업데이트가 요구된다. 따라서 QUIC가 배포되기 위해 네트워크 경로의 미들박스들이 모두 커널을 업데이트해야 하는 문제가 있다. 또는 미들 박스들이 기존 TCP 스택과 강하게 바인딩 된 어떤 소프트웨어로 동작하고 있는 경우 문제를 발생시킬 여지가 있다.

왜 UDP인가

위에서 언급한 것처럼 TCP를 사용하면서, TCP의 문제점을 수정한 업데이트 버전을 사용하지 않는다면 비효율적인 Handshake라든지, HOL Blocking 등 근본적인 문제와 또다시 직면하게 된다. UDP는 네트워크 프로토콜이라고 불릴 만큼 네트워크 레이어로부터 받은 데이터에 단순히 포트와 IP 정보만 추가된 정말 얇은 전송 계층의 프로토콜이다. 이 위에 신뢰성을 보장해줄 수 있는 QUIC를 만들었다.
QUIC 로고
이러한 이유로 QUIC를 바닥부터 설계하게 되었는데, QUIC의 설계는 다음과 같은 목표를 달성하기 위해 디자인되었다.

  • Deployability: 기존 시스템이 온전히 인식할 수 있어야 한다.
  • Security: 기존 TLS 암호화 과정처럼 데이터를 안전히 전달해야 한다.
  • Reduction in Handshake: 기존 TLS/TCP 스택이 갖고 있던 Handshake 비효율 문제를 해결해야 한다.
  • HOL Blocking: 멀티플렉싱 되는 데이터가 HOL Blocking 문제를 맞이하면 안 된다.

위 목적을 달성하면서도 TCP처럼 신뢰성 있는 전달, 흐름 제어 및 혼잡 제어 등을 처리해야 하는 목적도 있다. 이제 QUIC가 어떻게 동작하는지 하나씩 살펴보자.

Connection

QUIC 역시 신뢰성 있는 통신을 위한 Handshake 과정이 있지만, TCP와 다르게 암호화와 통신을 위한 Handshake 과정이 통합되어있다. 먼저 논문에 소개된 그림은 다음과 같다. 하나씩 천천히 확인해보자.
QUIC 컨넥션을 간단하게 그린 것

Initial Handshake

맨 처음 클라이언트는 서버에 대한 어떤 정보도 없기 때문에 서버의 정보를 얻어 오기 위한 가장 처음 요청이 필요하다. 이 Client Hello 단계를 Inchoate CHLO라고 한다. 논문을 봤을 때는 아마 이 메시지에는 QUIC 버전 협상과 관련된 정보가 있는 것으로 보인다. 버전 협상에 대한 내용은 이후 잠깐 후술한다.

REJ 메시지는 “reject”라는 뜻으로, 클라이언트의 메시지가 응답을 보내기에 부적합한 경우 서버가 보내는 메시지이다. 이 메시지에는 다음과 같은 내용이 포함되어 있다.

  • 서버의 설정 (서버의 장기(Long-term) Diffie-Hellman(이하 DH) 공개값 등)
  • 서버 인증서와 시그니처
  • 클라이언트의 공개 IP 주소와 서버의 타임스탬프가 포함된 인증 암호 블록

인증서와 시그니처는 HTTPS 연결 과정 처음처럼 해당 서버가 인증된 서버인지 확인할 수 있게 해주고, 인증 암호 블록은 이후 클라이언트가 IP 주소의 오너십을 증명하기 위해 다시 서버에게 보낸다.

DH는 두 개의 키를 가지고 하나의 시크릿 값을 추출할 수 있는 알고리즘이다.
Diffie-Hellman
Inchoate CHLO 과정 이후 REJ 메시지를 받은 클라이언트는 서버의 장기 DH 키값을 가지고 있게 되고 자신의 임시(Ephemeral) DH 키를 사용해 시크릿 값을 서버에게 전달할 수 있게 된다. 알고리즘에 대한 자세한 내용은 다음 링크에서 확인해보자.

Final Handshake

위에서 언급한 것처럼 REJ 메시지를 받은 클라이언트는 서버의 장기 키와 클라이언트의 임시 키를 사용해 DH 알고리즘으로 비밀 값을 만들어낼 수 있다. 이렇게 만들어진 비밀 값을 initial key라고 한다. 클라이언트는 서버에게 메시지를 initial key로 암호화하고, 사용된 클라이언트의 임시 DH 값을 같이 보낸다. 즉 두 번째 Handshake부터 서버에게 암호화된 메시지를 보내기 시작하므로, 이를 1-RTT(1 Round Trip Time) Handshake라고 한다.

이때 보내지는 클라이언트의 메시지는 Complete CHLO라고 불린다. 이 메시지를 받은 서버가 내용을 올바르게 복호화하고 Handshake가 성공적으로 되면 SHLO(Server Hello)라는 메시지를 보낸다. SHLO에서는 서버의 장기 DH 키가 아니라, 임시 키로 만든 비밀 값을 사용해 응답을 암호화 한다. 즉, 클라이언트의 임시 키와 서버의 임시 키로 새로운 비밀 값을 만들어내는 것이다. 이때 만들어지는 비밀 값을 forward-secure key라고 한다. 마찬가지로 SHLO에 서버의 임시 키를 포함함으로써 클라이언트 역시 forward-secure key를 만들 수 있게 한다. 이후 메시지는 양측 모두 forward-secure key를 통해 암호화하고 복호화하게 된다.
Handshake 전체 그림. 열심히 그렸어요. 제발 확대해서 봐주세요.

클라이언트는 Handshake에 성공하게 되면 서버 설정 및 소스 주소 토큰을 캐시하고 있다가 같은 곳으로 반복되는 컨넥션이 발생할 때 Inchoate CHLO를 건너뛰고 바로 Complete CHLO를 보낼 수 있게 된다. 만약 이 Handshake가 성공하게 되면 바로 응답을 받게 되므로 0-RTT 컨넥션이 성공하는 것이다.

항상 Complete CHLO가 성공적인 것은 아니다. 위 논문의 이미지에서처럼 만약 클라이언트가 캐시를 사용해 Complete CHLO를 서버에 보냈을 때, 모종의 이유로 서버의 설정이 변경되거나 장기 DH 값이 바뀌는 경우가 있다. 이 경우도 서버는 REJ를 보내게 되고 클라이언트는 다시 Complete CHLO를 보내야한다.

Version Negotiation

QUIC 클라이언트와 서버는 컨넥션이 일어나는 동안 버전 협상을 한다. QUIC 클라이언트는 첫 번째 패킷에 사용할 버전을 명시해서 보내는데 만약 서버가 사용할 수 없는 버전을 가지고 있다면 서버는 서버가 사용할 수 있는 모든 버전을 담아 협상 패킷을 보내야한다. 이 과정은 RTT 딜레이를 만드는 원인이 된다.

컨넥션을 만들 때 복잡한 Handshake 과정을 생략하고 바로 0-RTT로 컨넥션이 성공하거나 1-RTT만으로 암호화된 데이터를 전달할 수 있기 때문에 지표상 컨넥션 퍼포먼스가 TCP보다 항상 높은 편이다.

Stream Multiplexing

QUIC는 HTTP/2가 프레임 같은 데이터 유닛을 TCP의 추상화된 바이트 스트림을 통해 멀티플렉싱하는 것처럼 UDP에서 이렇게 동작하도록 해두었다. QUIC는 TCP의 순차 전달로 인해 생기는 HOL Blocking을 막기 위해 UDP 위에서 동작하고 특정 논리적인 스트림에서 데이터 손실이 발생해도 다른 스트림의 흐름을 막지는 않는다.

QUIC의 스트림은 신뢰성 있는 양방향으로 바이트 스트림을 전달하는 가벼운 논리적인 스트림이다. 스트림은 최대 2^64 바이트의 임의 크기 메시지를 애플리케이션에 전달할 수 있으며 아주 가벼워 여러 스트림이 동시에 동작할 수 있다.

QUIC의 패킷은 다음 그림처럼 하나 이상의 프레임이 뒤따르는 공통 헤더로 구성되어 있다.
QUIC Packet
스트림은 스트림 ID를 통해 구분되는데, 이 값은 서버에서 시작되는 경우 짝수 아이디를 갖게 되고 클라이언트로부터 시작되는 경우 홀수의 아이디를 갖게 되어 충돌을 막는다.

스트림은 첫 번째 바이트가 보내질 때 생성되고 양측이 마지막 스트림 프레임에 FIN 플래그 비트를 찍음으로써 스트림을 닫게 된다. 만약 클라이언트나 서버 중 한쪽이라도 스트림 위의 데이터가 필요 없다고 판단되는 경우 다른 스트림이나 QUIC 컨넥션 자체를 파괴하지 않으면서 스트림을 취소할 수 있다.

QUIC 패킷 전송 속도는 흐름 제어 및 혼잡 제어 등으로 인해 제한된다. 따라서 여러 스트림이 사용할 수 있는 대역폭을 나누는 방법을 결정해야 한다. QUIC의 구현에서는 특별한 방법은 없고 HTTP/2의 스트림 우선순위 기능에 의존한다.

Loss Recovery

TCP에서는 패킷에 시퀀스 번호를 부여하고 순서대로 패킷이 전송될 수 있도록 신뢰성 있는 통신을 한다. 손실 복구 작업 역시 이 패킷의 시퀀스 번호를 사용하는데, 서버가 ACK 응답을 보낼 때 받았던 패킷의 번호를 그대로 사용하므로 클라이언트는 재전송 패킷에 대한 ACK 응답인지 오리지날 패킷에 대한 ACK 응답인지 알지 못한다. 이를 “재전송 모호 문제“(Retransmission Ambiguity Problem)이라고 한다. 또한 일반적으로 재전송 세그먼트의 손실 같은 경우 타임아웃에 의해 탐지되는데 이는 아주 비싼 동작 방식이다.

Flow Control & Congestion Control

QUIC는 두 가지의 흐름 제어 전략을 쓰고 있다. 하나는 컨넥션 자체의 모든 스트림들이 가지고 있는 버퍼의 총량에 대한 흐름 제어, 다른 하나는 스트림마다 소비하는 버퍼의 흐름제어이다. 만약 애플리케이션에서 특정 스트림을 소비하는 속도가 느리다면 해당 스트림이 전체에 자치하는 버퍼를 제한한다. 그렇지 않으면 스트림 버퍼에 데이터가 늘어나면서 다른 스트림이 사용할 수 있는 버퍼 공간을 더 소비하게 된다. 이는 전체적인 관점에서 HOL Blocking과 같은 효과를 가져올 수 있다. 기본적으로 윈도우 사이즈는 패킷이 주고받을 때마다 커지며, 컨넥션 레벨의 흐름제어와 스트림 레벨의 흐름 제어가 모두 동일하게 동작한다. 다만 컨넥션 레벨의 크기가 훨씬 커 여러 스트림들이 내부적으로 동시 동작해도 문제가 없도록 설계되어 있다.

QUIC에서 혼잡 제어 알고리즘은 특정하고 있는 바가 없다. 인터페이스를 제공하고 해당 인터페이스를 구현한 혼잡 제어 알고리즘을 사용할 수 있다. 논문에서 말하길 구글에 배포된 상태에서는 TCP와 QUIC 모두 Cubic을 혼잡 제어 컨트롤러로 사용하고 있다고 한다.

Connection Migration

QUIC 연결은 64비트 Connection ID를 통해 식별된다. Connection ID는 클라이언트 IP 또는 포트가 달라지더라도 컨넥션을 유지할 수 있게 해준다. 예를 들어 NAT 타임아웃이 발생하면서 Rebinding이 발생한다든지, 클라이언트가 네트워크 연결을 변경한다든지 하는 상황이 있을 수 있다.

Discovery for HTTPS

맨 처음 요청을 보낼 때 클라이언트는 사실 서버가 QUIC를 제공하고 있는 서버인지 알 수는 없다. 처음에는 일반적인 TLS/TCP 요청을 보내는데, 서버는 응답 헤더에 Alt-Svc 헤더를 포함하여 QUIC를 지원하고 있다는 것을 알린다. 이후 클라이언트는 서버에게 후속 요청을 QUIC와 함께 보내게 된다.

후속 요청의 경우 기존 연결이 시작된 TLS/TCP 스택과 QUIC 스택이 동시에 전달되며 경합 과정을 거친다. 하지만 300ms 정도의 차이가 있어도 (즉, TCP 위의 요청이 300ms 안쪽으로 빨랐다면) QUIC를 선호하여 선택하게 된다.

후속 요청에서 MTU 패킷 사이즈보다 QUIC Handshake 사이즈가 더 크거나, 중간에 UDP 차단으로 인해 Handshake 과정이 실패한다면 TLS/TCP 연결을 사용하게 된다.


HTTP/3 성능과 문제

HTTP/3은 컨넥션 과정을 간결하고 빠르게 설정되도록 줄였고 TCP에서 발생하는 HOL Blocking 문제를 완화했다. 아주 실험적인 것처럼 보일 수 있지만 구글에서는 과거부터 유튜브 같은 곳에서 HTTP/3을 도입했다고 한다. 그래서 생각보다는 HTTP/3가 인터넷 트래픽에서 차지하는 비율이 꽤 높다.

25% 정도나 된다고 한다.

QUIC의 Handshake는 그냥 보기에도 의미가 있어 보인다. 아래는 TCP와 QUIC의 Handshake를 비교한 모습이다.
Handshake Performance

하지만 HOL Blocking은 레이턴시가 적은, 네트워크 환경이 좋은 곳이라면 TCP와 큰 차이가 안 난다. 아래 표에서는 레이턴시가 적은 곳에서는 오히려 성능상 이점을 보이지 못했다.
QUIC Performance

그리고 계속 개선될 것으로 보이지만, UDP 스택이 TCP에 비해서는 그렇게 많이 최적화되지 않았다. 그래서 CPU 같은 컴퓨팅 자원을 더 소비하게 된다고 한다.

Reference

Author

changhoi

Posted on

2023-02-04

Updated on

2023-02-04

Licensed under

댓글

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×