[4장] 커넥션 관리

cdwde·2022년 8월 9일

HTTP 완벽 가이드

목록 보기
5/6

TCP 커넥션

  • 모든 HTTP 통신은 패킷 교환 네트워크 프로토콜들의 계층화 집합인 TCP/IP를 통해 이루어짐
  • 클라이언트 애플리케이션은 서버 애플리케이션으로 TCP/IP 커넥션을 맺을 수 있고, 커넥션이 맺어지면 클라이언트와 서버 컴퓨터 간 주고받는 메시지들은 안전하게 전달

신뢰할 수 있는 데이터 전송 통로인 TCP

TCP는 HTTP에 신뢰할 만한 통신 방식 제공

TCP 스트림은 세그먼트로 나뉘어 IP 패킷을 통해 전송

  • TCP는 IP 패킷이라고 불리는 작은 조각을 통해 데이터를 전송
  • HTTP는 IP, TCP, HTTP로 구성된 프로토콜 스택에서 최상위 계층
  • HTTP에 보안 기능을 더한 HTTPS는 TLS 혹은 SSL이라고 불리며 HTTP와 TCP 사이에 있는 암호화 계층임

HTTP가 메시지를 전송할 때

  1. 현재 연결되어 있는 TCP 커넥션을 통해 메시지 데이터의 내용을 순서대로 보냄
  2. TCP는 세그먼트라는 단위로 데이터 스트림을 잘게 나누고, 세그먼트를 IP 패킷에 담아 인터넷을 통해 데이터를 전달
  • TCP/IP 소프트웨어에 의해 처리되고 HTTP 프로그래머에게 보이지 않음
  • TCP 세그먼트는 하나의 IP 주소에서 다른 IP 주소로 IP 패킷에 담겨 전달

IP 패킷

IP 패킷은 IP 패킷 헤더, TCP 세그먼트 헤더, TCP 데이터 조각 포함

  • IP 패킷 헤더: 발신지 IP, 목적지 IP, 크기, 기타 플래그 포함
  • TCP 세그먼트 헤더: TCP 포트번호, TCP 제어 플래그, 데이터 순서와 무결성 검사하기 위해 사용되는 숫자값 포함
  • TCP 데이터 조각

TCP 커넥션 유지하기

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

TCP 소켓 프로그래밍

  • 소켓 API를 사용하면 TCP 종단 데이터 구조를 생성하고, 원격 서버의 TCP 종단에 그 종단 데이터 구조를 연결하여 데이터 스트림을 읽고 쓸 수 있음
  • TCP API는 기본적인 네트워크 프로토콜의 핸드셰이킹, TCP 데이터 스트림, IP 패킷 간 분할 및 재조립에 대한 세부사항 외부로부터 숨김

TCP 성능에 대한 고려

HTTP는 TCP 바로 위에 있는 계층이기 때문에 HTTP 트랜잭션의 성능은 TCP 성능에 영향을 받음

HTTP 트랜잭션 지연


클라이언트나 서버가 너무 많은 데이터를 내려받거나 복잡하고 동적인 자원 실행하지 않는 한 HTTP 지연은 TCP 네트워크 지연 때문에 발생

HTTP 트랜잭션을 지연시키는 원인

  1. 클라이언트는 URI에서 서버의 IP 주소와 포트 번호 알아야하는데, 기존에 방문한 적 없는 호스트라면 DNS 인프라를 사용해 호스트 명을 IP 주소로 변환하는데 시간이 걸림
  2. 새로운 TCP 커넥션 시 허가 응답을 회신하길 바라는데, 수백 개의 HTTP 트랜잭션이 만들어지면 소요시간 증가
  3. 커넥션이 맺어지면 클라이언트는 HTTP 요청을 새로 생성된 TCP 파이프를 통해 전송하고, 웹 서버는 데이터가 도착하는대로 TCP 커넥션에서 요청 메시지를 읽고 처리하는데 요청 메시지가 인터넷을 통해 전달되고 처리되는데 시간 소요
  4. 웹 서버가 HTTP 응답 보내는 것 시간 소요

성능 관련 중요 요소

1. TCP 커넥션 핸드셰이크 지연

새로운 TCP 커넥션을 열면 TCP 소프트웨어는 커넥션을 맺기 위한 조건을 맞추기 위해 연속적으로 IP 패킷 교환하는데, 작은 크기의 데이터 전송에 커넥션이 사용된다면 이러한 패킷 교환은 HTTP 성능을 크게 저하시킬 수 있음

1. 클라이언트는 새로운 TCP 커넥션 생성 위해 작은 TCP 패킷(SYN(커넥션 생성 요청))을 서버에 보냄
2. 서버가 커넥션을 받으면 몇 가지 커넥션 매개변수를 산출하고, 커넥션 요청이 받아들여졌다는 TCP 패킷(SYN + ACK)을 보냄
3. 클라이언트는 커넥션이 잘 맺어졌음 알리기 위해 확인응답 신호를 보냄

HTTP 트랜잭션이 아주 큰 데이터를 주고 받지 않는 경우에는 SYN/SYN+ACK 핸드셰이크가 지연 발생시킴
(크기가 작은 HTTP 트랜잭션은 50% 이상의 시간을 TCP를 구성하는데 씀)
=> TCP 구성으로 인한 지연 제거하기 위해 HTTP가 이미 존재하는 커넥션 어떻게 재활용할까?

2. 확인응답 지연

인터넷 자체가 패킷 전송을 완벽하게 보장하지는 않기 때문에 TCP는 성공적인 데이터 전송을 보장하기 위해 자체적인 확인 체계 가짐

응답 순서
  1. 각 세그먼트의 수신자는 세그먼트를 온전히 받으면 확인응답 패킷을 송신자에게 반환
  2. 송신자가 특정 시간 안에 확인응답 메시지를 받지 못하면 패킷이 파기되었거나 오류가 있는것으로 판단하고 데이터 다시 전송
  • 확인응답이 같은 방향으로 가는 데이터 패킷에 편승되는 경우를 늘리기 위해 많은 TCP 스택은 확인응답 지연 알고리즘을 구현

  • 확인응답 지연은 송출할 확인응답을 특정 시간동안 버퍼에 저장해두고, 확인응답을 편승시키기 위한 송출 데이터 패킷을 찾음
    (일정 시간안에 송출 데이터 패킷 찾지 못하면 확인응답은 별도 패킷을 만들어 전송)

  • But, 요청과 응답 두 가지 형식으로만 이루어지는 HTTP 동작 방식은 확인 응답이 송출 데이터 패킷에 편승할 기회를 감소시킴
    (편승할 패킷을 찾으려고 하면 해당 방향으로 송출될 패킷이 많지 않기 때문에 확인응답 지연 알고리즘으로 인한 지연 자주 발생)

3. TCP 느린 시작(slow start)

  • TCP 커넥션은 처음에는 커넥션의 최대 속도를 제한하고 데이터가 전송됨에 따라 속도 제한을 높여나감
  • 인터넷의 급작스러운 부하와 혼잡을 방지하는데 사용
  • TCP가 한 번에 전송할 수 있는 패킷의 수 제한

혼잡제어기능(opening the congestion window)
1. 패킷이 성공적으로 전달되는 각 시점에 송신자는 추가로 2개의 패킷을 더 전송할 수 있는 권한 얻음
2. HTTP 트랜잭션에서 전송할 데이터의 양이 많으면 모든 패킷을 한 번에 전송할 수 없고, 한 개의 패킷만 전송 후 확인응답 기다림
3. 확인응답을 받으면 2개의 패킷을 보낼 수 있고, 그 패킷 각각 대한 확인응답을 받으면 총 4개의 패킷 보낼 수 있음

=> 새로운 커넥션은 이미 어느정도 데이터를 주고받은 튜닝된 커넥션보다 느림
=> 튜닝된 커넥션이 더 빠르기 때문에 HTTP에서는 이미 존재하는 커넥션을 재사용하는 기능 있음

4. 네이글 알고리즘과 TCP_NODELAY

추가 예정

5. TIME_WAIT의 누적과 포트 고갈

추가 예정


HTTP 커넥션 관리

커넥션 헤더

HTTP 헤더 필드 명(이 커넥션에만 해당하는 헤더들), 임시적인 토큰 값(커넥션에 대한 비표준 옵션), close값(작업 완료 시 종료) 세 가지 종류의 토큰 전달될 수 있음

커넥션 토큰이 HTTP 헤더 필드 명을 가지고 있으면, 해당 필드들은 현재 커넥션만을 위한 정보로 다음 커넥션에 전달하면 안됨
(= Connection 헤더에 있는 모든 헤더 필드는 메시지를 다른 곳으로 전달하는 시점에 삭제되어야 함)

  1. HTTP 애플리케이션이 Connection 헤더와 메시지를 전달받으면, 수신자는 송신자에게서 온 요청에 기술되어 있는 모든 옵션 적용
  2. 다음 홉에 메시지를 전달하기 전에 Connection 헤더와 Connection 헤더에 기술되어 있던 모든 헤더 삭제

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

각 트랜잭션이 새로운 커넥션을 필요로 하면, 커넥션을 맺는데 발생하는 지연과 느린 시작 지연이 발생

순차적으로 로드 시, 특정 브라우저의 경우 객체를 화면에 배치하려면 객체의 크기를 알아야하기 때문에 모든 객체를 내려받기 전까지 텅 화면을 보여줄 수 있음

그래서 HTTP 커넥션의 성능을 향상시킬 수 있는 여러 최신 기술이 존재

  • 병렬커넥션: 여러 개의 TCP 커넥션을 통한 동시 HTTP 요청
  • 지속커넥션: 커넥션을 맺고 끊는데서 발생하는 지연을 제거하기 위한 TCP 커넥션의 재활용
  • 파이프라인커넥션: 공유 TCP 커넥션을 통한 병렬 HTTP 요청
  • 다중커넥션: 요청과 응답들에 대한 중재

병렬 커넥션

HTTP는 클라이언트가 여러 개의 커넥션을 맺음으로써 여러개의 HTTP 트랜잭션을 병렬로 처리할 수 있게 함
(예제에서는 4개의 이미지를 할당받은 각 TCP 커넥션 상의 트랜잭션을 통해 병렬로 내려 받음)

페이지를 더 빠르게 내려받는다

  • 단일 커넥션의 대역폭 제한과 커넥션이 동작하지 않는 시간 활용 시, 객체가 여러개 있는 웹 페이지 더 빠르게 내려받을 수 있음
  • 하나의 커넥션으로 객체들을 로드할 때 대역폭 제한과 대기 시간 줄일 수 있다면 더 빠르게 로드 가능
  • 각 커넥션의 지연 시간을 겹치게 하면 총 지연 시간 줄일 수 있음

항상 더 빠르지는 않다

  • 일반적으로 병렬 커넥션이 더 빠르기는 하지만 항상 그렇지는 않음
  • 클라이언트의 대역폭이 좁을 때는 대부분 시간을 데이터 전송하는데만 쓸 것인데, 여러 개의 객체를 병렬로 내려 받는 경우 이 제한된 대역폭 내에서 각 객체를 전송받는 것은 느리기 때문에 장점 거의 없어짐
  • 다수의 커넥션은 메모리를 많이 차지하고 자체적인 성능 문제 발생
    => 브라우저는 실제로 병렬 커넥션을 사용하긴 하지만 적은 수(대부분 6개-8개)의 병렬 커넥션만 허용

더 빠르게 느껴질 수 있다

화면에 여러 개의 객체가 동시에 보이면서 내려받고 있는 상황을 볼 수 있기 때문에 사용자는 더 빠르게 내려받고 있는 것처럼 느낄 수 있음

단점

  1. 각 트랜잭션마다 새로운 커넥션 맺고 끊기 때문에 시간과 대역폭 소요
  2. 각각의 새로운 커넥션은 TCP 느린 시작 때문에 성능 떨어짐
  3. 실제로 연결할 수 있는 병렬 커넥션의 수 제한

지속 커넥션

사이트 지역성: 웹 클라이언트는 보통 같은 사이트에 여러 개의 커넥션 맺음
(웹페이지에 첨부된 이미지들은 대부분 같은 웹 사이트에 있고, 상당 수의 하이퍼링크도 같은 사이트를 가리키기 때문)
=> TCP 커넥션을 유지하여 앞으로 있을 HTTP 요청에 재사용 할 수 있음

비지속 커넥션은 각 처리가 끝날 때마다 커넥션을 끊지만, 지속 커넥션은 클라이언트나 서버가 커넥션을 끊기 전까지는 트랜잭션 간에도 커넥션 유지

장점

  1. 커넥션을 맺기 위한 사전 작업과 지연 줄여줌
  2. 튜닝된 커넥션(패킷을 수차례 성공적으로 전송한 결과로 한 번에 다수의 패킷을 전송할 수 있는 권한을 얻은 상태의 커넥션) 유지
  3. 커넥션 수 줄여줌

단점

지속 커넥션을 잘못 관리할 경우, 계속 연결된 상태로 있는 수많은 커넥션 쌓임
=> 로컬의 리소스, 원격의 클라이언트와 서버의 리소스에 불필요한 소모 발생

지속 커넥션은 병렬 커넥션과 함께 사용할 때 가장 효과적

HTTP/1.0+의 Keep-Alive 커넥션

연속적으로 4개의 커넥션을 생성하여 처리하는 방식과 하나의 지속 커넥션으로만 처리하는 방식 비교 시 커넥션을 맺고 끊는데 필요한 작업이 없어서 시간 단축

Keep-Alive 동작

keep-alive는 사용하지 않기로 결정되어 HTTP/1.1 명세에서는 빠졌지만 아직 브라우저와 서버 간 keep-alive 핸드셰이크가 널리 사용되고 있어 처리할 수 있게 개발해야함

  1. 클라이언트는 커넥션을 유지하기 위해 요청에 Connection:Keep-Alive헤더 포함
  2. 요청 받은 서버는 그 다음 요청도 이 커넥션을 통해 받고자 한다면, 응답 메시지에 같은 헤더를 포함시켜 응답
    (응답에 Connection:Keep-Alive 헤더가 없으면, 클라이언트는 서버가 keep-alive를 지원하지 않고 응답 메시지 전송 후 서버 커넥션을 끊을 것이라 추정)

Keep-Alive 옵션

Keep-Alive는 커넥션 유지를 바라는 요청일 뿐, 받았다고 해서 무조건 따를 필요는 없음

  • timeout: 커넥션이 얼마간 유지될 것인지 의미
    (But 이대로 동작한다는 보장은 X)
  • max: 커넥션이 몇 개의 HTTP 트랜잭션을 처리할 때까지 유지될 것인지
    (But 이대로 동작한다는 보장은 X)

Keep-Alive 커넥션 제한과 규칙

  • keep-alive는 HTTP/1.0에서 기본적으로 사용되지는 않아 사용하기 위해 클라이언트는 Connection:Keep-Alive 요청 헤더를 보내야 함 (클라이언트가 관련 헤더를 보내지 않으면 서버는 요청 처리 후 커넥션 끊을 것)
  • 커넥션이 끊어지기 전에 엔티티 본문의 길이를 알 수 있어야 커넥션 유지할 수 있음
    = 엔티티 본문이 정확한 Content-Length 값과 함께 멀티파트 미디어 형식을 가지거나 청크 전송 인코딩으로 인코드 되야함을 의미
    (트랜잭션이 끝나는 시점에 기존의 메시지의 끝과 새로운 메시지의 시작점을 정확히 알 수 없어서 keep-alive 커넥션에서 잘못된 Content-Length 값을 보내는 것은 좋지 않기 때문)
  • 프락시와 게이트웨이는 Connection 헤더의 규칙을 철저히 지켜야 함
    (=프락시와 게이트웨이는 메시지 전달, 캐시 넣기 전 Connection 헤더에 명시된 모든 헤더 필드와 Connection 헤더를 제거해야 함)
  • HTTP/1.0을 따르는 기기로부터 받는 모든 Connection 헤더 필드는 무시해야 함
  • 클라이언트는 응답 전체를 받기 전 커넥션이 끊어졌을 경우, 요청을 다시 보낼 수 있게 준비되어 있어야 함

Keep-Alive와 dumb 프락시

Connection 헤더의 무조건 전달

  • 오래되고 단순한 수많은 프락시가 Connection 헤더에 대한 처리 없이 요청을 그대로 전달
  1. 클라이언트는 커넥션을 유지하기를 요청하고, 요청이 받아들여졌는지 확인하기 위해 응답을 기다림
  2. dump 프락시는 요청받은 HTTP의 Connection 헤더를 이해히지 못하고, keep-alive가 무엇인지 모르기 때문에 다음 서버에게 메시지를 그대로 전달
    (Connection 헤더는 hop-by-hop 헤더(오직 한 개의 전송 링크에만 적용, 다음서버로는 전달 안되야함)이기 때문에 문제 발생)
  3. 전달된 HTTP 요청이 서버에 도착, 웹 서버는 프락시가 커넥션을 유지하자고 요청하는 것으로 잘못 판단하고 프락시와 커넥션을 유지하는 것에 동의하고 Connection: Keep-Alive 헤더를 포함해 응답, 하지만 프락시는 keep-alive 이해 못함
  4. 프락시는 서버로부터 받은 Connection: Keep-Alive 헤더를 포함한 응답 메시지를 받고, 프락시가 커넥션을 유지하는 것에 동의했다고 추정
  5. 프락시는 keep-alive를 전혀 모르기에 받았던 모든 데이터를 그대로 클라이언트에게 전달하고 서버가 커넥션을 끊기를 기다리지만, 서버는 커넥션 유지를 요청한 것으로 알고 있어서 커넥션을 끊지 않음
    (= 프락시는 커넥션이 끊어지기 전까지 계속 커넥션이 끊어지기를 기다림)
  6. 클라이언트는 응답 메시지 받고, 다음 요청을 시작하는데 커넥션이 유지되고 있는 프락시에 그 요청을 보냄. 프락시는 같은 커넥션상에서 다른 요청이 오는 것은 예상하지 못하기 때문에, 요청은 프락시로부터 무시되고 브라우저에는 로드 중이라는 표시만 나옴
  7. 브라우저는 자신이나 서버가 타임아웃이 나서 커넥션이 끊길 때까지 기다림

=> 이러한 잘못된 통신 피하기 위해서 프락시는 Connection 헤더와 Connection 헤더에 명시된 헤더, 홉별 헤더 전달해서는 안된다.

dumb 프락시 최종 정리
홉별 헤더들은 한 개의 특정 커넥션에서 쓰이고 이후에 전달하면 안되는데,
dump 프락시는 Connection: Keep-Alive 같은 홉별 헤더를 무조건 전달하기 때문에 문제를 일으킴
(홉별 헤더를 전달받은 서버가 그 헤더를 자신과 프락시 간의 커넥션으로 오해하기에 문제 발생)

Proxy-Connection 살펴보기

Connection 헤더 대신 비표준인 Proxy-Connection 확장 헤더를 프락시에게 전달
(프락시가 Proxy-Connection 헤더를 무조건 전달해도 웹 서버는 무시하기 때문에 문제가 되지 않는다.)

영리한 프락시(지속 커넥션 핸드셰이킹을 이해할 수 있는 것)이라면 의미 없는 Proxy-Connection 헤더를 Connection 헤더로 바꿈으로써 원하는 효과를 얻을 수 있음

위의 내용을 풀어서 설명하자면
Proxy-Connection 헤더가 웹 서버에 전달되더라도 클라이언트와 프락시 사이 혹은 프락시와 서버 사이에 keep-alive 커넥션이 맺어지지 않는데, 영리한 프락시는 Proxy-Connection 헤더가 keep-alive를 요청하는 것을 인식해서 자체적으로 Connection:Keep-Alive 헤더를 웹 서버에 전송

하지만 이 방식은 클라이언트와 서버 사이에 한 개의 프락시만 있는 경우 동작

프락시가 많은 구조에서는 Proxy-Connection을 사용해도 여전히 문제 발생할 수 있음

HTTP/1.1의 지속 커넥션

  • HTTP/1.1의 지속 커넥션은 기본으로 활성화되어 있음
  • 별도 설정을 하지 않는 한 모든 커넥션은 지속 커넥션으로 취급
  • 트랜잭션 끝난 후 커넥션을 끊기 위해서는 Connection:close 헤더 명시 필요
  • Connection:close 없어도 언제든지 커넥션 끊을 수 있음

지속 커넥션의 제한과 규칙

  • 클라이언트가 Connection:close 헤더를 포함해 보냈으면 그 커넥션으로 추가적인 요청 불가
  • 클라이언트가 해당 커넥션으로 추가적인 요청 보내지 않을 것이면, Connection:close 헤더를 포함해야함
  • 커넥션에 있는 모든 메시지가 자신의 길이 정보를 정확히 가지고 있을 때만 커넥션 지속 가능

추가 예정


파이프라인 커넥션

추가 예정


커넥션 끊기에 대한 미스터리

마음대로 커넥션 끊기

HTTP 애플리케이션은 언제든지 지속 커넥션을 임의로 끊을 수 있음

But, 서버가 유휴상태(지속 커넥션이 일정 시간동안 요청 전송하지 않고 있음)에 있는 커넥션을 끊는 시점에 서버는 클라이언트가 데이터를 전송하지 않을 것이라 확신하지 못하고, 클라이언트는 요청 메시지를 보내는 도중에 문제 생김

Content-Length와 Truncation

각 HTTP 응답은 본문의 정확한 크기 값을 가지고 Content-Length 헤더를 가지고 있어야함

클라이언트나 프락시가 커넥션이 끊어졌다는 HTTP 응답을 받은 후, 실제 전달된 엔티티의 길이와 Content-Length의 값이 일치하지 않거나, Content-Length가 존재하지 않으면 수신자는 데이터의 정확한 길이를 서버에게 물어봐야 함

커넥션 끊기의 허용, 재시도, 멱등성

커넥션은 에러가 없더라도 끊을 수 있기에, HTTP 애플리케이션은 예상치 못하게 커넥션이 끊어졌을 때 대응할 준비가 되어있어야 함

우아한 커넥션 끊기

TCP 커넥션은 양방향으로, 양쪽에는 데이터를 읽거나 쓰기 위한 입력 큐와 출력 큐가 있음
한쪽 출력 큐에 있는 데이터는 다른 쪽의 입력 큐에 보내질 것

전체 끊기와 절반 끊기

업로드중..

  • close(): TCP 커넥션의 입력 채널과 출력 채널의 커넥션 모두 끊음
  • shutdown(): 입력 채널이나 출력 채널 중 하나 개별적으로 끊음

TCP 끊기와 리셋 에러

단순한 HTTP 애플리케이션은 전체 끊기만 사용할 수 있지만, 기기들의 예상치 못한 쓰기 에러를 발생하는 것을 예방하기 위해 절반 끊기를 사용해야 함

보통 커넥션의 출력 채널을 끊는 것이 안전하며, 커넥션의 반대편에 있는 기기는 모든 데이터를 버퍼로부터 읽고 나서 데이터 전송이 끝남과 동시에 커넥션을 끊었단느 것을 알게 될 것임

우아하게 커넥션 끊기

애플리케이션 자신의 출력 채널을 먼저 끊고 다른 쪽에 있는 기기의 출력 채널이 끊기는 것을 기다리는 것이 우아하게 커넥션 끊는 방법

양쪽에서 더는 데이터를 전송하지 않을 것이라고 알려주면 커넥션은 리셋의 위험 없이 온전히 종료

출처
HTTP 완벽 가이드

0개의 댓글