HOL 블로킹(Head-of-Line Blocking)은 줄 맨 앞의 항목이 처리되지 않아 뒤에 있는 항목까지 처리되지 못하는 병목이다. 웹에서는 HTTP 버전마다 막히는 층이 다르다.
| 버전 | 한 연결에서 | 남는 막힘 |
|---|---|---|
| HTTP/1.1 | 응답이 와야 다음 요청 | 요청 단위 막힘 → 연결을 여러 개 연다 |
| HTTP/2 | 여러 요청을 스트림으로 섞어 보냄 | TCP 패킷 손실이 모든 스트림을 멈춤 |
| HTTP/3 | QUIC 스트림마다 따로 손실 복구 | 전송 계층 막힘 해소 |
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