Network

HTTP The Definitive Guide - 04. Connection Management

깡구_ 2022. 11. 7. 23:28

title: HTTP The Definitive Guide - 04. Connection Management
date: 2022-11-07
tags:

  • Network
  • HTTP



Introduction

HTTP The Definitive Guide




GDSC에서 해당 서적을 통해 HTTP를 공부하는 스터디에 참가하고 있다.
HTTP 이전의 내용은 개인적으로 공부하며 채울 예정이다.
이번 Chapter는 04장 Connection Management 이다.


Purpose

HTTP에서 TCP Connection을 어떻게 관리하는지를 파악한다.


TCP Connections

HTTP는 TCP/IP 기반으로 만들어진 Protocol이다.
TCP/IP에 대한 깊은 내용은 이전에 정리하였다.
Connection과 관련된 부분은 개인적으로 정리하였으나 포스팅을 하지 않았으며, 해당 내용과 서적을 이용한다.

TCP Connection을 통해 데이터를 교환하는 것은 아래 흐름을 따른다.

  • URL의 Host를 추출하여 DNS에 조회, IP Address를 가져온다.
  • IP Address와 Port를 이용하여 Server와 3-way Handshake를 통해 Connection을 맺는다.
  • Client와 Server는 서로 처리 가능한 양의 데이터를 전송한다. 자세한 처리는 생략한다.
  • 한쪽에서 Connection을 끊기를 희망할 경우, 4-way Handshake를 통해 Connection을 끊는다.

TCP는 데이터가 순서대로 전송이 되며 순서를 유지한 상태로 도착하는 것이 원칙이나, HTTP 2.0부터는 Multiplexed Stream의 도입으로 도착 순서가 바뀌어도 된다.

서적에서는 TCP에서 IP Address와 Port를 동시에 이용하여 식별한다고 나와 있으며, 이전에 나온 그림에서는 OSI 7 Layer를 이용한다. TCP/IP 4 Layer와 섞어서 설명이 되는 것으로 추정된다.
OSI 7 LayerTCP Segment가 Port, IP Packet이 IP Address, 그리고 DataLink Layer에서 MAC Address까지 이용한다.
TCP/IP 4 Layer를 기반으로 설명하더라도 Network Interface Layer에서 MAC Address가 추가된다.
총 3가지 정보를 이용하여 상대를 식별하며, 하나라도 달라진다면 상대방이 달라진다고 볼 수 있다.
IP Address만 다를 경우 상대방이 다르다고 식별하나, 하나의 Server가 여러 IP Address를 이용할 수도 있다. 실질적으로는 같은 상대방일 수 있으나, 우선 시스템 측면에서는 다르다는 것이다.

OS가 TCP Connection을 맺을 때 여러 방법이 있을 수 있으며, 서적에서는 일반적으로 사용하는 Socket을 통해 보여주었다.
이 또한 자세한 내용은 이전에 정리하였다.

TCP Performance Considerations

HTTP는 TCP의 바로 상위 Layer에 속하기에 TCP의 성능에 크게 영향을 받는다.
HTTP Transaction에서 지연이 발생하는 것은 아래와 같은 TCP의 문제일 가능성이 높다.

  • Cache에도 없는 URL의 IP Address를 받아오기 위해 DNS에 질의할 때
  • Server와 TCP Connection을 맺을 때 (3-way Handshake)
  • Server에 데이터를 전송할 때, Server가 받기까지 시간이 오래 걸릴 때
  • Server가 데이터를 받은 후, Response가 도착하기까지 시간이 오래 걸릴 때

이러한 문제들로 인하여 TCP와 관련된 지연은 크게 5가지로 나눌 수 있다.
고성능의 HTTP Application을 만들어야 한다면, 이후 작성하는 내용들을 숙지하고 있어야 한다.

TCP Connection Handshake Delays

초기 Connection을 맺을 때 발생하는 3-way Handshake로 인한 지연이다.
Server에 SYN Flag를 설정하여 보낸 후, Server는 이에 대한 응답으로 ACK Flag 설정 + SYN Flag를 설정하여 보낸다.
마지막으로 Server에게 ACK Flag를 설정하여 보내야 하며, Connection 이후 보내야 하는 Request를 함께 보낼 수 있다면 한 번에 보낸다.
크기가 작은 HTTP Transaction의 경우 50% 이상의 시간을 Connection을 맺는 데에 사용한다.

Delayed Acknowledgements

TCP는 데이터가 올바르게 전송되었는지 확인하기 위하여 ACK 응답을 받아야 한다.
ACK 응답 자체의 크기는 작으나, 많은 데이터가 오갈 때 이러한 자잘한 응답을 계속 보내는 것은 부담이 될 수 있다.
그렇기에 Piggyback 이라는 기법을 도입한다. Piggyback이란 바로 다음에 보낼 Packet에 ACK 응답을 함께 보내는 것이다.
최대 전송할 Packet의 개수가 50%까지 줄어들 수 있기에 이러한 기법을 도입하였다.
문제는 Piggyback으로 인하여 ACK 응답을 바로 보내지 않고, 보통 100~200ms를 기다린다.
기다리는 도중 보내야 할 Packet이 있다면 ACK 응답을 함께 보내며, 없다면 ACK 응답만 보낸다.
이러한 대기 시간으로 인하여 지연이 발생하며, 일부 OS는 이러한 대기 없이 바로 응답을 보내도록 수정할 수 있다.
대기 시간을 두지 않는다는 것은 예상보다 많은 Packet이 전달되는 것이기에 충분한 검토가 필요하다.

TCP Slow Start

TCP Connection이 언제 맺어졌는지에 따라 전송 속도가 달라진다.
갑작스레 최대 한도까지 데이터 전송을 하게 되면 과부하가 발생하거나 순식간에 네트워크가 혼잡해질 수 있다.
이를 방지하기 위하여 처음에는 Packet 1개, ACK 이후 2개, ACK 이후 4개 ... 방식으로 한도를 늘려나간다. 이를 Congestion Window를 연다고 한다.
먼저 Connection이 맺어진 것이 새로운 Connection보다 더 빠르다. 이 때문에 HTTP는 기존 Connection을 재사용하려고 하며, 이것이 Persistent Connection과 연관이 된다.

Nagle's Algorithms and TCP_NODELAY

위에 적은 Delayed Acknowledgements와 관련이 깊다.
Delayed Acknowledgements는 ACK 응답만 보내지 않기 위해 다른 Packet을 기다린다.
이와 비슷하게, 1Byte 데이터만 보내려고 할 수도 있으며, 동일하게 너무 많은 Packet을 전달하게 될지도 모른다.
그렇기에 Nagle's Algorithm은 최대한 데이터를 모아서 한 번에 보내기 위하여 사용되는 알고리즘이다.
다양한 알고리즘이 존재하나, 기본 이론은 다음과 같다. Packet의 데이터가 최대 크기에 도달하거나, 다른 Packet들이 모두 확인되었을 경우 전송한다.
Delayed Acknowledgements로 인하여 1차 지연이 되며, Nagle's Algorithm으로 인하여 2차 지연이 된다.
물론 Nagle's Algorithm 또한 TCP_NODELAY를 통해 비활성화할 수 있지만, 그만큼 많은 Packet이 전달될 수 있기에 주의해야 한다.

TIME_WAIT Accumulation and Port Exhaustion

TCP 4-way Handshake

TCP Connection이 끊어질 때, 4-way Handshake 과정을 거친다.
이때 Server가 전송하였으나 아직 받지 못한 데이터가 존재할 것을 대비하여 TIME_WAIT 상태로 대기한다.
2MSL 시간만큼 기다리며, 과거에는 2분이었으나 현재는 기술의 발전으로 훨씬 작다.
이 기간 동안 해당 Port는 다시 사용할 수 없다. 다시 사용한다면 3-way Handshake 과정의 Packet을 Server가 늦게 보낸 Packet이 도착한 것으로 인식할 수 있기 때문이다.
많은 TCP Connection이 동시에 끊어지면 해당 Port들은 다시 이용할 수 없으며, Port가 고갈되면 새로 Connection을 맺는 데에 시간이 걸린다.
물론 Port는 약 65K의 여유가 있으며, 초당 수백 개 이상의 TCP Connection이 끊어지지 않으면 이러한 문제는 발생하지 않는다.
그럼에도 동시에 많은 Connection을 유지하는 것은 OS 측면에서 이를 관리하기 위해 많은 Resource를 사용하기에 주의해야 한다.

HTTP Connection Handling

 

Connection Header

HTTP Message는 Proxy와 같은 장치들을 하나씩 거치면서 Server로 전달된다.
인접한 장치로 전달할 때만 사용하는 Option이 필요할 경우가 존재한다. 이때 Connection Header를 이용한다.
Connection Header에는 여러 정보가 들어갈 수 있으며, 아래 유형의 정보가 담긴다.

  • HTTP Header Field name - 다른 Header를 명시한다. 해당 Header는 다음 장치로 전달이 되면 제거된다.
  • Arbitrary Token Value - Connection에 대한 비표준 Option을 의미한다.
  • Close - 해당 Connection은 현재 Transaciton이 처리된 후, Connection을 유지하지 않고 끊을 것이라는 의미이다.

Transaction Delay

TCP의 특징으로 인하여 Delay가 발생한다. 아래와 같이 4개의 Resource가 필요할 때, 각각 다른 Connection을 요구하여 많은 Delay가 발생한다.

TCP Transaction Delay

Web Browser가 화면을 그리기 위한 정보일 경우, Resource가 하나씩 그려질 것이다.
이해는 잘 안되지만, 유저는 동시에 여러 Resource가 그려지는 것보다 하나씩 그려지는 것에서 더 느리다고 느낀다고 한다.
위 그림처럼 Resource를 가져오면 과거에는 잘 사용하였더라도, 현재는 답답해서 해당 Application을 사용하지 않을 것이다.
이러한 Delay를 줄이기 위한 여러 기법이 도입되었다.

Parallel Connection

Parallel Connection

HTTP 1.1에서 도입된 기법으로, 여러 Connection을 동시에 맺어 유지하는 것이다.
동시에 여러 Connection이 맺어지며, 특정 Connection에서 문제가 생기면 동일한 Server에 여러 Connection이 존재하기에 여유가 있는 Connection을 이용할 수 있다.

하지만 Parallel Connection이 항상 빠른 것은 아니다. Network 대역폭은 정해져있기 때문이다.
아무리 많은 Connection이 존재하더라도 정해진 대역폭을 넘는 데이터를 전달할 수 없다.
오히려 이러한 Connection때문에 발생하는 Overhead로 인하여 하나의 Connection을 이용하는 것보다 더 느릴 수도 있다.
또한 Server의 입장에서도 Parallel Connection이 반가운 것은 아니다. Server는 한정된 Resource를 이용하여 여러 Client에 데이터를 전달한다.
N개의 Client가 동시에 M개의 Connection을 맺는다면 Client 입장에서는 겨우 M개의 Connection이더라도, Server 입장에서는 NM개의 Connection을 유지해야 한다.
이 과정에서 많은 Memory는 물론 Resource 사용이 매우 커지며, 이로 인하여 N개의 Connection을 이용하는 것보다 더 느릴 수 있다.
이러한 이유로 인하여 동시에 맺을 수 있는 Parallel Connection의 개수를 적은 수로 한정하여 사용하는 편이다.

Persistent Connection

Persistent Connection

최근에 사용한 Memory를 다시 사용할 가능성이 높은 Temporal Locality와 비슷한 개념이 Network에도 존재한다.
Web Browser를 이용할 때 몇몇 Site를 주로 이용할 것이며, 이를 Site Locality라고 한다.
특정 Site를 자주 이용한다면 Connection을 끊지 않고 계속 유지하는 것이 Connection을 맺는 시간을 줄일 수 있을 것이다.
이 때문에 HTTP 1.1에서 Persistent Connection이라는 기법이 도입되었다.

Transaction이 종료되어도 Connection을 끊지 않고 유지하여 재사용하는 기법이다.
Nonpersistent Connection은 기존의 Connection과 같이 Transaction이 종료되면 Connection을 끊는다.
Connection을 새로 맺는 불필요한 Overhead가 줄어들며, 이미 열려있기에 Slow Start 문제가 발생하지 않는다.

물론 Persistent Connection이 항상 좋은 것은 아니다.
Persistent Connection이 도입되면서, 모든 Connection은 명시하지 않는 한 Persistent Connection을 맺도록 되어 있다.
Timeout이 없는 경우도 존재하며, 이를 관리하지 않는다면 너무 많은 Connection을 불필요하게 유지하여 Overhead가 발생할 것이다.
Proxy는 많은 Client와 Server를 중개해야 하기에 한 Server당 최대 2개의 Persistent Connection만 유지할 수 있다.

Parallel Connection vs Persistent Connection

Parallel Connection과 Persistent Connection 모두 장점과 동시에 단점이 존재한다.
두 가지를 조합하여 사용하는 것이 가장 효과적이다.
HTTP 1.1 당시의 Web Browser는 적은 수의 Parallel Connection을 Persistent Connection으로 맺어서 사용하였다.
서적은 HTTP 1.1까지만 나와 있으나, 현재 HTTP 2.0의 Multiplexed Stream의 도입으로 두 기법이 불필요해져 사용되지 않는다.

Keep-Alive

Keep-Alive

Keep-Alive는 HTTP 1.0이 발표된 이후 도입이 된 기법이며, HTTP 1.1에서 매우 유사한 Persistent Connection이 도입되었다.
Persistent Connection의 도입으로 HTTP 1.1부터는 Deprecated 되었으나, 서적을 집필했을 때는 여전히 일부 Browser와 Server는 Keep-Alive를 사용하였다.

Connection Header에 Keep-Alive Option을 명시해야 사용할 수 있으며, Keep-Alive를 유지하고 싶다면 Request에 해당 Option을 포함해야 한다.
또한 Body의 길이가 명시되어야 Keep-Alive를 유지할 수 있다.
Server가 Connection을 유지할 수 있다면 해당 Option은 다음 장치에 전달할 때도 계속 유지한다.
Server의 Response에 Keep-Alive Option이 없다면 Server가 이를 지원하지 않거나 더 이상 유지할 필요가 없다고 판단한 것이며, Client는 이를 확인 후 Connection을 끊는다.
Keep-Alive Header에는 Keep-Alive에 대한 상세한 정보가 들어있다. timeout 시간, Server가 최대로 받을 Transaction의 개수, 진단이나 디버깅 목적의 Value Attribute 등이 존재하며 필수는 아니다.
Proxy 혹은 Gateway는 Connection Header를 지원해야 사용할 수 있으며, 이를 모르는 Dumb Proxy는 Blind Relay 문제를 발생시킨다.
HTTP 1.0은 Keep-Alive를 지원하지 않기에 HTTP 1.0을 사용하는 장치에서의 Keep-Alive Option은 무시해야 하지만, 문제가 발생할 수 있음을 감수하고 이를 허용하기도 한다.

Dumb Proxy & Blind Relay

Blind Relay

Blind Relay는 Dumb Proxy가 Connection Header를 알지 못하여 발생하는 문제이다.
Client는 Keep-Alive Option을 명시하여 Request를 하여도, Dumb Proxy는 이를 모르기에 그저 하나의 Extension Header로 인식하여 그대로 Server에 전달한다.
Server는 Keep-Alive Option을 확인한 후, Connection을 유지하기로 결정하여 Keep-Alive Option을 명시한 Response를 보낸다.
다시 한번 Dumb Proxy는 이를 이해하지 못한 채 그대로 전달하고, Client는 Response를 확인하여 Connection을 유지하기로 한다.
하지만 Dumb Proxy는 이를 모르기에 Connection을 끊으려고 하고, Client와 Server 또한 Dumb Proxy의 동작을 모르기에 Connection을 유지하려고 한다.
Client가 Request를 보내더라도 Dumb Proxy는 이를 무시하며, Client와 Server는 아무런 동작 없이 Connection이 Timeout으로 끊어질 때까지 유지하게 된다.

Fixed Blind Relay

이러한 Blind Relay를 해결하기 위해 Proxy-Connection Header를 추가적으로 사용하게 되었다.
해당 Header를 보내면 Dumb Proxy는 이해하지 못하기에 Extension Header로 인식하고 그대로 Server에 전달한다.
Smart Proxy는 이를 이해하고 Connection Header로 바꾸어 Server에 전달한다.
Proxy-Connection Header는 정식으로 사용하는 Header가 아니기에 Server는 이를 인식하지 못한다.
Dumb Proxy가 그대로 전달하였기에 Server는 이를 이해하지 못하여 Connection을 유지하지 않으며, 덕분에 Blind Relay 문제가 사라진다.
Smart Proxy가 Connection Header로 바꾸어 전달하기에 Server는 Connection을 유지한다.

물론 이것이 완벽한 해결책은 아니다.
Dumb Proxy 혹은 Smart Proxy만 존재하면 위 방식을 통해 Blind Relay 문제를 해결할 수 있으나, 두 Proxy가 섞인다면 문제가 발생한다.
두 Proxy가 섞여 있기에 Server에 도착하기 전에 Smart Proxy로 인하여 Connection Header로 바뀌며, Client로 Response하는 과정에서 Dumb Proxy에게 해당 Header가 전달된다.

Pipelined Connection

Pipelined Connection

지금까지의 Connection은 Response를 받은 후 다음 Request를 보낼 수 있었다. HTTP 1.1에서 이를 개선한 방식인 Pipelined Connection(Pipelining)이 도입되었다.
Response를 기다리지 않고 순서대로 Request를 연속하여 보내는 것이다.
Persistent Connection에서만 이용 가능하며, Response는 Request의 순서와 동일하게 도착해야만 한다.
Client는 Connection을 언제든 끊거나 Server로 인하여 Connection이 끊어져도 다음 Request를 보낼 준비가 되어 있어야 한다.
단 POST와 같이 멱등하지 않은 Method의 경우, Request 도중 Server가 Connection을 끊으면 POST Request가 제대로 처리되었는지 모를 수 있다.
중복된 여러 Resource가 생성될 수 있기에 이러한 멱등하지 않은 Method는 Pipelining을 해서는 안 된다.

Connection Close

모든 장치는 언제든 TCP Connection을 끊을 수 있다.
Pipelined Connection을 이용하지 않는다면 Response를 확인하며 재시도할 수 있으나, Pipelined Connection은 꽤 복잡하다.
연속된 Request를 보내던 도중 Server와의 Connection이 끊어졌을 때, 어디까지 Request가 전송이 되었는지 정확히 알기 어렵다.
그렇기에 멱등하지 않은 Method을 사용하는 것이 위험하며, 끊어질 경우 Response를 받지 못한 Request부터 다시 전송하는 것이다.
또한, Server가 Response Packet을 만들던 도중 Connection이 끊긴다면 Response를 받은 데이터가 완전한지 알 수 없다.
그렇기에 Content-Length를 이용하여 데이터의 길이를 통해 완전한 데이터가 전달된 것인지 확인한다.

Graceful Connection Close

Full Close and Half Close

Connection을 끊는 것은 두 가지로 분류할 수 있다. Input Channel과 Output Channel을 동시에 끊는 Full Close, 한쪽만 먼저 끊는 Half Close이다.

Full Close만 지원하는 HTTP Application이 간혹 존재하나, Half Close를 지원하는 것은 매우 중요하다.
Output Channel을 먼저 끊는 것은 안전하다. 더 이상 데이터를 전달하지 않기에 이전에 보낸 데이터만 처리가 되면 양측 모두 Connection을 안전하게 끊을 수 있다.
하지만 Input Channel을 먼저 끊는 것은 위험하다. 상대방이 데이터를 전달하였으나, Input Channel이 끊겨있기에 무의미한 전송이 된다.
또한 이 경우 Connection Reset by Peer 문제가 발생한다. OS는 이러한 전달을 심각한 에러로 판단하여 처리되지 않은 Buffer에 저장된 데이터를 모두 지워버린다.
10개의 데이터를 보낸 후, 11번째 데이터를 받아야 하는데 Input Channel이 끊겨 있다면 이전까지 받은 10개의 데이터가 모두 삭제되어 무의미한 데이터 전달로 남게 된다.
그렇기에 먼저 Output Channel을 끊어 더 이상 데이터를 전달하지 않는다는 것을 알려준 후, 서로 모든 데이터를 받았다면 Input Channel을 끊는다.

이것을 Graceful Close라고 한다.

Conclusion

HTTP에서 TCP Connection을 효율적으로 관리하는 방법들에 대해 알아보았다.
HTTP 1.1의 내용이기에 현재 사용하는 HTTP 2.0에 대해서도 알아보아야 한다.