1편이 주로 기존 전송 프토토콜의 확장성 문제를 다루었다고 한다면 2편에서는 프로토콜의 성능 문제 및 기존의 개선 사항을 알아 보도록 하겠습니다. 이 글의 성능 개선 기법을 이해 한다면 QUIC의 디자인이 더 쉽게 이해될 것입니다.

성능에 영향을 미치는 문제들

HTTP/1.1 + TLS (https)를 생각해 보면 하나의 HTTP요청을 위해서는 다음과 같은 단계를 거쳐야 합니다.

  1. DNS 조회. 호스트 이름에 대해서 DNS 조회 후 연결할 IP주소를 확보
  2. TCP 연결을 위한 핸드쉐이크
  3. TLS 연결을 위한 핸드쉐이크
  4. HTTP 요청 전송
  5. HTTP 응답 수신

DNS는 보통 1 RTT 라서 캐싱 이외에 시간을 줄일 수 있는 방법은 없습니다만 기본 평문으로 전송되는 단점이 있어서, 암호화를 위해 DoT (DNS over TLS)나 DoH (DNS over HTTPS)를 사용하는 경우가 있습니다.

DNS 이후 과정을 중심으로 기존의 성능 개선 사항을 알아 보도록 하겠습니다.

네트워크 지연시간 문제

인터넷에서는 물리적 또는 네트워크 지점간의 거리 및 처리 속도에 의해서 발생하는 패킷 발송과 도착 사이의 시간차가 존재 합니다. 보통 지연시간(latency)이라고 부르는 것인데, 단방향일 수도 있고 양방향 (보낸 후 받을 때 까지)일 수 있습니다만 우리가 보통 네트워크 지연 시간이라고 하는 경우 요청 패킷을 보내고 난 뒤 응답 내용의 패킷이 도착하는 왕복 시간(Round Trip Time, RTT)을 의미 합니다.

지연시간은 웹 성능 특성상 중요한 문제로 작용 하는데, 지연시간은 인터넷 발달에 더불어 점점 줄어들고 있지만 가정 또는 모바일 네트워크의 인터넷 사용에 있어서는 아직 수십 - 수백 밀리초가 걸리고 있는 것이 현실입니다. (온라인 게임에서는 핑 시간이라고 이야기하는 경우가 많습니다) 한국은 워낙 인터넷 기반 환경이 좋아서 잘 느끼지 못하는 부분이지만 국가마다 이런 상황은 크게 차이가 납니다.

지연시간이 강조 되는 이유는 또 한가지가 있습니다. 보통 인터넷 성능에서 중요하다고 생각하는 것은 100Mbps 나 1Gbps 로 표현되는 대역폭(속도)인데, 웹페이지 로딩에 큰 영향을 미치는 것은 지연시간이지 대역폭이 아니기 때문입니다.

그런데 전송 프로토콜의 관점에서 보면 지연 시간은 네트워크의 구성에 의한 요소이므로 물리적으로 줄일 수 있는 방법은 존재하지 않습니다. 가령 LTE 모바일 네트워크를 사용한다면 이론적인 RTT 한계는 40ms 정도 이며 더 줄어들지 않습니다. (5G에서는 1ms 까지 가능하다고 하므로 향후 기대되는 부분입니다) 그런데 이런 숫자는 단순하게 사용자 장치에서 사용중인 ISP까지일 경우이므로 실제 웹 서버에 도착하기 위해서는 중간에 거쳐야 하는 네트워크 경로가 더 있습니다. 가령 한국에서 미국의 웹 서버에 접속하기 위해서는 태평양 해저 케이블을 건너야 하므로 한국-미국 구간 만으로 100 ~ 150ms 정도가 추가 됩니다.

따라서, 기존의 전송 프로토콜에서의 성능 개선은 크게 두가지로 나누어 집니다.

  1. 큰 데이터를 전송해야 하는 경우 대역폭을 최대한 활용
  2. 프로토콜 상에서 초기 왕복이 필요한 구간을 최소화

1의 경우 TCP의 혼잡 제어 알고리즘 및 패킷 유실 복구 기법이 중요합니다. 관련된 기술로 1편에서 잠시 언급된 TCP의 RACK/TLP(패킷 유실 복구를 개선하는 방법)와 BBR 혼잡 제어 알고리즘에 대해 알아보면 도움이 될 것입니다.

아래에서는 주로 2에 대한 솔루션을 알아 보도록 하겠습니다. 그 이유는 HTTP의 경우 상대적으로 짧은 연결이 많이 발생하기 때문이고, 왕복 구간의 최소화는 사용자 입장에서 얼마나 웹 페이지가 빨리 표시되는지에 관련이 있기 때문입니다.

TCP - Fast Open

2014년 RFC가 나온 Fast Open은 기존 TCP의 연결 성립 과정이 최소 1-RTT(3-way handshake)가 걸리는 문제를 개선하기 위해서, 사전에 연결이 한번 되었던 경우에 클라이언트가 서버에 SYN패킷을 보낼 때 사전에 교환된 쿠키 값과 더불어 데이터를 추가하여 보낼 수 있습니다. HTTP와 같이 클라이언트가 요청하는 데이터를 먼저 보내는 경우라면 0-RTT로 HTTP요청을 보낼 수 있습니다.

아래 그림에서 좌측은 기존의 방법, 우측은 Fast Open 을 사용한 경우 입니다. 1 RTT가 절약 되었음을 알 수 있습니다.

TCP Fast Open

단점으로는 실제 사용을 위해서는 클라이언트와 서버가 모두 Fast Open을 지원해야 하고 소켓 API 사용을 일부 수정해야 하고, 1편에서 이야기한 것 처럼 중간 장비나 방화벽에 따라 동작하지 않는 경우가 있습니다. 만약 0-RTT가 불가능하다면 1-RTT로 처리되게 됩니다.

TLS

TLS는 TCP와 같은 신뢰성 있는 데이터 전송 위에서 사용할 수 있는 데이터 암호화 프로토콜입니다. 최신 버전은 2018년에 RFC가 발표된 TLS 1.3입니다.

일반적으로 TLS의 경우 암호화 키를 상호 교환하기 위해서 2 RTT를 필요로 합니다. 첫번째 왕복에서 client hello 메시지를 통해서 프로토콜 버전, 암호화 알고리즘 등을 전달하고 서버는 응답으로 server hello, certificate 메시지를 통해서 서버 인증서, 사용할 암호화 알고리즘 등을 전달하고 두번째 왕복에서는 첫번째 왕복에서 만들어진 세션 키를 통해서 암호화된 메시지를 주고 받고 서로 확인하는 과정입니다. TCP + TLS의 경우 이 경우 실제 데이터를 보내기 위한 연결을 만들기 위해 1 RTT (TCP) + 2 RTT (TLS) = 3 RTT가 소요되므로 지연 시간이 큰 경우 데이터를 보내기 이전에 암호화 연결을 만들기 위해 많은 시간이 걸리게 됩니다.

RTT를 포함한 성능 개선을 위해 TLS 계층에서 사용될 수 있는 방법은 다음과 같습니다.

  • RFC 5077 Stateless TLS Session Resumption은 최초에 만들어졌던 TLS 세션 정보를 클라이언트에서 보관 했다가 두번째 같은 서버에 다시 접속하는 경우 만들었던 세션 티켓을 사용하여 TLS 핸드쉐이크 과정의 2 RTT를 1 RTT로 줄일 수 있습니다. 기존에 동일 목적을 위해서 세션 ID를 사용하였으나 이 경우 서버에서 세션 상태를 보관하고 있어야 하는 문제가 있었는데 RFC 5077은 클라이언트에서만 상태를 보관하는 장점이 있습니다. 이 기법은 TLS 1.1 이후에서 사용할 수 있습니다.
  • RFC 8446 TLS 1.3 TLS의 최신 버전은 2018년에 발표된 1.3인데, TLS 1.3의 중요한 개선점으로 0-RTT 연결이 있습니다. 이는 RFC 5077과 유사하게 첫번째 TLS연결의 정보를 포함하고 있다가 조건이 만족 되면 TLS cliethello를 보낼 때 요청 데이터 암호화해서 같이 보낼 수 있도록 하는데, 이 경우 TCP Fast Open과 유사하게 TLS 연결에서 0-RTT가 가능 합니다. 즉 기존의 TLS연결에 2 RTT가 걸렸다면 조건이 만족되면 0 RTT에 TLS 암호화 연결 및 요청 데이터를 한번에 보낼 수 있습니다(TCP연결은 되어 있다고 가정 합니다).

위의 두 기법은 각각 1 RTT, 2 RTT를 절약할 수 있습니다. 자세한 사항은 Introducing Zero Round Trip Time Resumption (0-RTT)을 참조 하시기 바랍니다.

아래의 두 기법은 직접 RTT를 줄일 수 없지만 TLS 연결 상의 부가적인 지연을 줄일 수 있는 기법입니다.

  • OCSP Stapling - OCSP는 TLS 인증서의 상태를 확인하기 위한 프로토콜인데 보통 인증서 파기 여부를 확인하기 위해 사용됩니다(과거에는 CRL이라는 것을 사용 했습니다). 이 프로토콜을 사용하는 경우 TLS 클라이언트 (보통 브라우저) 인증서 상태 확인을 위해 별도의 OCSP에 통신하는 시간만큼 지연이 발생 하는데 OCSP Stapling은 TLS 핸드쉐이크 과정에서 TLS서버에서 인증서 상태에 대한 확인 정보를 보내므로 클라이언트가 확인할 필요가 없어지는 장점이 있습니다.
  • 동적 레코드 크기 조절 TLS는 데이터 전송을 위해 레코드를 정의하고 레코드 단위로 암호화된 데이터를 보냅니다. 이 레코드는 최대 16KB의 크기를 갖는데, TCP위에서 전송하는 경우 레코드 경계가 TCP 패킷 경계와 일치한다는 보장이 없으므로, TLS 레코드 크기가 한 TCP 패킷 크기보다 큰 경우 중간의 TCP 패킷이 유실된 경우 복구에 시간이 걸릴 수 있습니다. 또한 TCP 연결 초기 슬로우 스타트 구간에 큰 TLS 레코드 전송을 하는 경우, 실제 데이터가 브라우저에 전달 될 때 까지 복수의 왕복 시간이 걸릴 수 있습니다. 이 문제를 완화하기 위해 연결 초기의 경우 TLS 레코드 크기를 일부러 하나의 TCP 패킷 크기 (MSS)이하로 맞추어 보내는 기법을 동적 레코드 크기 조절이라고 합니다.

위의 기법을 활용하기 위해서는 클라이언트와 서버가 모두 해당 기능을 지원해야 하는 문제가 있습니다. 최신 버전인 TLS 1.3을 사용하도록 하는 것이 제일 좋습니다만 기존의 많은 프로그램이 업데이트되려면 상당한 기간이 필요할 것입니다.

Head-of-Line Blocking 문제

HTTP/1.1에는 하나의 TCP 연결을 열어서 여러 요청을 순차적으로 보낼 수 있는 연결 재사용(keep-alive)기능이 있습니다. TCP연결을 다시 열어도 되지 않고 HTTPS를 사용하는 경우 TLS연결에 필요한 시간도 절약할 수 있다는 장점이 있습니다만 여기에는 문제점이 있습니다.

보통 Head-of-Line Blocking (HoL Blocking) 이라고 하는데 (딱히 좋은 번역이 없습니다. 이 글에서는 HoLB라고 하겠습니다), 하나의 TCP 연결은 신뢰성 있는 데이터 전송을 제공해야 하므로 중간에 패킷이 유실된 경우 반드시 복구를 해야 합니다. 이 특성 때문에 한 TCP 연결에서 HTTP 요청을 순차적으로 여럿 보내는 경우 중간에 있는 요청이나 응답에서 패킷 유실이 발생하는 경우 복구될 때 까지 지연 시간이 발생하고 그 다음 요청도 그만큼 지연되게 됩니다. 이 문제를 HoLB라고 합니다.

Head-of-Line Blocking

위 그림은 하나의 TCP 연결에서 일어나는 3개의 HTTP 요청과 응답을 나타내고 있습니다. 첫번째 요청(/)은 잘 처리 되었지만 두번째 요청(/a.png)는 중간의 패킷 유실 (X로 표시된) 이 발생할 경우 TCP 수준에서 복구를 위한 재전송이 일어날 때 까지 데이터 전송이 지연된 것을 알 수 있는데, 이는 즉 두번째 응답을 받는 시간 및 세번째 요청(/b.png)을 보내는 시간이 그만큼 지연된다는 것을 의미 합니다. 하나의 연결에서 순차적으로 요청을 보내는 경우에는 이러한 중간의 패킷 유실이 전체 성능을 크게 저하시킬 수 있습니다. HTTP Pipelining 이라는 기법도 제안된 바 있습니다만 실제로는 사용되지 않고 하나의 TCP연결을 사용하는 한 HoLB도 그대로 존재 합니다.

여기에 추가하여 가용한 대역폭을 효과적으로 활용하기 위해서 HTTP/1.1 에서는 복수개의 TCP연결을 열어서 여러 HTTP 요청을 각 TCP 연결에 분배해서 처리하는 것이 일반적입니다. Chrome 이나 Firefox 의 경우 기본값이 호스트 이름 당 최대 6개의 TCP연결을 열 수 있습니다.

다음 그림은 HTTP 1.1에서 TLS 연결을 병렬로 4개 열어서 5개의 요청을 보내는 것입니다.

HTTP 1.1

이는 성능상의 이점은 있을지 모르지만 한 브라우저가 서버에 여러개의 연결을 열게 되므로 서버에 그만큼 부담이 가는 문제가 발생 합니다. 게다가 병렬 연결 수는 호스트 단위로 제한된다는 점을 이용하여 같은 서버에 여러개의 이름을 만들어서 연결 갯수를 더 늘리고자 하는 도메인 샤딩 (가령 이미지 파일이 img.foo.com에 있는 경우 img1.foo.com, img2.foo.com 과 같이 동일한 내용의 호스트 이름을 더 만들면 그만큼 TCP연결을 더 만들 수 있습니다)과 같은 기법도 사용되고 있습니다. 이 기법은 서로 다른 호스트명을 사용하는 만큼 DNS 요청 시간이 추가로 필요하고 서버 입장에서는 더 많은 부하로 작용 합니다.

또한 병렬 TCP 연결을 사용하는 경우의 생각해 봐야 하는 문제가 하나 더 있습니다. TCP의 혼잡 제어는 연결 단위로 이루어 집니다. 즉 동일 서버로 여러개의 TCP 연결이 생기는 경우 각각의 연결 마다 혼잡 제어를 하게 되는데, 서버까지 동일한 경로라면 하나의 혼잡 제어를 통하는 것이 보다 대역폭을 효율적으로 사용할 수 있습니다. 또한 큰 파일 다운로드가 아니라 웹 페이지와 같이 상대적으로 짧은 연결이 여러개 일어나는 경우라면 사용 가능한 대역폭을 모두 사용하지 못하는 경우가 많은데, 여러개의 병렬 연결 대신 하나의 연결로 여러 HTTP요청을 보낼 수 있다면 대역폭을 보다 효율적으로 이용할 수 있고 이런 생각이 HTTP/2의 멀티플렉싱으로 이어 집니다.

HTTP/2의 멀티플랙싱의 경우는 아래 그림처럼 하나의 TLS연결에 여러개의 요청을 넣어서 보낼 수 있습니다. 실제로는 적절한 크기의 프레임 단위로 각 요청과 응답을 나누어서 보내게 됩니다.

HTTP 2 멀티플렉싱

이 방법은 리소스를 절약하고 대역폭의 효과적인 사용을 가능하게 합니다만, 하나의 TCP연결을 사용하고 있으므로 위에서 언급한 HoLB문제가 여전히 존재 합니다.

종단간 암호화 문제

TLS를 사용하면 데이터 통신상의 암호화 문제는 대부분 해결이 됩니다. TLS 1.3 에서는 기존 1.2까지에서 암호화하지 않던 핸드쉐이크 과정을 추가로 암호화해서 인증서도 암호화 된 상태로 전달이 되고, 평문으로 노출이 되던 SNI 확장의 경우 ESNI를 사용하면 추가로 암호화가 가능해 집니다.

하지만 여전히 TLS는 TCP위에서 동작하게 되므로 TCP 연결을 추적하는 경우 TLS연결에 대해서도 어느정도 정보를 얻을 수 있고, 연결을 변조할 수는 없지만 중간에서 RST패킷을 조립해서 보내는 방법으로 강제로 종료시킬 수 있습니다.

이러한 중간 장비의 개입을 추가로 방지하기 위해서라면 TCP 헤더에 평문으로 노출이 되는 정보까지 모두 암호화해야 할 필요성이 제기 됩니다. 이 부분은 TCP + TLS 기반의 HTTP/2 에서는 해결되지 못하는 부분입니다.

HTTP 2.0

위에서 언급된 문제를 해결하기 위해서 기존의 HTTP/1.1을 보완하기 위한 HTTP/2 가 2015년에 발표되었지만, 아직도 HTTP/2를 적용하지 않은 사이트도 많습니다. Usage of HTTP/2 for websites에 따르면 전세계 웹사이트의 34.1% (2019년 3월 기준)이 사용중이라고 하니 아마 국내에서의 도입율은 더 낮을 것으로 생각이 됩니다. 가령 www.naver.com은 HTTP/2 로 응답합니다만 www.daum.net은 HTTP/1.1로만 응답하고 있습니다.

확인을 위해서 http2기능이 포함된 curl 을 사용해 보도록 합니다. 네이버의 경우

% curl -vso/dev/null https://www.naver.com
* Connected to www.naver.com (210.89.164.90) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
...
* ALPN, server accepted to use h2
...
> GET / HTTP/2
...
< HTTP/2 200
...

위와 같이 ALPN에서 h2 라고 응답하고 실제 요청에 HTTP/2 로 응답하는 것을 볼 수 있습니다.

daum의 경우 아래와 같이 http/1.1 으로 응답하는 것을 볼 수 있습니다.

% curl -vso/dev/null https://www.daum.net
...
* ALPN, offering h2
* ALPN, offering http/1.1
...
* ALPN, server accepted to use http/1.1
...
> GET / HTTP/1.1
...
< HTTP/1.1 200 OK
...

HTTP/2는 구글이 만든 SPDY를 기반으로 표준화한 것이라 할 수 있는데, 멀티플렉싱, 헤더 압축, 우선순위 및 푸시 등은 SPDY의 주된 디자인 개념이기도 합니다.

다음은 HTTP/2의 주요 특징 입니다.

  • TCP와 같은 신뢰성 있는 전송 프로토콜 위에서 동작
  • 바이너리 포맷
  • HTTP의 요청-응답 형태, 헤더나 각각의 헤더의 의미에 변화가 없음. 전송하는 방법만을 재정의함
  • 멀티플렉싱 - 여러개의 HTTP 요청/응답을 하나의 TCP연결에서 보냄. 각각의 HTTP 요청/응답은 요청 별로 스트림이라는 단위로 전송 되고 스트림 안에 여러개의 프레임이 존재
  • HPACK 기법을 통해서 요청/응답 헤더를 압축
  • 흐름 제어 - 복수의 스트림이 단일 연결에서 전송되지만 각각의 흐름 제어를 통해서 클라이언트에서 받아들일 수 있을 만큼만 전송 가능
  • TLS에 의한 암호화 - HTTP/2는 스펙상 평문으로도 보낼 수 있지만 실제 브라우저는 모두 TLS 위에서만 동작
  • 서버 푸시 - 클라이언트가 명시적으로 요청하지 않아도 서버가 선제적으로 보낼 수 있음
  • 요청에 대해 우선순위 부여 가능

HTTP/2에 대한 더 자세한 설명은 Introduction to HTTP/2, http2 explained을 참조 하시기 바랍니다.

정리하며

이 글에서는 기존에 사용중인 프로토콜인 TCP, TLS, HTTP/2의 주된 성능 개선을 위한 기법을 알아 보았습니다. 이러한 기능을 지원하는 최신 버전의 브라우저와 웹 서버 및 클라우드, CDN을 사용한다면 많은 성능 향상을 얻을 수 있습니다만, 다음과 같이 여전히 해결되지 않은 문제들이 남아 있습니다.

  • 하나의 TCP 연결을 사용하는 한 HoLB 회피 불가
  • TCP를 업그레이드하려면 OS 수준의 클라이언트 및 서버 변경 필요
  • TLS를 업그레이드하려면 클라이언트 및 서버의 변경 필요
  • 중간 장비 (middlebox)의 고착화 문제로 TCP나 TLS의 성능 개선 기법이 적용되지 않는 경우
  • 중간 장비에 의한 데이터 변조 및 필터링

QUIC은 기존에 해결된 방법을 그대로 활용하면서 남아있는 문제를 해결하기 위해 새로운 접근 방법을 제시하고 있다고 할 수 있습니다.

다음 편에서는 QUIC의 특징에 대해서 알아 보도록 하겠습니다.