recoen.

HTTP3에서 HOLB 문제를 해결한 방법 | HTTP3 멀티플렉싱의 원리

http의 변천사를 공부하던 중 TCP 자체가 가지고 있던 HOLB 문제를 http3 해결되었다는 정보를 계속해서 접했지만, 근본적으로 어떻게? 이것이 해결된 것인지 제대로 설명해주는 글을 찾는 것이 어려웠다. 이제는 우리의 친구가 되어버린 chat gpt에게 물어봐도, 얘가 제대로 된 정보를 알려주는 것 같지가 않았다. 그렇게 리서치를 계속해보던 중 좋은 자료를 찾았다.

해당 자료는 HTTP의 변천사 속에 있던 HOLB의 문제들을 소개하며 버전별로 그것이 어떻게 해결되어왔고, 어떤 한계점들이 있었는지를 소개해주고 있다. 말 그대로 HOLB 문제를 중심으로 HTTP 버전별 변천사를 설명해 준 것이다.

이 자료 덕분에 HTTP3에서는 어떤 원리로 이 문제를 해결할 수 있었는지 개략적이나마 이해할 수 있었고, 해당 내용을 기록하고 공유하고자 이렇게 글을 작성하게 되었다.


TCP에서의 HOLB

http2가 되면서, 멀티플렉싱이라는 기술이 도입되었다. 해당 기술은 다수의 요청을 연속으로 보내더라도, 응답을 받는 것은 순서와 상관없이 받을 수 있는 기가 막힌 기술이었다. 앞번의 요청에서 지연이 발생하더라도, 이후의 요청은 이전의 요청이 클라이언트에 도착하게 되기를 기다리지 않고서도 클라이언트로 전송될 수 있었다. 하지만, 이런 기술에도 문제점이 있었으니, 바로 이 멀티플렉싱이라는 기술이 TCP라는 전송프로토콜 위에서 만들어진 것이라는 점이다. 왜 이것이 문제가 되었느냐? 조금 더 구체적으로 설명해보자면, 기본적으로 멀티플렉싱이라는 기술은 하나의 커넥션 안에서 이루어진다. 하나의 커넥션 안에서 다수의 요청을 보낼 수 있도록 만들어진 것인데, 문제는 이 커넥션이 TCP를 기반으로 만들어졌다는 것이다. 같은 말을 반복하고 있는 것 같은데, 조금 더 들어보시라.

TCP를 기반으로 만들어진 커넥션이기 때문에, 이 안에서 왔다갔다 하는 패킷들은 신뢰성을 보장해야한다. 때문에 하나의 패킷에 손실이 생기면 해당 패킷이 재전송 될 때까지, 다른 패킷은 멈춰있어야하는 것이다. 이렇게 멈춰버리면서 다른 패킷들에 병목현상이 생기는 것을 HOLB라고 부를 수 있다. 그러면 이런 TCP의 HOLB 문제를 HTTP3에서는 어떻게 해결했다는 것일까?


HTTP3에서 해결된 HOLB

HTTP3에서 이 문제가 해결된 것은 HTTP2에서 멀티플렉싱이 가능하게 했던 원리와 비슷하다. 무슨 말인지 이해하려면 HTTP2에서 멀티플렉싱이 어떻게 가능했는지를 이해할 필요가 있다.

HTTP2에서 멀티플렉싱이 이루어진 원리

HTTP2에서 멀티플렉싱이 이루어진 원리는 바로 stream id에 있다. HTTP2에서는 메시지의 요청과 응답을 주고 받는 하나의 흐름을 스트림이라고 한다. 그리고 그 요청과 응답의 메시지 안에는 프레임이 담겨있다. 이 실질적으로 왔다갔다하는 데이터는 이 프레임이라고 이해하면 된다. 이 프레임이 쪼개어져서 클라이언트와 서버 사이를 왔다갔다하는데, 중요한 것은 이 프레임에 stream id가 부여되어 있다는 것이다. 실제로 다수의 요청이 이루어지면, 다수의 스트림이 생성된다. 그리고 위에서 이해했다시피 그 스트림들 안에 다수의 프레임이 생성될 것이다. 그리고 그 프레임들에는 각각의 스트림에 대한 고유한 Id가 부여되어 있을 것이다. 이 고유한 Id를 가지고 있기 때문에, 서버에서 클라이언트로 응답을 보낼 때 순서에 상관없이 막 보내도 괜찮다. 왜냐하면 클라이언트에서 해당 프레임들을 받아서, "어디보자 이 프레임은 어디 stream에 속한거지? 이 친구는 stream 1번에 속한 프레임이구만. stream1번 프레임끼리 합쳐!"이렇게 확인을 하고 재조합을 하는 것이다. 그리고 이 재조합의 과정이 TCP위에 있는 Binary Framing Layer, Application Layer 밑에 있는 Binary Framing Layer에서 이루어진다는 사실도 알아두자.

binary framing layer

그러니까, 핵심은 프레임에 부여된 stream id다. 이 stream id 덕분에 멀티플렉싱이 가능해진 것이다. 만약 내가 위에서 설명한 글이 잘 이해가 안된다면, 조금 더 친절하게 설명한 글이 여기에 있다.


HTTP3에서도 중요한 stream id

실제로 HTTP3에서도 HTTP2의 stream id에서 영감을 얻었다. 그래서 마찬가지로 이런 원리를 통해서 HOLB문제를 해결했다. 이제부터 그 내용을 살펴보자. 먼저, 알아야 할 것은 기존에는 이 멀티플렉싱의 기능이 TCP. 그러니까 전송계층 위에 속해있었다는 것이다. 하지만, HTTP3(QUIC)에서는 이 멀티플렉싱의 기능이 전송계층으로 내려왔다. 그리고 각각의 byte stream에 stream id를 부여한 것이다. 아, 이건 도대체 무슨말인가. 여기까지만 읽어선 이해할 수가 없다. 나도 이해를 못했다. 그런데 다음의 그림을 보고, 차근 차근 이해하는 과정 속에서 한층 더 높은 이해를 가질 수 있었다.

bytestream tracking

그림을 보자. 위쪽에 있는 주황색 그림이 TCP 기반의 패킷들이다. 각각의 패킷을 보면 byte range가 주어져있다. 그리고 각 패킷 안에는 스트림, 메시지, 프레임과 같은 개념과 정보들이 들어가 있을 것이다.(http2라고 전제할 때) 하지만, 중요한 것은 패킷들이 전송될 때 저 각각의 byte range의 순서가 맞추어져야한다는 것이다. 물론 저 안에 들어가 있는 스트림의 순서는 맞춰지지 않아도 된다. 어차피, 그 안에 들어가 있는 스트림 식별자를 통해서 클라이언트쪽에서 재조립할테니까. 중요한 것은 전송계층의 패킷 자체에서 byte range가 순서대로 맞춰져야한다는 것이다. 만약 {packet : 1, byte : 0-449} 가 도착한 다음, {packet : 3, byte: 750-1599}가 도착하면 그 사이에 비어있는 {packet : 2, byte : 450-749} 데이터를 재전송 요청할 것이다. 왜냐 0-499와 750-1599 사이에 450-749라는 gap이 존재하기 때문이다.

하지만 아래쪽 파란색 그림을 보자. 아래쪽 그림은 HTTP3를 이루고 있는 QUIC이라는 전송 프로토콜이다. 여기에선 각각의 패킷마다 그 안에 stream id를 가지고 있다. 이게 핵심이다. 차근 차근 생각해보자. {packet : 1, streamId : 1, byte : 0-449} 이 데이터가 먼저 도착했고, 그 다음에 {packet : 3, streamId: 1, byte : 450-999}가 도착했다. 그러니까 packet 2에 손실이 생긴 것이다. 만약 TCP였다면 그대로 다시 packet2를 재전송 요청했을 것이다. 사실 그 packet2안에는 순서와 상관 없는 stream이 담겨 있음에도 불구하고 그냥 재전송을 요청할 것이다. 하지만, QUIC에서는 패킷이 도착한다? 그러면 stream id를 먼저 확인한다. 그런 다음, 이전 stream id에서 가지고 있던 byte range를 확인한다. 내가 예를 든 패킷을 통해서 순서대로 살펴보자. 아래의 내용을 차근차근 읽다보면 원리를 이해하게 될 것이다.

  1. packet 1이 도착했다.
  2. stream id를 확인하니 1번이다. 해당 스트림에 대한 byte range를 기억한다.
  3. packet 3이 도착했다.
  4. stream id를 확인해보니 1번 stream이다. 그러면 이전에 stream id의 byte range를 확인한다. 확인해보니 0-449다. 지금 받아온 byte range는 450-999다. byte range 사이의 어떤 gap도 존재하지 않는다. 정상이라고 처리한다.
  5. packet 4를 받았다.
  6. stream id를 확인해보니 2번 stream이다. 그러면 이전에 stream id 2번의 byte range를 확인한다. 어, 그런데 해당 데이터가 존재하지 않는다. 지금 받아온 byte range는 300-599인데, 0-299라는 gap이 존재한다. stream id 2 번에 대한 이전 패킷을 다시 요청해야겠다.
  7. stream 2번에 해당하는 이전 패킷을 요청한다. 다시 손실된 패킷을 받아오는 동안, packet 4번의 데이터는 보관된다. 그리고 나머지 상관없는 stream의 패킷에는 지연이 발생하지 않는다. 오로지 Stream 2번과 관련된 패킷에만 지연이 생긴다.

이런 원리를 통해서 http3에서는 TCP 차원에서 발생하던 HOLB의 문제를 해결했다. 결국 핵심은 stream id다. 이런 고유 번호를 통해서 독립적으로 전송될 수 있도록 하는 멀티플렉싱이라는 기술이 가능해진 것이다.


이번 글에서는 HTTP3에서 어떻게 HOLB 문제를 해결했는지, HTTP3에서 어떻게 멀티플렉싱이 가능한 것인지에 대해서 이야기를 나누어보았다. 부디 여러분의 HTTP3 이해에 도움이 된 글이었기를 바란다.