카테고리 없음

HTTP 완벽 가이드 - 북 리딩 스터디 02

DungDung.dev 2025. 3. 16. 20:18

 4장 커넥션 관리는 TCP/IP와 관련된 내용이다. 커넥션에서 발생할 수 있는 지연 상황과 해결방법, 한계 등을 설명한다.

 

1주차 진행 내용

더보기

1. HTTP: 웹의 기초

04. 커넥션 관리

 

TCP 커넥션

 TCP/IP는 패킷 교환 네트워크 프로토콜들의 계층화된 집합으로 메시지를 안전하고 순서대로 전달하는 것을 보장한다. HTTP 커넥션은 몇몇 사용 규칙을 제외하고 TCP 커넥션에 불과하다. 

 

 TCP는 IP패킷으로 데이터를 전송한다.(TCP는 세그먼트라는 단위로 데이터 스크림을 잘게 나누고, 세그먼트를 IP 패킷에 담아 데이터를 전달) HTTP는 프로토콜 스택에서 최상위 애플리케이션 계층으로 HTTP가 메시지를 전송하고자 할 경우, 현재 연결되어 있는 TCP 커넥션을 통해서 메시지 데이터의 내용을 순서대로 보낸다. 이 과정은 HTTP 프로그래머에게 보이지 않는다. 하지만 보이지 않아도 개념을 알고있어야 더 많은 것을 이해할 수 있다.

 

 컴퓨터는 <발신지 IP 주소> <발신지 포트> <수신지 IP 주소> <수신지 포트> 이 네가지 값으로 유일한 커넥션(서로 다른 구 개의 TCP 커넥션은 네가지 주소 구성요소의 값이 모두 같을 수 없다.) 서로 다른 구 개의 TCP 커넥션은 네가지 주소 구성요소의 값이 모두 같을 수 없다. ()을 생성하고 TCP 는 포트 번호를 통해서 여러개의 커넥션을 유지한다. 

 

TCP의 성능에 대한 고려

 HTTP는 TCP 바로 위에 있는 계층이기 때문에 HTTP 트랜잭션의 성능은 그 아래 계층인 TCP 성능에 영향을 받는다. 클라이언트나 서버가 너무 많은 데이터를 내려받거나 복잡하고 동적인 자원들을 실행하지 않는 한, 대부분 HTTP 지연은 TCP 네트워크 지연때문에 발생한다. 

 

[지연 시간]

1. 클라이언트가 URL에서 DNS연결을 통해 웹 서버의 IP 주소와 포트 번호를 알아내는 시간

2. 새로운 TCP 커넥션을 생성할 때 요청을 보내고 서버가 커넥션 허가 응답을 회신하기를 기다리는 시간

3. 요청 메시지가 인터넷을 통해 전달되고 서버에 의해서 처리되는 시간

4. 웹 서버가 HTTP 응답을 보내는 시간

 

* 고성능의 HTTP 소프트웨어를 개발하는 것이 아니라면 건너뛰어도 좋다고 했지만 개인적으로 재밌는 부분이라 요약에 추가했다.

 

· TCP 커넥션 핸드셰이크 지연 (새로운 TCP 커넥션을 열 때 연속으로 IP패킷을 교환하는 과정이다.)

1. 클라이언트는 플래그 'SYN'을 가진 작은 TCP패킷을 서버에 보낸다.

2. 서버가 요청을 허가했다는 의미의 'SYN'과 'ACK'를 TCP 패킷을 클라이언트에게 보낸다.

3. 서버에게 'ACK'확인 응답과 TCP연결 목적인 데이터를 함께 전송한다.

 

 TCP의 ACK 패킷은 HTTP 요청 메시지 전체를 전달할 수 있을 만큼 큰 경우가 많고, 많은 HTTP 서버 응답 메시지는 하나의 IP 패킷에도 담길 수 있다. (IP 패킷: 인터넷에서는 수백 바이트, 로컬-LAN상에서는 1500바이트 정도의 크기)

 

·  확인 응답 지연

 인터넷 자체가 패킷 전송을 완벽히 보장하지는 않기 때문에(통신 장애) 자체적인 확인 체제를 가진다. TCP는 같은 방향으로 송출되는 데이터 패킷에 확인 응답을 편승시킨다. 이때 데이터 패킷에 현승되는 경우를 늘리기 위해서, 확인 응답 지연 알고리즘을 구현한다. 송출할 확인응답을 특정 시간동안 버퍼에 저장해두고 편승시킬 송출 데이터 패킷을 찾는 것인데 안타깝게도 요청과 응답 형식으로 이루어진 HTTP 동작에서는 확인 응답이 송출 데이터 패킷에 편승할 기회를 감소시킨다. 막상 편승할 패킷을 찾으려고 하면 해당 방향으로 송출될 패킷이 많지 않기 때문에, 확인 응답 지연으로 인한 지연이 자주 발생한다. 

 

·  TCP 느린 시작(slow start)

 TCP 커넥션은 시간이 지나면서 자체적으로 튜닝되어서, 처음에는 커넥션의 최대 속도를 제한하고 데이터가 성공적으로 전송됨에 따라서 속도 제한을 높여나가는 것을 TCP의 느린 시작이라고 부른다. 이는 인터넷의 급작스러운 부하와 혼잡을 방지한다. TCP의 느린 시작은 한 번에 전송할 수 있는 패킷의 수를 제한한다. (성공하는 시점에 2개의 패킷을 추가로 전송할 권한을 얻는다.)


· 
네이글(Nagle) 알고리즘과 TCP_NODELAY

 TCP가 작은 크기의 데이터를 많은 수의 패킷으로 전송한다면 네트워크 성능은 크게 떨어진다. (반사회적 행동이라는 것인데 분노가 느껴진다.) 이를 방지하기 위해 패킷을 전송하기 전에 많은 양의 TCP 데이터를 한 개의 덩어리로 합치는 것을 네이글 알고리즘이라고 한다. 

 네이글 알고리즘은 세그먼트가 최대 크기(위에 적힌 IP 패킷 크기)가 되지 않으면 전송을 하지 않는다. 다른 모든 패킷이 확인 응답을 받았을 경우에 전송을 허락하지만 아직 전송 중이면 버퍼에 저장하고 함께 전송한다. 여기에서 크기가 작은 HTTP 메시지는 최대 크기를 채우지 못하기 때문에 지연이 발생한다. 확인응답 지연 알고리즘과 함께 쓰이면, 네이글 알고리즘은 확인 응답이 도착할 때까지 데이터 전송을 멈추고 확인응답 지연 알고리즘은 확인 응답을 지연시켜 형편없이 동작하게 된다.

 

·  TIME_WAIT의 누적과 포트 고갈

 TIME_WAIT 포트 고갈은 실제 상황에서는 문제를 잘 발생시키지 않지만, 성능 측정 시에 심각한 성능 저하를 발생시킨다. TCP 커넥션 종단에서 커넥션을 끊으면, 커넥션의 IP 주소와 포트 번호를 메모리의 control block에 기록해 놓는데 이 정보는 같은 주소와 포트 번호를 사용하는 새로운 TCP 커넥션이 일정 시간동안에는 생성되지 않게 하기 위한 것이다. 이는 이전 커넥션과 관련된 패킷이 그 커넥션과 같은 주소와 포트를 가지는 새로운 커넥션에 삽입되는 문제를 방지한다. (현대에는 빠른 라우터 덕분에 커넥션이 닫힌 후 중복되는 패킷이 생기는 경우는 거의 없다.) 만약 이전 커넥션의 패킷이 그 커넥션과 같은 연결 값으로 생성된 커넥션에 삽입되면, 패킷은 중복되고 TCP 데이터는 충돌한다. 성능 측정 시 발신지 포트만 변경하여 클라이언트에서 새로운 커넥션을 생성하고 서버가 초당 500개 이상의 트랜잭션을 감당하지 못하면 TIME_WAIT  포트고갈이 일어나는데 장비를 더 많이 사용하거나 가상 IP 주소를 사용하면 된다.

 

HTTP 커넥션 관리

* 홉별(hop-by-hop)에서 홉은 각 서저를 의미하며 홉별은 특정 두 서버 간에만 영양을 미치고 다른 서버 간에는 영향을 미치지 않음을 뜻한다.

 

 HTTP는 클라이언트와 서버 사이에 프락시 서버, 캐시 서버 등과 같은 중개 서버가 놓이는 것을 허락한다. HTTP Connection 헤더 필드는 커넥션 토큰을 쉼표로 구분하여 가지고 있으며, 그 값들은 현재 커넥션만을 위한 정보이므로 다음 커넥션에 전달하면 안 되며 다른 곳으로 전달하는 시점에 삭제되어야 한다. Connection 헤더에 기술되지 않더라고 홉별 헤더인 것들도 있다.

 

순차적인 트랜잭션 처리에 의한 지연 

 커넥션 관리가 제대로 이루어지지 않으면 TCP성능이 매우 안 좋아질 수 있다. 각 트랜잭션이 새로운 커넥션을 필요로 한다면, 계속해서 커넥션을 맺는데 발생하는 지연과 느린 시작 지연이 발생할 것이다. 순차 처리로 인한 지연은 심리적 지연도 발생시키는데 사용자는 여러 개의 이미지가 동시에 로드되는 것을 더 좋아하기 때문이다. (텅 빈 웹 페이지는 답답하다) 이 책은 이런 부분을 개선하는 기술 네 가지를 설명한다.

 

병렬 커넥션

 HTTP는 클라이언트가 여러 개의 커넥션을 맺음으로써 여러 개의 HTTP 트랜잭션을 병렬로 처리할 수 있게 한다. 각 커넥션의 지연 시간을 겹치게 하면 총 지연 시간을 줄일 수 있고, 클라이언트도 나머지 객체를 내려받는 데에 남은 대역폭을 사용할 수 있다. 이는 사용자가 내려받는 상황을 볼 수 있기 때문에 더 빠르다고 느낄 수 있다.

 하지만 병렬 커넥션이 항상 더 빠르지는 않다. 클라이언트의 대역폭이 좁을 때는 대부분의 시간을 데이터 전송 하는 데만 쓰이기 때문에 장점이 거의 없어진다. 또한 다수의 커넥션은 메모리를 많이 소모하고 성능 문제를 발생시킨다. 백 명의 사용자가 각각 100개의 커넥션을 맺으면 서버는 총 10,000개의 커넥션을 떠안는다. 이렇기 때문에 대부분 6~8개의 병렬 커넥션만 허용한다. 

 

지속 커넥션

 보통 웹 클라이언트는 같은 사이트에 여러 개의 커넥션을 맺는다. 이것을 사이트 지역성(site locality)라 한다. 지속 커넥션은 처리가 완료된 후에도 TCP 커넥션을 유지하여 앞으로 있을 HTTP 요청에 재사용할 수 있다. TCP의 느린 시작으로 인한 지연 시간을 피함과 동시에 커넥션을 맺고 끊는 데 필요한 작업이 없음으로 더 빠르게 데이터를 전송할 수 있다. 

 하지만 잘못 관리할 경우, 계속 연결된 상태로 있는 수많은 커넥션이 쌓이 될 것이다. 이는 불필요한 리소스 소모를 발생시킨다. 

 

  * 지속 커넥션은 병렬 커넥션과 함께 사용될 때에 가장 효과적이다. 애플리케이션은 적은 수의 병렬 커넥션만을 맺고 그것을 유지한다.

 

HTTP/1.0+의 Keep-Alive 커넥션

 설계상의 문제는 HTTP/1.1.에서 수정 되었지만 아직 많은 클라이언트와 서버는 이 초기 keep-alive 커넥션을 사용하고 있다. 

 클라이언트는 커넥션을 유지하기 위해서 요청에 Connection:Keep-Alive 헤더를 포함시킨다. 서버가 그 다음 요청에도 이 커넥션을 통해 받고자 한다면, 응답 메시지에 같은 헤더를 포함시킨다. 클라이언트는 Connection:Keep-Alive가 없으면 응답 메시지가 전송된 후 서버가 커넥션을 끊을 것이라 추측한다. 이 헤더는 커넥션을 유지하기를 바라는 요청일 뿐이다. 

 언테티 본문이 정확한 Content-Length 값과 함께 멀티파트 미디어형식(multipart media type)을 가지거나 청크 전송 인코딩(chunked transfer encoding)으로 인코딩 되어야 한다. 커넥션이 끊어지기 전에 엔터티 본문의 길이를 알 수 있어야 커넥션을 유지할 수 있기 때문이다.

 

< 멍청한(dumb) 프락시 >

 멍청한 프락시는 홉별헤더를 삭제하지 않고 다음 프락시에 전달한다. 다음은 멍청한 프락시가 커넥션을 망치는 상황이다.

1. 클라이언트가 멍청한 프락시에 Connection:Keep-Alive 헤더와 함께 메시지를 보내고, 커넥션을 유지하길 요청한다.

2. 멍청한 프락시는 그대로 다음 서버로 전달한다.

3. 서버는 프락시가 커넥션을 유지하자고 이해하고 Connection:Keep-Alive 헤더를 포함해서 응답한다.

4. 멍청한 프락시는 또 그대로 클라이언트에 요청을 전달한다. 이제 클라이언트와 서버는 각각 커넥션을 프락시와 유지하고 있다고 생각하고 프락시는 이해하지 못한 상황이 되었다.

5. 클라이언트는 서버가 커넥션을 끊기를 기다리고 서버는 자신과 커넥션을 유지하는 것으로 알기때문에 커넥션을 끊지 않는다. 프락시는 자신의 양쪽에서 커넥션이 끊기기를 기다리고 있는 클라이언트와 서버를 인식하지 못하고 계속 커넥션이 끊어지기를 기다린다.

6. 이런 상황에서 다음 요청을 보내기 시작하면, 프락시는 같은 커넥션에서 다른 요청이 오는 경우를 예상하지 못하기 때문에, 요청을 무시하고 브라우저는 아무런 응답 없이 로드 중이라는 표시만 나온다.

7. 타임아웃으로 커넥션이 끊길 때까지 기다린다.

 이와 같은 상황을 피하기 위해 홉별 헤더는 절대 전달 또는 캐시해서는 안 된다.

 

 Proxy-Connection 확장 헤더를 사용하면 영리한 프락시는 Proxy-Connection을Connection:Keep-Alive으로 변경 한다. 멍청한 프락시는 헤더를 무조건 전달하더라고 웹 서버는 그것을 무시하기 때문에 별 문제가 되지 않지만, 이 헤더를 인식하는 영리한 프락시 양옆에 멍청한 프락시가 끼어들면 문제가 다시 발생한다.


HTTP/1.1의 지속 커넥션

 keep-alive 커넥션을 지원하지 않는 대신, 더 개선된 지속 커넥션을 지원한다.(기본적으로 활성화 되어있다)

모든 커넥션을 지속 커넥션으로 취급하며, 다음 커넥션을 끊으려면 Connection: colse 헤더를 명시해야 한다. 하지만 Connection: colse를 보내지 않는 것이 서버가 영원히 커넥션을 유지한다는 말은 아니다. 클라이언트와 서버는 언제든 커넥션을 끊을 수 있기 때문이다.

 

파이프라인 커넥션

https://developer.mozilla.org/ko/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x

 

HTTP/1.x의 커넥션 관리 - HTTP | MDN

커넥션 관리는 HTTP의 주요 주제입니다: 대규모로 커넥션을 열고 유지하는 것은 웹 사이트 혹은 웹 애플리케이션의 성능에 많은 영향을 줍니다. HTTP/1.x에는 몇 가지 모델이 존재합니다: 단기 커넥

developer.mozilla.org

 지속 커넥션을 통해서 요청을 파이프라이닝할 수 있다. 여러 개의 요청은 응답이 도착하기 전까지는 큐에 쌓인다. 대기 시간이 긴 네트워크 상황에서 네트워크상의 왕복을 줄여 성능을 높여준다. (HTTP요청 순서와 응답 순서에 주의해야 한다. HTTP메시지는 순서가 없어 응답이 순서 없이 오면 정렬할 방법이 없다)

 

커넥션 끊기

 언제 어떻게 커넥션을 끊어야 하는지 명확한 기준이 없다. HTTP 클라이언트, 서버, 프락시는 언제든지 TCP 전송 커넥션을 끊을 수 있다. 

 전송된 데이터의 상태를 확인하기 위해 클라이언트나 프락시가 커넥션이 끊어졌다는 HTTP 응답을 받은 후, 실제 전달된 엔터티의 길이와 Content-length의 값의 일치 여부와 존재 여부를 검사하여 정확한 데이터의 길이를 서버에게 물어봐야 한다. (프락시가 캐시하거나 변형하면 안 된다)

멱등한 메서드가 이닌 POST요청을 보낼 때 파이프라인을 통해 요청하면 안 된다. 

 

TCP커넥션은 양방향이다. TCP 커넥션을 끊을 때는 예상치 못한 쓰기 에러를 방지하기 위해 '절반 끊기'를 사용해야 한다. 보통 출력 채널을 끊는다. 출력 채널을 절반 끊기 하면 반대편의 있는 기기는 모든 데이터를 버퍼로부터 읽고 나서 데이터 전송이 끝남과 동시에 커넥션이 끊겼다는 것을 알게된다. 만약 입력 채널을 끊어버리면 클라이언트는 이미 끊긴 입력 채널에 데이터를 전송하고 서버의 운영체제는 TCP 'connection reset by pree' 메시지를 클라이언트에 보내게 되고 클라이언트는 심각한 에러로 취급하여 버퍼에 저장된 아직 읽히지 않은 데이터를 모두 삭제한다.

 

 일반적으로 애플리케이션이 우아한 커넥션 끊기를 구현하는 것은 자신의 출력 채널을 먼저 끊고 다른 쪽에 있는 기기의 출력 채널이 끊기는 것을 기다리는 것이다. 커넥션은 리셋의 위험 없이 온전히 종료된다.