노트

HOL 블로킹과 HTTP/2 멀티플렉싱

Head-of-Line Blocking

프론트엔드#network#http#performance · 연결된 개념 6개

쉽게 말하면

HOL 블로킹은 계산대가 한 줄뿐인 마트에서 앞사람 계산이 길어지면 뒤에 선 사람이 모두 기다리는 상황이에요. 그래서 계산대를 여러 개 열거나, 한 계산대에서 여러 손님 물건을 번갈아 찍는 방법이 나왔어요.

비유의 한계 번갈아 찍어도 계산원 손이 하나라 물건이 많으면 다 같이 느려져요. HTTP/2도 요청 순서의 막힘은 풀었지만 대역폭은 나눠 쓰고, TCP 패킷 하나를 잃으면 모든 스트림이 함께 멈춰요.

HOL 블로킹(Head-of-Line Blocking)은 줄 맨 앞의 항목이 처리되지 않아 뒤에 있는 항목까지 처리되지 못하는 병목이다. 웹에서는 HTTP 버전마다 막히는 층이 다르다.

버전한 연결에서남는 막힘
HTTP/1.1응답이 와야 다음 요청요청 단위 막힘 → 연결을 여러 개 연다
HTTP/2여러 요청을 스트림으로 섞어 보냄TCP 패킷 손실이 모든 스트림을 멈춤
HTTP/3QUIC 스트림마다 따로 손실 복구전송 계층 막힘 해소

HTTP/1.1: 연결을 여러 개 열기

HTTP/1.1 연결 하나는 앞 요청의 응답을 기다리는 동안 새 요청을 보내지 않는다. 응답을 기다리지 않고 요청을 이어 보내는 파이프라이닝(pipelining)이 있지만, 응답은 요청 순서대로 와야 해서 막힘이 그대로 남는다. 버그 있는 프록시 문제도 있어 브라우저는 기본으로 꺼 둔다.

  • 그래서 브라우저는 서버 하나에 연결을 여러 개 연다. 명세는 상한 숫자를 정하지 않고 "보수적으로 열라"고만 한다. 브라우저들은 보통 도메인당 6개를 쓴다
  • 연결이 6개뿐이면 7번째 요청은 앞의 요청이 끝날 때까지 기다린다. SSE(Server-Sent Events) 연결이 탭마다 하나씩 자리를 차지해 막히는 것도 이 제한 때문이다
  • 리소스를 www1, www2 같은 여러 호스트 이름에 나눠 연결 수를 늘리는 도메인 샤딩(domain sharding)이 한때 쓰였다. HTTP/2에서는 쓸모가 없고 오히려 성능을 해친다. 연결마다 준비 비용이 들고(RTT와 연결 준비 비용), 연결이 많을수록 혼잡 제어가 따로 돌아 네트워크 효율이 떨어진다

HTTP/2: 한 연결에 섞어 보내기

HTTP/2는 메시지를 프레임으로 쪼개 한 연결 위에서 번호 붙은 스트림으로 섞어 보낸다(멀티플렉싱, multiplexing). 앞 응답이 오래 걸려도 다른 스트림은 계속 오간다. 요청에 우선순위를 줄 수 있고, 연결 수가 줄어 다른 트래픽과의 경쟁도 준다. gRPC가 HTTP/2 위에서 도는 이유도 이 멀티플렉싱이다.

그래도 두 가지는 남는다.

  • TCP 수준의 HOL 블로킹. TCP는 HTTP/2의 스트림을 모른다. 패킷 하나가 사라지거나 순서가 바뀌면, 그 패킷과 관계없는 스트림까지 재전송을 기다리며 멈춘다. HTTP/3은 UDP 위의 QUIC을 써서 스트림마다 따로 손실을 복구하므로, 손실은 해당 스트림에만 영향을 준다
  • 대역폭 경합(network contention). 연결은 하나여도 대역폭은 나눠 쓴다. LCP(Largest Contentful Paint) 리소스에 높은 우선순위를 줘도, 동시에 받는 리소스가 많거나 높은 우선순위를 남발하면 LCP 리소스가 늦어진다. 화면 밖 이미지는 지연 로딩으로 미루고, 우선순위는 fetchpriority로 꼭 필요한 곳에만 준다. 이 경합은 LCP 하위 구간의 Resource Load Duration을 늘린다

명세: RFC 9112 §9.4 — Concurrency · RFC 9113 §1 — HTTP/2: Introduction · RFC 9114 §1.1 — HTTP/3: Prior Versions of HTTP 출처: MDN — Head-of-line blocking · MDN — Connection management in HTTP/1.x: Domain sharding · web.dev — Optimize LCP: Reduce contention for network bandwidth

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 동기·비동기와 블로킹·논블로킹

    동기·비동기는 결과를 언제 어떻게 받느냐, 블로킹·논블로킹은 기다리는 동안 호출한 스레드가 멈추느냐의 문제다.

  • 커넥션 드레이닝과 무중단 재시작

    배포 중 서버를 재시작하는 몇 초 동안 로드밸런서가 그 서버로 요청을 보내면 502가 난다. 먼저 로드밸런서에서 빼고(드레이닝), 진행 중인 요청을 마친 뒤 재시작하고, 준비되면 다시 넣는다.

  • HTTP와 HTTPS

    HTTPS는 HTTP(Hypertext Transfer Protocol) 메시지를 TLS(Transport Layer Security)로 감싸 보내는 방식이다. HTTP 자체는 바뀌지 않고, 그 아래에 암호화 계층이 하나 끼어든다. 기본 포트는 HTTP가 80, HTTPS가 443이다.

  • 업스트림과 다운스트림

    흐름의 방향을 강물에 빗댄 말이다. 다만 무엇의 흐름을 기준으로 하느냐에 따라 방향이 반대로 쓰인다. 서비스 간 호출을 말할 때는 요청이 흘러가는 쪽을 기준으로 다운스트림 = 내가 호출하는 쪽, 업스트림 = 나를 호출하는 쪽이라 부르는 경우가 많다. 아래 설명은 이 용법을 따른다.

  • loading 경계와 레이아웃 끌어올리기

    Next.js App Router에서 loading.tsx는 같은 세그먼트의 page와 그 아래(하위 layout 포함)를 suspense로 감싸지만, **같은 세그먼트의 layout은 감싸지 않는다**. 그래서 데이터를 기다릴 필요가 없는 공통 UI(탭바, 필터 헤더)를 page에서 layout으로 끌어올리면 스켈레톤에 덮이지 않고 처음부터 실제 모습으로 보인다.

보기 옵션