TT Lab
시작하기
배우기 러닝패스 코스

실시간 통신 — WebSocket·gRPC 스트리밍·WebRTC

HTTP/2 — 연결 하나에 요청을 겹쳐 싣다

TT Lab 에서 이어서 보기

한 줄 요약

HTTP/2 는 연결 하나에 요청 여럿을 스트림으로 겹쳐 싣고 헤더를 압축하지만, 그 대가로 모든 스트림이 연결 하나의 흐름 제어 창과 TCP 바이트 흐름을 나눠 씁니다.

왜 이게 필요했나

HTTP/1.1 의 연결은 한 번에 요청 하나만 처리합니다. 명세에는 응답을 기다리지 않고 요청을 이어 보내는 파이프라이닝이 있지만, 응답은 요청 순서대로만 돌아와야 해서 앞의 느린 응답이 뒤를 모두 붙잡습니다. 이것이 애플리케이션 수준의 머리 막힘(head-of-line blocking)입니다. 그래서 브라우저와 클라이언트 라이브러리는 파이프라이닝 대신 연결을 여러 개 여는 쪽을 택했고, 연결마다 TCP 핸드셰이크와 TLS 핸드셰이크, 혼잡 창의 느린 출발을 따로 치렀습니다. 요청마다 똑같이 반복되는 쿠키와 인증 헤더도 매번 평문으로 다시 보냈습니다.

RFC 9113 의 HTTP/2 는 이 두 가지 낭비를 겨냥합니다. 연결은 하나로 두고 그 위에 독립된 스트림을 여럿 열며, 헤더는 연결 단위의 상태를 가진 압축(RFC 7541 HPACK)으로 줄입니다. gRPC 는 이 위에서 돌고, 스트리밍 응답이 갑자기 멈추는 장애의 상당수가 여기서 설명하는 흐름 제어 창에서 납니다.

연결을 여러 개 여는 길과 한 연결에 겹쳐 싣는 길

본문은 HTTP/1.1 의 낭비를 둘로 짚고 HTTP/2 가 그 둘을 겨냥했다고 말한다. 파이프라이닝이 막힌 이유와 연결을 여러 개 열 때의 비용을 HTTP/2 의 해법과 나란히 놓는다.

  • HTTP/1.1: 연결을 여러 개 연다파이프라이닝은 응답이 요청 순서대로만 돌아와야 해서 앞의 느린 응답이 뒤를 모두 붙잡는다. 그래서 연결을 여러 개 여는 쪽을 택했고, 연결마다 TCP 핸드셰이크와 TLS 핸드셰이크와 혼잡 창의 느린 출발을 따로 치른다. 쿠키와 인증 헤더는 요청마다 평문으로 다시 보낸다.
  • HTTP/2: 연결 하나에 스트림을 여럿 연다연결은 하나만 두고 그 위에 독립된 스트림을 연다. 서로 다른 스트림의 프레임은 한 연결 안에서 섞여 흐른다. 헤더는 연결 단위의 상태를 가진 압축(HPACK)으로 줄이고, 같은 연결에서 같은 헤더를 두 번째 보낼 때는 테이블 번호 하나로 줄어든다.

여기서 구분할 것 대가가 있다. 모든 스트림이 연결 하나의 흐름 제어 창과 TCP 바이트 흐름을 나눠 쓴다. 그리고 HPACK 의 이득은 연결을 오래 쓰는 데서 오며, 연결을 새로 열면 테이블도 비어서 처음부터 다시 커진다.

잠깐, 예측해 보세요 요청마다 새 연결을 열어서 HTTP/2 로 보내는 클라이언트가 있다. 본문 기준으로 이 클라이언트는 HTTP/2 가 겨냥한 두 낭비 가운데 무엇을 줄이지 못할까?

설명 확인 · 채점 없는 자가 점검

둘 다 거의 줄이지 못한다. 연결을 열 때마다 핸드셰이크 비용을 다시 치르고, 새 연결에서는 HPACK 테이블이 비어 있어서 두 번째부터 번호 하나로 줄어드는 이득도 얻지 못한다. 본문이 말하듯 압축의 이득은 연결을 오래 쓰는 데서 온다.

근거 문서

어떻게 동작하나

HTTP/2 의 모든 것은 프레임입니다. 4.1절의 프레임 머리는 9바이트로 고정되어 있습니다.

필드 크기 뜻
Length 24비트 머리를 뺀 본문 길이. 기본 상한은 16,384(SETTINGS_MAX_FRAME_SIZE)
Type 8비트 DATA 0x0, HEADERS 0x1, RST_STREAM 0x3, SETTINGS 0x4, PING 0x6, GOAWAY 0x7, WINDOW_UPDATE 0x8 …
Flags 8비트 END_STREAM, END_HEADERS 처럼 종류마다 뜻이 다르다
R + Stream Identifier 1 + 31비트 예약 비트는 보낼 때 0, 받을 때 무시한다

클라이언트가 여는 스트림은 홀수, 서버가 여는 스트림은 짝수 번호이고, 0번은 연결 전체의 제어(SETTINGS·PING·GOAWAY)에 씁니다. 요청 하나는 HEADERS 프레임으로 시작해 END_STREAM 플래그로 끝나고, 서로 다른 스트림의 프레임은 한 연결 안에서 얼마든지 섞여 흐릅니다. 연결은 늘 클라이언트의 서문(PRI * HTTP/2.0 으로 시작하는 24바이트)과 양쪽의 SETTINGS 로 시작하고, HTTP/2 를 쓰기로 정하는 방법은 TLS 에서는 ALPN 의 h2, 평문에서는 3.3절의 사전 지식(prior knowledge)입니다.

동시에 열 수 있는 스트림 수는 받는 쪽이 SETTINGS_MAX_CONCURRENT_STREAMS 로 알립니다. 6.5.2절은 이 값의 초깃값이 무제한이고, 100 보다 작게 두지 말 것을 권합니다. 여기서 흔한 함정이 나옵니다. 상대의 첫 SETTINGS 가 도착하기 전에는 한도를 모르므로, 서문을 보내자마자 요청을 몰아 보내면 한도를 넘길 수 있습니다.

흐름 제어(5.2절, 6.9절)는 DATA 프레임에만 걸립니다. 창은 스트림마다 하나, 연결 전체에 하나 있고, 둘 다 65,535 바이트로 시작합니다. 보내는 쪽은 두 창 가운데 작은 쪽만큼만 보낼 수 있고, 받는 쪽이 WINDOW_UPDATE 로 창을 돌려줘야 더 보냅니다. SETTINGS_INITIAL_WINDOW_SIZE 로 바꿀 수 있는 것은 스트림 창의 초깃값뿐이고, 연결 창은 WINDOW_UPDATE 로만 넓어집니다. 그래서 스트림 창을 100,000 으로 알려도 첫 응답은 65,535 바이트에서 멈춥니다. 창을 돌려주지 않는 클라이언트 하나가 있으면 큰 응답 하나가 연결 창을 다 쓰고, 같은 연결의 3바이트짜리 응답까지 기다리게 됩니다.

HPACK 은 61개 항목의 정적 테이블과, 연결마다 하나씩 쌓이는 동적 테이블(기본 4,096 바이트)을 씁니다. 같은 연결에서 같은 헤더를 두 번째 보낼 때는 테이블 번호 하나로 줄어듭니다. 연결을 새로 열면 테이블도 비어서 처음부터 다시 커집니다. 압축의 이득은 연결을 오래 쓰는 데서 옵니다.

현장에서 만나는 모습

HTTP/2 로 바꾸자 느린 페이지는 빨라졌는데, 모바일 망에서는 오히려 나빠졌다는 보고가 흔합니다. HTTP/1.1 의 연결 여섯 개는 서로 독립이라 하나에서 패킷이 사라져도 나머지 다섯은 흐릅니다. HTTP/2 의 연결 하나는 스트림이 몇 개든 TCP 입장에서는 바이트 흐름 하나라서, 세그먼트 하나를 다시 보내는 동안 모든 스트림이 섭니다. 다음 모듈이 이 이야기를 이어 갑니다.

또 하나는 gRPC 스트림이 몇 초씩 멈추는 장애입니다. 받는 쪽 애플리케이션이 메시지를 늦게 꺼내면 라이브러리가 창을 늦게 돌려주고, 보내는 쪽은 창이 닫혀 기다립니다. 네트워크는 한가한데 처리량이 바닥인 모습으로 보입니다.

받는 쪽이 느리면 네트워크가 한가해도 멈춘다

본문이 현장 장애로 꼽는 gRPC 스트림 멈춤을 흐름 제어 창의 움직임으로 풀어 쓴다. 이 흐름 제어는 DATA 프레임에만 걸리고, 창은 스트림마다 하나와 연결 전체에 하나가 있다.

  1. 받는 애플리케이션이 메시지를 늦게 꺼낸다읽는 쪽 코드가 메시지를 늦게 꺼낸다. 네트워크의 문제가 아니라 소비 속도의 문제이다.
  2. 라이브러리가 창을 늦게 돌려준다보내는 쪽은 받는 쪽이 WINDOW_UPDATE 로 창을 돌려줘야 더 보낼 수 있다. 두 창은 모두 65,535 바이트로 시작하고, 보내는 쪽은 둘 중 작은 쪽만큼만 보낼 수 있다.
  3. 보내는 쪽의 창이 닫혀 기다린다처리량은 바닥인데 네트워크는 한가해 보인다. 창을 돌려주지 않는 클라이언트 하나가 있으면 큰 응답 하나가 연결 창을 다 쓰고, 같은 연결의 3바이트짜리 응답까지 기다리게 된다.

여기서 구분할 것 연결 창은 SETTINGS_INITIAL_WINDOW_SIZE 로 바뀌지 않는다. 그 설정이 바꾸는 것은 스트림 창의 초깃값뿐이고 연결 창은 WINDOW_UPDATE 로만 넓어진다. gRPC 스트림이 멈추는 사례는 레슨 본문의 서술이다.

잠깐, 예측해 보세요 큰 응답을 받는 스트림 A 의 WINDOW_UPDATE 를 클라이언트가 보내지 않는다. 같은 연결에서 스트림 B 의 3바이트 응답은 B 의 스트림 창이 넉넉해도 늦어질 수 있을까?

설명 확인 · 채점 없는 자가 점검

늦어질 수 있다. 보내는 쪽은 스트림 창과 연결 창 가운데 작은 쪽만큼만 보낼 수 있다. A 의 큰 응답이 연결 창을 다 쓰면 B 의 창이 넉넉해도 연결 창에서 막힌다.

근거 문서

다음 실습에서 할 것

h2 라이브러리로 프레임 머리를 읽고, HPACK 크기를 재고, 한 연결에 요청을 겹쳐 싣는 클라이언트를 만듭니다. 그다음 동시 스트림 한도를 지키게 고치고, 창이 닫히는 바이트 수를 16,384·임의 값·100,000 세 경우로 재서 연결 창의 존재를 숫자로 확인합니다. 마지막에는 TCP 가 한 번 멈추는 중계기를 두고 HTTP/2 와 HTTP/1.1 의 작은 응답이 언제 끝나는지 견줍니다.