[HTTP/2] HTTP2에 관하여

Doccimann·2022년 9월 9일
1

❗️ 이 글을 쓰게된 계기

최근에 러닝 HTTP/2 라는 책을 다 읽게되어, 이를 리뷰하는 김에 해당 글을 작성하게 되었습니다.

해당 책을 하나의 글로 요약할것이지만, 상세한 내용은 책을 읽어보는것을 추천합니다. 제가 깨달은 바, 그리고 책의 내용이 뒤섞여있기 때문에 책이 더 정확할수도 있습니다! 🤗

📚 HTTP의 발전 역사

HTTP/2에 관해서 서술하기 이전에 HTTP의 발전 역사에 관해서 서술을 해보고자합니다.

1️⃣ HTTP/0.9

HTTP/0.9는 매우 단순한 프로토콜이었습니다. 메소드는 GET만이 존재했고 HTML만을 주고받을 수 있게 설계되었습니다.

그리고 HTTP 헤더또한 존재하지 않았습니다.

2️⃣ HTTP/1.0

HTTP/1.0에 들어서 꽤 많은 기능이 추가되었는데, 다음과 같은 것들이 추가되었습니다.

  • 헤더
  • 응답 코드 (Status Code)
  • 리다이렉트
  • 오류
  • 조건부 요청
  • 콘텐츠 인코딩
  • 다양한 메소드 지원 (POST, DELETE, PATCH, etc...)

하지만 HTTP/1.0은 REST하기 위한 요건들을 만족못한 부분이 있었기도하고, 1개의 요청 당 1개의 TCP 커넥션을 맺고 끊기 때문에 TCP Handshake 오버헤드가 상당히 많이 발생한다는 단점이 있었습니다.

3️⃣ HTTP/1.1

REST를 만족하기 위한 조건은 아래와 같은 것들이 있습니다.

  • CS (Client-Server) 구조
  • Caching
  • Stateless
  • Uniform Interface
  • Code-On-Demand

이 중에서 HTTP/1.1 들어서 Caching을 강하게 지원하기 시작하였고, Uniform Interface 요건 중 Self-Descriptive를 만족시키기 위해서 Request Header에서 HOST 헤더를 필수 항목으로 넣었습니다.

그리고 HTTP/1.1 부터는 1개의 요청마다 TCP Connection을 맺고 끊지않고, Application Layer에서도 TCP에서 적용되던 Timer 개념을 도입하여 여러개의 요청을 하나의 커넥션으로 해결할 수 있게되었습니다.

그 외에도 HTTP/1.1 버전부터 파이프라이닝 이라는 기능이 지원되기 시작하였지만, HOL(Head of Line) Blocking 현상 때문에 HTTP/1.1 버전에서는 파이프라이닝을 구현하는 곳은 거의 없습니다. (이거는 차차 설명드리겠습니다.)


📈 HTTP/1.1의 문제점을 논하기 이전에 응용계층 상에서의 성능 지표를 언급하고 가겠습니다.

1️⃣ 지연 시간

지연 시간이란, 패킷 하나가 다른 지점까지 이동하는데 걸리는 시간을 의미합니다. 여기서 추가로, RTT(Round Trip Time)이라는 개념 또한 존재하는데, 지연시간 * 2 의 값을 의미합니다.

2️⃣ 대역폭

대역폭이란, 현실 세계로 따지만 도로의 폭이라고 생각하시면 되겠습니다. 하나의 연결에 대한 용량을 의미하는데, 대역폭이 작은 경우 포화 상태에서 병목이 발생할 수 있습니다.

3️⃣ 연결 시간

연결을 수립하려면 TCP 계층에서 3-way handshake라는 과정을 거쳐야합니다. 3-way handshaking 과정은 아래의 단계를 거쳐 수립됩니다.

  • 한 쪽에서 SYN 비트를 날립니다.
  • 이를 받는 수신자는 SYN + ACK을 날리기 이전에 자신의 SYN Queue에 해당 SYN에 대한 정보를 쌓아둡니다.
  • 다른 한 쪽에서 SYN + ACK을 수신하면 ACK을 다시 반대편으로 날립니다.

이 과정에서 병목점은 다음과 같습니다.

  • 3개의 segment가 왔다갔다 하는데 걸리는 시간, 그리고 대역폭 문제
  • Receiver 측의 SYN Queue의 상태 (Queue가 꽉차서 overflow가 걸리면 여기서 치명적인 병목이 발생할 수 있다)

4️⃣ TLS 협상 시간

클라이언트가 만약 HTTPS 연결을 하고있다면 최초 연결 시 TLS Handshake를 해서 서로의 대칭키를 주고받아야합니다. 이 과정에서 약간의 병목이 발생합니다.

5️⃣ Rendering Time

그 외에도 서버 측에서 렌더링하는데 걸리는 시간 또한 성능 지표에 포함이 됩니다.


🤔 HTTP/1.1은 무엇이 문제였을까?

우선 이전에 언급했던 HOL Blocking 문제부터 언급하고 가겠습니다.

1️⃣ HOL Blocking 문제

HTTP/1.1까지의 패킷 교환은 선로가 1개뿐인 열차의 행렬 이라고 생각하면 매우 쉽습니다.

HTTP/1.1 까지는 HTTP 데이터를 순차 처리하기 때문에 먼저 들어온 요청의 처리가 오래 걸리면 뒤따르는 요청에 대한 처리는 계속 밀릴 수 밖에 없습니다. 이를 Head of Line Blocking 현상이라고 부릅니다.

2️⃣ 이를 막기위해서 하나의 브라우저가 TCP Connection을 6개 정도 맺는다

말 그대로, HTTP/1.1 까지의 브라우저들은 HOL 블로킹 현상으로 인한 병목 현상을 막기 위해 TCP Connection을 6개 정도 맺어두고 시작했습니다.

그런데 이것도 그거대로 문제인데,

  • TCP Connection은 공짜가 아니다. 커넥션을 유지하는데도 비용이 일어난다.
  • 커넥션 개수가 늘어났을 뿐이지, 각각의 커넥션에는 여전하게도 HOL 블로킹 현상이 일어난다.
  • 각각의 TCP Connection은 각자만의 혼잡제어를 수행한다. 이 과정에서 최선의 Congestion Window 사이즈를 찾기 위해 널뛰기를 시작하는데, 이게 각 커넥션에서 일어난다고 생각해보아라. 진짜 끔찍하다.

🥲 GET 요청에 걸리는 메시지 헤더의 총 크기를 63Kb 정도로 추산해봅시다. TCP의 한 개 Segment Size는 1460 byte이기 때문에, 리노 알고리즘에 따라서 혼잡제어 알고리즘을 수행하면 대략 63Kb를 보내는데 6번 정도의 Congestion 널뛰기를 거친다고 알려져있습니다. (Stadium Effect에 대한 고려는 배제하였습니다)

결론적으로, TCP 연결을 다중으로 맺는것은 일시적인 해결책이 될수는 있지만, TCP가 각각의 연결을 최적화해주지는 않기 때문에 여기서 병목이 발생할 수 있습니다.

3️⃣ 불필요하게 비대한 메시지 헤더

한 브라우저를 로딩한다고 가정해봅시다. 이 때 GET요청이 서버를 향해 날아갈텐데, 하나의 Header가 460 byte, 그리고 개체의 수는 140개라고 가정하면 브라우저를 로딩하는데 헤더만 63Kb가 들어갑니다.

여기서 헤더의 내용이 겹치는 부분이 많을것이지만, HTTP/1.1까지는 이 헤더에 공통적인 내용이 있다고 하더라도 이를 떼낼 방법이 없었기 때문에 곧이곧대로 보내야합니다. 여기서 속도 저하의 요소가 하나 추가됩니다.

4️⃣ 우선순위의 문제

HTTP/1.1 까지는 한번에 하나의 요청/응답을 순차처리할 수 있다고 말한 바 있습니다.

여기서 만약에 특정 응답이 높은 우선순위를 가져서 먼저 처리가 되어야한다고 가정하면, 아래의 문제가 유발됩니다.

  • 해당 우선순위를 가지는 요청을 먼저 탐색해야합니다. 이 과정에서 요청 처리가 멈춥니다.
  • 찾아도 문제입니다. 우선순위가 밀리는 요청이 기아 상태 (Starvation status)에 빠질 확률이 존재합니다.

🔨 HTTP/2에서의 개선점

위의 문제들을 해결하기 위해 HTTP/2는 아래의 사항을 채택하였습니다.

1️⃣ Message를 Header + Data -> Data layer / Framing layer 로 분리해서 날리기 시작했습니다.

HTTP/2부터는 데이터의 효율적인 전송을 위해서 데이터 부분과 그 외의 정보를 담는 프레임 계층으로 분리하여 각각을 따로 보내기 시작했습니다. 이게 아무래도 HTTP/2의 핵심이 아닐까 생각합니다.

HTTP/2는 이걸 이용해서 많은 부분을 개선하였습니다. 자세한거는 차차 설명드리겠습니다.

2️⃣ 프레임을 Text가 아닌 Binary 형태의 정해진 Frame으로 보낸다

이전의 HTTP/1.1 까지는 모든 데이터를 응용계층에서 하위 계층으로 text 타입으로 보냈었습니다. 해당 특징은 아래의 문제를 야기한 바 있습니다.

  • 데이터를 파싱하는데 얼마나 파싱할지 결정이 되어있지 않다. 즉, 데이터 파싱 크기의 예측이 불가능했다.
  • 결국에 읽는건 컴퓨터다. text를 binary로 결국에 해석해야하기 때문에 여기서 오버헤드가 발생한다.

3️⃣ 파이프라이닝

Message를 데이터 레이어와 프레임으로 쪼개서 보내기 때문에, 전송 메세지의 다중화가 가능해졌습니다. 따라서 요청을 서로 뒤섞어서 보내는게 가능해지다보니 아래의 개선이 이루어졌습니다.

  • HOL 블로킹 현상이 개선되었다.
  • 개체 요청간 우선순위를 부여하는게 더욱 효율적으로 이루어질 수 있어졌다.

4️⃣ 흐름제어 (Flow Control)

Flow Control 기능은 TCP에서 등장하는 개념이었습니다. 이게 HTTP/2에 들어서 Application layer에서도 추가되었습니다.

이또한 Frame 계층을 이용해 구현이 되었는데, 요청을 보낼 때 함께 WINDOW_UPDATE 프레임을 함께 보냄으로써, 자신의 수신 가능한 최대 바이트 수를 서버측으로 전달하여 Client또한 전송 데이터의 속도 조절에 참여할 수 있게되었습니다.

5️⃣ 서버푸시 (Server Push)

HTTP/2 부터는 약속된 개체를 Client에 미리 전달해두는 것이 가능해졌습니다. 다름 아닌, PUSH_PROMISE라는 프레임을 client 측에 전송함으로써 이를 구현했습니다.

이를 통해서 얻은 이점은 아래와 같습니다.

  • 미리 약속된 개체, 혹은 우선순위가 높은 개체, 자주 사용되는 개체를 미리 client로 보내서 렌더링 시간을 절약합니다.
  • Server측에서 한번에 받는 요청의 수가 줄어드는 효과를 가져오기 때문에 비용 절감, 그리고 요청 처리 시간이 줄어드는 효과가 발생합니다.

하지만 이를 위해선 약속되어야하는게 몇가지가 있습니다.

  • 전송 데이터보다 PUSH_PROMISE 프레임이 먼저 도착해야하는 것이 보장되어야합니다. 전송 데이터가 먼저 도착하는 경우 Client 측에선 이게 서버푸시로 인해 들어온 데이터인지 판별할 방법이 없기 때문에 후에 해당 개체를 재요청하는 현상이 벌어질 수도 있습니다.
  • 멱등적인 요청이어야한다. 즉, 몇번의 요청이 이루어지든 간에 상태가 변하지 않아야합니다. 이는 곧, 서버푸시는 GET 요청으로 이루어져야한다는 뜻입니다.

6️⃣ 헤더 압축 (HPACK)

이전에도 말했다시피, 460 byte 정도의 헤더를 140번 정도 날리게되면 63Kb 만큼의 헤더가 발생합니다. 여기서는 공통된 내용이 많기 때문에 이를 뽑아낼 방법이 필요했는데, HTTP/2부터는 Frame layer의 형태로 헤더가 분리되어 날아가기 때문에 공통된 부분을 미리 보내고 별도의 추가 내용은 따로 보내는 방식으로 헤더의 크기를 줄여내는게 가능하기도합니다.

여기서 추가적으로, HTTP/2부터는 허프만 인코딩 알고리즘을 이용해서 헤더를 압축하는것 또한 가능하기 때문에 기존의 방식보다 대략 90% 정도의 바이트 수를 줄일수있다고 알려져있습니다.


🥲 HTTP/2에도 단점은 있어요

HTTP/2의 단점을 소개해보겠습니다.

1️⃣ 패킷 손실에 취약합니다.

HTTP/2는 파이프라이닝 기능이 지원되기 때문에 TCP 다중 연결은 안티패턴으로 통합니다.

이로 인해서 발생하는 문제일수도 있는데, TCP 혼잡제어의 널뛰기 현상이 패킷 손실에 의해 발생해버리면 CWND가 1로 초기화되어서 다시 널뛰기를 시작해야하기 때문에 여기서 커다란 병목이 발생할 여지는 충분히 존재합니다.

2️⃣ TCP에 여전히 의존한다

1번의 연장일수도 있는데, single connection에서 패킷 로스가 터지면 TCP는 CWND가 1로 깎이거나, 그 외의 TCP 구현에 의해 손해를 볼 때가 있습니다.


⭐️ What's next of HTTP/2 ?

구글에서 개발한 QUIC 이라는 Transport layer의 프로토콜이 있습니다.

해당 프로토콜은 UDP를 기반으로 만든 전송계층 프로토콜인데, 특징은 아래의 것들이 있습니다.

  • 낮은 Handshaking connection 오버헤드를 가진다. 한번의 연결만 이뤄지면 해당 연결을 클라이언트가 캐싱해뒀다가 나중에 재사용이 가능할뿐더러, 연결 과정에서 piggy-bagging이 가능하기 때문에 연결 과정에서 데이터를 동시에 주고받을 수도 있습니다.
  • 다중 스트림을 지원한다. TCP의 경우 단일 스트림 상에서 흐름제어/혼잡제어를 하기 때문에 패킷 손실, 혹은 3-duplicated ACK이 발생하면 CWND가 깎여나가는 현상이 있었지만, QUIC 프로토콜부터는 다중 스트림으로 segment를 주고받기 때문에 하나의 스트림에서 막히면 다른 스트림에 영향을 끼치지 않는다. 이 덕분에 데이터 전송에 있어 성능 개선이 이루어졌다.
  • 혼잡제어가 유연하다. QUIC 프로토콜부터는 혼잡제어를 플러그인 형태로 적용이 가능하기 때문에 경우에 따라 혼잡제어 알고리즘을 교체할 수 있어졌다.

그리고 구글은 이를 이용해 HTTP/3를 구현하여 현재 사용중에 있다고 알려져있습니다.


🌲 Reference

러닝 HTTP/2

profile
Hi There 🤗! I'm college student majoring Mathematics, and double majoring CSE. I'm just enjoying studying about good architectures of back-end system(applications) and how to operate the servers efficiently! 🔥

0개의 댓글