HTTP/1.1의 문제점
HTTP/1.1은 설계 자체가 논문을 주고 받기 위해 탄생한 기술이다. 따라서, html, css, js, 작은 사진 파일을 받기 위한 것이지, 큰 정보를 주고 받기 위해 탄생한 것이 아니다!
따라서 생겨난 단점이 존재한다.
1. HTTP/1.1 layer 상에서의 Head-of-Line Blocking 문제
Head-of-Line Blocking: Client가 보내는 페이로드에 대해 서버 측에서 보내는 페이로드가 와야 client가 다음 페이로드를 보낼 수 있기에 Client는 그 동안 새로운 요청을 보내지 못하고 기다려야 하는 문제이다.
2. 헤더가 중복되는 경우가 많고, 헤더가 기본적으로 너무 크다.
3. 페이로드가 5개로 분할이 되는 큰 요청이 먼저 오고, 작은 요청들이 뒤이어 온다면, HTTP/1.1에서는 그 요청이 끝날 때 까지 커넥션을 점유한다. 운영체제의 CPU 스케줄링 중 FIFO 와 같은 느낌으로, 뒤에 있는 작은 요청들은 큰 요청이 처리될 때 까지 막혀 있게 된다.
대안:
따라서 HTTP/1.1는 실제 서비스를 위하여 도메인 샤딩과 여러 커넥션을 만들어 연결하여 커넥션의 양으로 해결하였다. 또한 파이프라이닝으로 요청에 대한 응답을 기다리지 않고, 여러 요청을 보낸 후 이후에 순서대로 응답을 받았다.
하지만 이는 큰 네트워크 부하와 비효율을 초래하며, 근본적인 해결 방법이 아니다!
HTTP/2의 도래
따라서 HTTP/2가 탄생하였다. 기존 HTTP/1.1과의 차이점은 다음과 같다.
기본 특징:
1. HTTP/2는 기존 HTTP/1.1에서 페이로드 단위가 아닌 바이너리 프레임 단위로 나뉜다. 이 프레임은 HTTP/2에서 통신의 최소 단위이다.
2. HTTP/2는 요청을 프레임으로 만들어 스트림 id를 할당하고, 멀티플렉싱이 지원되기에 요청과 응답이 동시에 이뤄질 수 있다.
3. 클라이언트는 요청에 대한 우선순위를 할당할 수 있다. 이를 참고하여 서버는 요청을 처리할 수 있다.
4. HPACK이라는 헤더 압축 알고리즘이 존재한다. 이를 통해 큰 헤더를 줄일 수 있다.
5. 흐름 제어를 위한 프레임 상의 영역이 따로 존재한다.
6 서버 푸시 기능으로 사용자가 필요할 것 같은 정보를 미리 보내줄 수 있다.
바이너리 프레임:
HTTP/2에 속한 binary framing layer에서 요청이 Binary Frame 형태로 바뀐다. Binary Framing Layer는 여러 스트림의 프레임들을 인터리빙(커넥션에 끼워넣어 전송하기)하여 전송하고, 이는 Stream ID를 통해 올바른 스트림으로 서버 측에서 재조립된다.
프레임의 구조는 아래와 같다.

메세지는 프레임 단위로 쪼개서 전달해도 각 프레임의 헤더의 Stream ID 영역으로 구별할 수 있다. 그리고 재조립 또한 가능하기에 인터리빙이 가능하다.
즉, 스트림은 논리적인 흐름이라고 생각할 수 있고, 이것의 실질적인 구현은 바이너리 프레임의 스트림 id와 인터리빙으로써 이루어진다!
프레임은 HEADERS 프레임, DATA 프레임 등 다양한 종류가 있으며, 이는 Type 영역을 통하여 구현된다.
우선순위:
스트림 별 우선순위 처리 또한 프레임으로 관리한다. PRIORITY 프레임, 혹은 HEADERS 프레임의 영역에서 우선순위의 종속성, 그리고 가중치를 조절할 수 있다.
이는 클라이언트가 우선순위 지정 트리를 구성하며 구현이 된다.

이걸 서버가 확인하여 리소스에 맞게 우선순위를 설정한다. 그리고 우선순위와 weight에 맞는 대역폭을 할당한다.
스트림 종속성은 다른 Stream ID를 상위 요소로 참조하면 됨. 생략된다면 루트 스트림에 종속된다.
동일한 우선순위라면 가중치에 따라 달라진다.
사용자의 상호작용과 데이터에 따라 종속성을 변경하고, 가중치 또한 재할당이 가능하기도 하다.
헤더 압축 기능
HPACK 압축 알고리즘을 사용하여 큰 크기의 헤더를 압축한다. HPACK은 크게 2가지 방법을 통하여 압축한다.
1. 기본적으로 Indexed List를 공유하며, HEADERS 프레임에서 생략된 부분은 서버 측에서 암묵적으로 Indexed List의 값을 참고하여 처리한다. 이 중, Static 부분과 Dynamic 부분이 존재하여 Dynamic 테이블은 FIFO 방식으로 바뀌어 나간다.
2. 허프만 인코딩으로 자주 나올수록 더 작은 바이트를 할당한다.
아래 사진을 보면 요청 중에 달라진 부분만 전송하는 것을 볼 수 있다.

흐름 제어:
WINDOW를 설정하기 위하여 먼저 모든 스트림에서의 WINDOW_SIZE를 조절하는 SETTINGS 프레임을 교환한다.
이후, 스트림 당 흐름 제어는 WINDOW_UPDATE 프레임을 통하여 구현된다.

이는 DATA 프레임에만 적용된다.
흐름제어는 Hop-by-Hop이기에 client-proxy간, proxy-server 간 다르게도 설정할 수 있다.
단, 흐름 제어를 위하여 빌딩 블록으로서 WINDOW_UPDATE 프레임은 제공되지만, 구체적인 흐름 제어 구현은 개발자의 몫이다.
서버 푸시:
앞으로 필요할 요청을 사용자의 요청 없이도 보내주는 기능이다. PUSH_PROMISE 프레임으로 구현이 가능하지만, 웹브라우저에서는 사장되었다.
앱에서는 가능하다!
장점:
1. HTTP/2는 한 커넥션 내의 스트림 단위로 요청과 응답 처리를 하기에 한 커넥션에서 동시에 여러 요청을 처리할 수 있다.
2. t0라는 동시간 대에 동시에 요청과 응답이 오고 갈 수 있음. 즉, 멀티플렉싱이 가능함.
3. 인터리빙 기능으로 스트림 별 프레임 단위로 쪼개어져 프레임이 들어오는 즉시 처리가 된다. 위의 HTTP/1.1의 문제 상황인 5개의 큰 프레임으로 이루어져 있는 요청이 오고, 이후에 다른 요청들이 온다고 해도, 각각 스트림을 열어 처리하기에 작은 프레임도 바로 처리가 가능하다. 이걸 "라운드 로빈 방식"이라고 한다.
4. 클라이언트가 우선순위에 따른 요청을 보낼 수 있고, 이를 서버는 받아들여 서버의 상태에 따라 로직을 통하여 처리할 수 있다.
5. 헤더의 압축으로 네트워크 부하가 줄어든다.
6. 개발자의 역량에 따라 흐름 제어, 우선 순위 등을 구현하여 더욱 최적화시킬 수 있다.
7. HTTP/1.1는 TCP slow start 등의 스파이크 문제로 인하여 연결이 불안정하다. 이와 대비되어 HTTP/2는 커넥션 당 100개의 스트림을 멀티플렉싱할 수 있도록 설정되어 있음. 이 말은 부족하면 커넥션을 더 생성하긴 하지만 100개의 커넥션을 유지해야 하는 HTTP/1.1 대비 매우 좋다.
단점:
1. HTTP/2 또한 TCP 차원에서의 HOL blocking이 발생한다.
2. 개발자가 HTTP/2를 구현하기 위하여 더욱 많은 지식과 비즈니스 로직을 명확하게 이해해야 효율적인 서버를 개발할 수 있다.
실전 팁:
문제상황:
Concurrent한 스트림에는 문제가 있다. 스트림 A와 B가 있다고 할 때, 스트림 A로 처리할 수 있는 양보다 훨씬 많은 양의 데이터를 수신하여 수신자 버퍼가 가득 차 TCP receive window가 송신자를 제한하는 경우가 있다. 이러면 B는 스트림 A 때문에 흐름이 막히게 된다.
이 문제를 흐름 제어를 통하여 막을 수 있다. 스트림 당 최대 recevie window를 budget으로 설정하고, sender는 이 budget을 spend한다. receiver가 가능하다면 추가적인 버퍼를 sender에게 WINDOW_UPDATE 프레임을 통하여 알려줄 수도 있다. 단, budget이 소진되면 보내는 것을 중단한다.
이를 통해 스트림 단위로 요청을 처리하고, 스트림 단위로 수신 윈도우를 조절할 수 있도록 하여 어느 한 큰 요청에 의해 다른 요청이 막히는 것을 방지한다.
프록시:
HTTP/2는 프록시를 더욱 효과적으로 만들어준다고 한다. Hop-by-Hop 방식이기에 다양한 방식으로 흐름 제어가 가능할 수 있다. 또한, 프록시는 성능 또한 다양한 방식으로 끌어올릴 수 있다고 한다.
코드:
파이썬으로 간단하게 구현 가능하다.
클라이언트:
http-client에서 HTTPX 라이브러리로 구현하였다.
ssl 인증은 개인적으로 만든 인증서로 테스트하였다.
프로토콜에 따라 http1.1로도 요청을 보낼 수 있게 하였다.
import httpx
import ssl
import argparse
async def main(config: argparse.Namespace):
ssl_context = ssl.create_default_context(cafile=config.certfile)
protocol = {"http1": False, "http2": False}
protocol[config.protocol] = True
client = httpx.AsyncClient(
verify=ssl_context,
**protocol
)
response = await client.get(config.address)
print(response.text)
await client.aclose()
if __name__ == "__main__":
args = argparse.ArgumentParser()
args.add_argument("--certfile", type=str, required=True) # "./http2-client/cert.pem"
args.add_argument("--protocol", type=str, choices=["http1", "http2"], default="http2")
args.add_argument("--address", type=str, default="https://localhost:8000/")
args = args.parse_args()
import asyncio
asyncio.run(main(args))
서버:
프로토콜에 따라, ssl 유무에 따라 분기 처리를 해주었다. TLS 오류가 있다면, 에러를 raise해준다.
핸들러 부분에서 요청의 프로토콜을 보고 추가적인 처리를 해줄 수도 있다.
from fastapi import FastAPI
from fastapi.requests import Request
from fastapi.responses import HTMLResponse
app = FastAPI()
def check_tls(certfile: str, keyfile: str) -> bool:
import os
if not os.path.exists(certfile) and not str(certfile).endswith(".pem"):
print(f"Certificate file {certfile} not found")
return False
if not os.path.exists(keyfile) and not str(keyfile).endswith(".pem"):
print(f"Key file {keyfile} not found")
return False
return True
@app.get("/")
async def read_root(request: Request) -> HTMLResponse:
client_host = request.client.host
client_port = request.client.port
client_addr = f"{client_host}:{client_port}"
protocol = request.headers.get("X-Forwarded-Proto", "http")
http_version = request.scope.get("http_version")
print(f"Got connection: {protocol}/{http_version} from {client_addr}")
return HTMLResponse(content=f"Hello")
if __name__ == "__main__":
import hypercorn
import hypercorn.asyncio
config = hypercorn.Config()
config.bind = ["localhost:443", "localhost:80"]
config.certfile = "./http2-server/cert.pem"
config.keyfile = "./http2-server/key.pem"
config.alpn_protocols = ["h2", "http/1.1"]
if config.ssl_enabled:
print(f"Running on https://localhost:443 (CTRL + C to quit)")
else:
print(f"Running on http://localhost:80 (CTRL + C to quit)")
if "h2" in config.alpn_protocols:
print("HTTP/2 is enabled")
elif "http/1.1" in config.alpn_protocols:
print("HTTP/1.1 is enabled")
else:
print("HTTP/2 and HTTP/1.1 are disabled")
if not check_tls(config.certfile, config.keyfile):
raise("TLS configuration error")
import asyncio
asyncio.run(hypercorn.asyncio.serve(app, config))
추가적으로 hypercorn.Config() 부분에서 h2_max_concurrent_streams 등 다양한 추가 설정을 할 수 있다.
완전한 코드는 아래에서 확인할 수 있다.
https://github.com/ket0825/fssn-http2
GitHub - ket0825/fssn-http2: 파이썬을 이용한 간단한 HTTP/2 구현 과제
파이썬을 이용한 간단한 HTTP/2 구현 과제. Contribute to ket0825/fssn-http2 development by creating an account on GitHub.
github.com
참고:
https://www.cncf.io/blog/2018/07/03/http-2-smarter-at-scale/
HTTP/2: Smarter at scale
This guest post was written by Jean de Klerk, Developer Program Engineer, Google Much of the web today runs on HTTP/1.1. The spec for HTTP/1.1 was published in June of 1999, just shy of 20 years ago.
www.cncf.io
HTTP: HTTP/2 - High Performance Browser Networking (O'Reilly)
What every web developer must know about mobile networks, protocols, and APIs provided by browser to deliver the best user experience.
hpbn.co
'Data Engineering > Server' 카테고리의 다른 글
| [HTTP] Request와 Response 패킷 파헤치기 (1) | 2024.08.15 |
|---|---|
| [Server] REST API란 무엇일까? (3) | 2023.05.30 |