Cloudflare는 HTTP/2 클라이언트들이 우리 네트워크에 업로드하는 속도를 향상하였습니다. 이 글에서는 문제를 찾아내는 방법에서 시작하여 이를 수정하고 글로벌 인터넷의 업로드 속도 향상이 되기까지의 과정에 대해 설명하겠습니다.

우리는 사용자 네트워크의 성능 측정을 위해 2020년 5월에 speed.cloudflare.com을 런칭하였습니다. 이 사이트는 다운로드, 업로드와 지연 시간 테스트를 제공 합니다. 릴리즈 직후 몇몇 사용자에게서 업로드 속도가 낮게 표시된다는 리포트를 받았습니다. 조사 결과 고속의 업로드 대역폭을 가진 고객 (수백Mbps 업로드 속도가 가능한 케이블이나 파이버 서비스)에게서 발생하는 것으로 보였습니다. 우리의 속도 측정은 브라우저 자바스크립트로 동작하며 대부분의 브라우저는 HTTP/2 를 기본 사용하고 있습니다. 이후 사용자가 매우 빠른 업로드 대역폭을 갖고 있는 경우 HTTP/2 업로드 속도가 HTTP/1.1 (모두 TLS라 가정 합니다)보다 매우 느린 경우가 있는 것을 알게 되었습니다.

업로드 속도는 가정용 브로드밴드 연결을 사용하고 있는 사람들에게 예전보다 더 중요해 졌습니다. 많은 사람들이 예전과는 다르게 집에서 가정용 회선으로 재택 근무를 할 수 밖에 없는 상황이 되었기 때문입니다. 코로나 대유행 이전의 브로드밴드 트래픽은 매우 비대칭적이었습니다만 (다운로드 속도가 업로드보다 매우 빠릅니다... 음악을 듣거나 영화를 스트리밍으로 보는 경우를 생각해 보세요) 이제 사용자들이 가정에서 비디오 컨퍼런스에 참여하거나 컨텐츠를 만들어 올리는 일을 많이 보게 되었습니다.

초기 테스트

고객의 리포트는 주로 빠른 가정용 네트워크에 집중되었으므로, 제어된 환경에서 업로드 속도를 테스트할 수 있도록 dummynet 네트워크 시뮬레이터를 설정 하였습니다. 맥북 프로 안에 회사 코드를 실행할 수 있도록 리눅스 VM을 만들고 맥북 호스트와 VM간에 dummynet을 구성 하였습니다. 업로드 속도를 측정하는 것은 간단합니다 - 파일을 만들어서 요청 본문을 받아들일 수 있는 종착점으로 curl을 사용해서 업로드하면 됩니다. 동일한 테스트를 20회 수행하여 중간값을 측정 하였습니다(Mbps).

% dd if=/dev/urandom of=test.dat bs=1M count=10
% curl --http1.1 -w '%{speed_upload}\n' -sf -o/dev/null --data-binary @test.dat https://edge/upload-endpoint
% curl --http2 -w '%{speed_upload}\n' -sf -o/dev/null --data-binary @test.dat https://edge/upload-endpoint

200Mbps 대역폭과 40ms 왕복 시간(RTT)의 네트워크에서 10MB 크기 파일을 업로드하는 것을 측정해 보자 결과는 놀라웠습니다. 실 서비스와 동일한 설정을 사용할 때 HTTP/2 업로드 테스트 속도는 동일 환경의 HTTP/1.1보다 거의 절반 정도의 속도였습니다. (아래 차트는 높은 수치가 좋은 것입니다)


여러분의 네트워크에 따라 결과는 다를 수 있지만 네트워크가 더 빠르면 차이가 더 컸습니다. 반대로 느린 네트워크의 경우, 가령 느린 가정용 케이블 연결의 경우 (5Mbps 업로드와 20ms RTT) HTTP/2 업로드 속도는 HTTP/1.1에서 측정된 것과 거의 동일 하였습니다.

수신 흐름 제어

이 주제에 대해서 더 알아보기 전에, 제 느낌은 이것은 수신 흐름 제어와 관련된 것이 아닌가 하는 것이었습니다. 일반적으로는 클라이언트 (브라우저 또는 HTTP 클라이언트)는 데이터의 수신자이지만, 이 경우에는 클라이언트가 서버에 컨텐츠를 업로드하는 것이라 서버가 데이터의 수신자가 됩니다. 그리고 수신자는 수신 버퍼의 흐름을 제어할 필요가 있습니다.

HTTP/1.1 과 HTTP/2 의 수신 흐름 제어 방식은 어떻게 다를까요? 가령 HTTP/1.1은 수신 흐름 제어를 정의하지 않았는데, 연결 내에 여러 요청이 동시에 전송되지 않기 때문에 수신 흐름 제어는 TCP 계층에 맡겨져 있습니다. 최근의 OS TCP 스택은 수신 버퍼의 자동 조정을 하고 있으며 (나중에 다시 살펴 봅니다) 이는 현재의 BDP (대역폭-지연시간 곱)에 기반하고 있습니다.

HTTP/2 의 경우 프로토콜이 스트림 멀티플렉싱을 지원하기 때문에 스트림 수준 흐름 제어가 존재 합니다. 각 HTTP/2 스트림은 자체적인 흐름 제어 윈도우를 갖고 있고 연결의 모든 스트림을 제어하는 연결 수준 흐름 제어도 있습니다. 만약 흐름 제어에 여유가 없다면 데이터 송신은 흐름 제어에 의해 일시 중지될 수 있습니다. 만약 너무 여유가 있다면 버퍼링에 메모리를 낭비하게 될 수도 있습니다. 따라서 최적의 흐름 제어를 유지하는 것은 흐름 제어를 구현할 때 중요하며 일반적인 최적 전략은 수신 버퍼 크기를 현재의 BDP에 맞추는 것입니다. BDP는 현재 네트워크에서 전송되고 있는 데이터의 최대 크기이며 최적의 버퍼 크기로 사용될 수 있습니다.

NGINX가 요청 본문 버퍼를 다루는 법

처음에는 NGINX 업로드 버퍼링 관련 설정값을 찾아 보고 이 값을 변경하는 것이 좋은 결과를 나타내는지 보려 했습니다. 요청 본문 업로드에 관련된 설정값은 다음과 같습니다.

HTTP/2 관련으로는 다음이 있습니다.

Cloudflare는 proxy_request_buffering 을 사용하지 않으므로 이 상황은 고려하지 않도록 하겠습니다. client_body_buffer_size 는 프로토콜에 공통으로 사용되는 요청 본문 버퍼의 크기이며 HTTP/1.1 과 HTTP/2 에 모두 적용 됩니다.

코드를 살펴보게 되면 다음과 같이 동작 합니다:

  • HTTP/1.1: client_body_buffer_size 크기의 버퍼를 클라이언트와 업스트림 사이에서 사용하고 이 버퍼를 이용하여 읽기와 쓰기를 반복 합니다.
  • HTTP/2: HTTP/2 DATA 프레임의 흐름 제어 업데이트를 해야 하기 때문에 다음의 두 설정값이 관련 됩니다.
    • http2_body_preread_size: 업스트림으로 보내기 전에 미리 읽어둘 초기 요청 본문의 크기를 지정 합니다.
    • client_body_buffer_size: 요청 본문 버퍼의 크기를 지정 합니다.
    • 이 두 설정값은 업로드 중의 요청 본문 버퍼를 할당하는데 사용 됩니다. 다음은 버퍼링 없는 업로드가 어떻게 동작하는지에 대한 간단한 요약 설명 입니다:
      • http2_body_preread_sizeclient_body_buffer_size 중 큰 값으로 하나의 요청 본문 버퍼를 할당 합니다. 만약 http2_body_preread_size가 64KB이고 client_body_buffer_size가 128KB라면 128KB 의 버퍼가 할당 됩니다. 우리는 client_body_buffer_size로 128KB를 사용하고 있습니다.
      • HTTP/2 스트림의 INITIAL_WINDOW_SIZE 설정은 http2_body_preread_size로 지정되며 우리는 64KB (RFC7540의 기본값)을 사용하고 있습니다.
      • HTTP/2 모듈은 업스트림으로 보내기 전에 http2_body_preread_size 만큼 읽어 들입니다.
      • 사전에 읽어들인 버퍼를 모두 업스트림으로 보내고 나면 요청 본문을 모두 읽어 들일 때 까지 클라이언트에서 계속 읽어 들인 내용을 업스트림으로 보내고 필요시 WINDOW_UPDATE 프레임을 클라이언트에게 보내는 일을 반복 합니다.

요약하자면 다음과 같습니다. HTTP/1.1은 단일 버퍼를 사용 하고 TCP 소켓 버퍼가 흐름 제어를 합니다. 하지만 HTTP/2의 경우 어플리케이션 계층에서도 수신 흐름 제어를 하는데 NGINX는 수신 버퍼에 고정 크기를 사용 합니다. 이는 현재 네트워크의 BDP가 정해진 수신 버퍼 크기보다 큰 경우 업로드 속도를 제약하게 됩니다. 따라서 버퍼 크기가 너무 작은 경우에 HTTP/2 흐름 제어에서 병목이 생기게 됩니다.

버퍼 크기를 늘리면 될까요?

이론적으로는 버퍼 크기를 늘리면 업로드 병목은 해결될 것이므로 이 값을 늘려 가며 테스트를 다시 해 보았습니다. 이전 결과 차트는 "(prod)"라고 표시하였습니다. client_body_buffer_size 를 256KB, 512KB, 1024KB로 늘려 가며 HTTP/2 업로드 테스트를 다시 해 보았습니다:


client_body_buffer_size의 최적값은 512KB로 보입니다.

만약 다른 네트워크에서는 어떨까요? 아래는 동일 네트워크에서 RTT가 10ms일 경우입니다. 이 경우 256KB가 최적으로 보입니다.

두가지 모두 현재의 128KB보다는 더 낫고 HTTP/1.1 과 동등하거나 때로는 더 나은 성능을 보여주고 있습니다. 하지만 최적의 버퍼 크기는 경우에 따라 다르며, 단순히 큰 버퍼 크기를 선택하게 되면 오히려 성능에 악영향을 미치게 됩니다. 따라서 최적의 버퍼 크기를 찾기 위한 똑똑한 방법이 필요합니다.

요청 본문 버퍼 크기 자동 조정

이런 경우에 도움이 되는 아이디어 중 하나는 자동 조정입니다. 가령 최근의 TCP 스택은 수신 버퍼 크기를 자동 조정합니다. 우리의 엣지 서버에서도 TCP 수신 버퍼 자동 조정은 기본으로 설정되어 있습니다.

net.ipv4.tcp_moderate_rcvbuf = 1

하지만 HTTP/2 의 경우, HTTP/2 계층에서 직접 수신 흐름 제어를 하고 있고 기존의 128KB 는 큰 BDP 에서는 너무 작은 값이므로 TCP 버퍼 자동 조정은 그리 효과적이지 않습니다. 이런 이유로 TCP가 하는 것 처럼 HTTP/2 수신 버퍼 자동 조정을 시도해 보기로 하였습니다.

기본적인 아이디어는 BDP를 측정하여 그 값에 맞게 HTTP/2 요청 본문 버퍼 크기를 두배씩 늘리는 것입니다. 다음은 현재 우리의 NGINX에 구현된 알고리즘입니다:

  • 위에서 설명한 대로 요청 본문 버퍼를 할당.
  • 매 RTT (linux tcp_info 사용) 마다 현재 BDP를 갱신.
  • 현재 BDP > ( 수신 윈도우 / 4 ) 인 경우 요청 본문 버퍼 크기를 두배로 늘림.

테스트 결과

실험실 테스트

아래는 HTTP/2 업로드 버퍼 자동 조정을 사용하였을 때의( client_body_buffer_size는 계속 128KB를 사용) 테스트 결과입니다. "h2 autotune"으로 표시 하였는데, 잘 동작 하는 것을 볼 수 있습니다 - (원래의 목표였던) HTTP/1.1 속도와 동등하거나 조금 더 빠른 것을 알 수 있습니다. 특정 네트워크에 맞게 수동으로 고른 버퍼 크기 보다는 약간 느릴 수 있지만, 서로 다른 네트워크 조건에 맞게 자동적으로 최적의 버퍼 크기를 찾는 것을 볼 수 있습니다.

실 환경 테스트

이 기능을 실 서비스에 적용한 후에 빠른 네트워크 노드를 사용해서 실 서비스 엣지 서버에 10MB 파일을 업로드하는 동일한 테스트를 진행 하였습니다. 빠른 네트워크 (Gbps급)과 낮은 지연 시간 (10ms 이하)에서 업로드 테스트를 하기 위해 구글 클라우드에 리눅스 VM 인스턴스를 만들었습니다.

다음은 구글 클라우드 벨기에 지역에서 7ms 떨어져 있는 우리의 CDG (프랑스 파리) PoP로 업로드 테스트를 진행한 경우 입니다. 이 경우 3배 가까운 향상이 있음을 알 수 있습니다.


구글 클라우드 도쿄 지역에서 2.3ms 떨어진 우리의 NRT (도쿄) PoP으로 업로드하는 경우도 테스트해 보았습니다. 일반 가정용 네트워크라고 볼 수는 없겠지만 결과는 흥미롭습니다. 128KB 고정 버퍼 크기도 괜찮게 동작하지만 HTTP/2 업로드 버퍼 자동조정의 경우가 HTTP/1.1 보다 더 빨랐습니다.

요약

HTTP/2 업로드 버퍼 자동 조정 기능은 현재 Cloudflare 엣지 서버에 기본 적용되어 있습니다. speed.cloudflare.com 을 포함하여 고객은 HTTP/2 연결을 통한 업로드 속도의 향상을 기대할 수 있습니다. 업로드 버퍼 크기를 자동 조정하는 것은 대부분의 경우에 잘 동작 하며 이제 HTTP/2 업로드는 예전보다 더 빠릅니다! 성능이라고 하면 다운로드 속도나 지연 시간의 개선을 생각하기 쉽습니다만, 대량의 데이터 업로드가 필요한 재택 근무자에게 빠른 업로드도 도움을 줄 수 있습니다. 가령 사진/비디오 공유, 콘텐츠 제작, 비디오 회의나 라이브 방송과 같은 경우입니다.

이 글 작성에 많은 도움을 준 Lucas PardueRustam Lalkaka에게 감사드립니다.

추신: 패치를 공개하고 NGINX 개발자에게 보냈습니다. 다음 링크에서 찾아 볼 수 있습니다.