모니터랩 연구소 장원규 선임연구원 HTTP/1.1 은 변해가는 웹 서비스 환경에서 발생하는 상당한 규모의 문제들을 처리하는데 훌륭한 방법을 제시해주었고, 그 결과 성능적으로나 서비스적으로나 변해가는 웹 생태계에 걸 맞는 진화를 보여주었습니다. 이에 HTTP/2 는 HTTP/1.1 에서 다소 불안정하던 부분을 해소하고 웹 어플리케이션을 더 빠르고 효율적으로 만들어주는데 초점을 두어 현재까지 계속 발전해 오고 있습니다. HTTP2에서는 동일한 호스트에 여러 요청을 동일한 TCP 연결에 스트림이라는 단위로 다중화 할 수 있도록 되어있습니다. 흐름 제어는 예를 들어 하나의 스트림이 자원을 점유 해 버리는 것으로, 다른 스트림이 block되는 것을 방지 한다는 목적으로 가지고 있습니다. 그럼 http2 흐름제어에 대해 파헤쳐 보겠습니다.
– 목차 – 1. 개요 2. 흐름제어의 개념 3. connection레벨과 스트림 레벨 |
개요
앞서 기술한 바와 같이 HTTP2의 흐름제어는 하나의 스트림이 자원을 점유 해 버리는 것으로, 다른 스트림이 block되는 것을 방지 하기 위해 만들어졌다고 하였습니다.
구체적인 상황은 아래와 같습니다.
- 큰 파일의 통신이 대역을 전부 사용하여 다른 통신을 방해한다.
- 어떤 요청 처리에 서버가 여기에만 매달려 다른 요청을 서버가 처리해 줄 수 없게 된다.
- 고속의 업로드를 행하는 클라이언트와 저속으로 읽어 들이는 서버 사이에 낀 프록시가 조정을 위해 데이터를 모으고 있는 버퍼가 넘친다.
이러한 상황을 방지하기 위해 HTTP2는 연결 (TCP 연결를 뜻함)과 그 위에 다중화 된 스트림 (HTTP 1.1의 요청/응답에 해당됨) 2가지 측면에서 흐름 제어를 실시하는 구조를 갖추고 있습니다. 실제로 Window Size는 임계 값을 설정하고 이 범위 내에 있으면 데이터를 보낼 수 있고, 그 값이 소진된 경우 송신 측은 데이터 전송을 중지합니다. 수신 측은 자원이 회복 된 것을 WINDOW_UPDATE라는 프레임으로 통지하고 거기에 설정된 값만 Window Size를 회복하는 것으로, 발신자는 데이터 전송을 다시 할 수 있다는 것입니다.
위에서 언급한 프록시 같은 경우에 대응하기 위해 흐름 제어는 송신과 수신 양측에서 흐름 제어를 해제 할 수 없습니다. 또한 Window Size를 소비하는 것은 DATA Frame(http1.1의 body에 해당) 만이고, 다른 프레임이 수신되지 않는 것에 의해 제어를 잃는 것을 방지합니다.
- 흐름제어의 개념
http2 흐름제어는 2가지 frame을 사용합니다.
- SETTINGS FRAME (Initial Window Size)
- SETTINGS FRAME에는 스트림 레벨의 Window Size의 초기 값인 SETTINGS_INITIAL_WINDOW_SIZE를 포함 할 수 있습니다. 포함하지 않으면 기본값으로 2 ^ 16-1 (64KB-1) byte되고, 최대 값은 2 ^ 31-1 (2G-1)입니다. SETTINGS FRAME 통해 “초기 값을 나중에 변경” 할 수 있습니다
- WINDOW_UPDATE FRAME
- WINDOW_UPDATE FRAME에는 지정한 ID스크림 또는 연결자체로 소비된 Window Size의 회복을 통지합니다.
자, 그럼 예를 들어 흐름제어의 개념을 이해하도록 하겠습니다.
두 컴퓨터가 연결되어 있고, A가 B에게 파일을 보내는 상황으로 B의 처리능력을 근거로 흐름제어를 생각 해 보겠습니다.
연결 시 B는 A에게 SETTINGS FRAME으로 “Window Size의 초기 값은 100K”라고 보냈다고 예를 들어 보겠습니다. 파일을 전송하기 위해 양쪽 사이에 확립 된 스트림에서 B의 Initial Window Size는 100K라는 것입니다
A가 10K 파일을 B에게 보내는 경우는 Window Size보다 작기 때문에 단번에 보내는데 문제 없습니다.
다만 A가 300K 파일을 B에게 보내는 경우는 주의가 필요합니다.
Window Size는 100K로 정해져 있기 때문에, A는 파일 중 100 / 300K를 보낸 시점에서 전송을 일단 중지 할 필요가 있습니다.
중지한 건 좋지만, 언제 다시 전송을 할 수 있을까요?
B는 이미받은 데이터를 처리하고, 또한 추가 데이터를 수신 할 수 있게 되면 WINDOW_UPDATE로 Window Size 회복를 A에게 통지합니다.
B는 WINDOW_UPDATE로 갱신된 Window 크기에 맞는 범위에서 A는 데이터를 계속 보냅니다.
이렇게 B는 A로부터 데이터를 원할 하게 처리 할 수 있는 것입니다.
여기에서는 B의 처리 능력에 주목 한 흐름으로 설명했지만, 실제로는 프록시 버퍼 및 대역폭 등 여러 가지 요인을 바탕으로 제어 할 수 있기 때문에 이 설명은 어디까지나 일례라고 생각 해 주시기 바랍니다.
- connection레벨과 스트림 레벨
흐름 제어는 connection 레벨과 스트림 레벨 2가지가 있습니다.
왜냐하면 HTTP2는 하나의 connection (이것은 TCP 수준의 연결과 동의어)에 논리적 스트림이 다중화되어 있기 때문입니다.
앞의 예는 스트림 레벨에 대하여 설명한 예시입니다..
여기에서는 실제 HTTP2 의해 접근하기 위해, A와 B의 예로 connection 레벨의 개념을 넣어 설명합니다.
A는 B에게 동일한 연결에서 두 파일을 보내려고 생각합니다.
file1 (50K)
file2 (40K)
스트림 ID가 붙습니다. 클라이언트 (A)로부터 시작한 스트림 ID는 홀수이며, connection은 ID가 0 스트림과 같이 처리됩니다. connection 자체의 Initial Window Size 64K 고정입니다.
연결이 설정되면 먼저 SETTINGS Frame을 교환합니다 여기서 Initial Window Size를 80K로 설정합니다. 이 값은 그 후 생성되는 두 스트림의 Window Size의 초기 값(80K)으로 사용됩니다.
스트림에서 데이터가 전송되면 스트림의 Window Size와 함께 connection Window Size도 소비됩니다.
그러면 초기 상태에서 stream1이 단번에 50K 파일을 보냈다고 합시다. 전송이 끝난 시점에서 다음 상태가 됩니다.
스트림1에 데이터가 흐른 경우, 스트림1 (id = 1)상의 Window가 소비되는 동시에 동일한 만큼 connection의 Window도 소비됩니다.
스트림 레벨에서의 Window Size는 충분히 있지만, connection 레벨의 Window Size는 14K 밖에 남지 않았기 때문에 파일 전체를 보낼 수 없습니다.
이 경우, B의 connection레벨 (id = 0)의 WINDOW_UPDATE을받을 때까지 블록 합니다.
무사히 모든 보낼 수 있었습니다.
id = 0인 WINDOW_UPDATE는 connection 레벨의 Window Size를 회복합니다.
실제 데이터 스트림 레벨, connection 레벨 모두 Window Size의 범위 내에서 보내진 것입니다.
위에서 서술 한 바와 같이 SETTINGS Frame에 포함하는 Initial Window Size를 변경할 수 있는 것은 어디 까지나 스트림 레벨의 Initial Window Size뿐입니다.
conneciton 레벨의 Initial Window Size는 64K 그대로, 이것은 SETTINGS Frame에서는 변경 할 수 없습니다.
그러나 앞의 예와 같이 처음부터 conneciton Window Size를 크게 하고 싶은 경우도 있을 것입니다. 이 경우 WINDOW_UPDATE에서 큰 값을 보내두면 됩니다.
예를 들어, 연결 설정하고 즉시 WINDOW_UPDATE에서 100K를 보내면 conneciton Window Size를 164K로 할 수 있습니다.
그런데, SETTINGS FRAME은 언제든지 원하는 타이밍에 보낼 수 있습니다.
그러면 Initial Window Size를 중간에 바꾸면 어떻게 될까요.
예를 들어 다음과 같은 상황의 스트림을 생각해 보겠습니다.
- Initial Window Size가 20K에서 시작된 스트림
- 8K 데이터를 전송 한
- 3K WINDOW_UPDATE을 반환
- 4K 데이터를 전송 한
- 그 Initial Window Size를 30K로 변경
Initial Window Size를 보내기 직전은 8K – 3K + 4K = 9K 보낸 것이기 때문에
스트림의 Window Size는 11K되어 있을 것입니다.
이 상태에서 Initial Window Size를 30K로 변경한다는 것은 “초기 값이 30K 상태에서 데이터 송수신이 시작되었다는 것으로 한다” 라고 하는 행동입니다.
즉, 이 경우는 30K로 시작해서 9K 보냈다는 것으로 하기 때문에 다음 상태가 됩니다.
계산식은 다음과 같습니다.
- New Window Size = New Initial Window Size – (Current Initial Window Size – Current Window Size)
현재 Initial Window Size에서 현재 Window Size를 빼는 것으로 여기에서 소비 된 Window Size를 새로운 Initial Window 크기에서 소비 된 것으로 결정하는 것입니다.
이것을 모든 스트림에 대해서 계산 해 줄 필요가 있습니다.
이것을 근거로 다음과 같은 경우를 생각합니다.
- Initial Window Size가 64K에서 시작된 스트림
- 60K의 데이터를 전송 한
- 그 Initial Window Size를 16K로 변경
Initial Window Size를 변경하기 직전은 Window Size가 4K입니다.
여기서 Initial Window Size 업데이트하고 이전 식에 적용시켜 보겠습니다
Window Size = 16 – (64 – 4) = -44
마이너스가 되어 버렸습니다.
원래 데이터를 보내고 있는 것만으로는 Window Size가 전부 소비된 시점에서 전송을 차단하는 것이지만, Settings Frame에 의해 도중에 초기 값이 변경 됨으로써, Window Size가 마이너스가 될 가능성이 있습니다.
이 경우 44K 이상 WINDOW_UPDATE을 반환하고 Window Size가 플러스가 될 때까지 전송을 차단합니다.
RFC 7540(HTTP/2)의 “6.9.2. Initial Flow Control Window Size”의 후반부에 논의 되어 있습니다.