4.0 들어가기

  • HTTP는 어떻게 TCP 커넥션을 사용하는가
  • TCP 커넥션의 지연, 병목, 막힘
  • 병렬 커넥션, keep-alive 커넥션, 커넥션 파이프라인을 활요한 HTTP의 최적화
  • 커넥션 관리를 위해 따라야 할 규칙들

4.1 TCP 커넥션

  • 모든 HTTP 통신은 패킷 교환 네트워크 프로토콜들의 계층화된 집합인 TCP/IP를 통해 이뤄진다.
  • TCP 커넥션 예시: http://www.joes-hardware.com:80/power-tools.html 호출하면
    • 1단계: 브라우저가 www.joes-hardware.com이라는 호스트 명을 추출한다.
    • 2단계: 브라우저가 이 호스트 명에 대한 IP 주소를 찾는다.
    • 3단계: 브라우저가 포트 번호(80)를 얻는다.
    • 4단계: 브라우저가 202.43.78.3의 80 포트로 TCP 커넥션을 생성
    • 5단계: 브라우저가 서버로 HTTP GET 요청 메시지를 보낸다.
    • 6단계: 브라우저가 서버에서 온 HTTP 응답 메시지를 읽는다.
    • 7단계: 브라우저가 커넥션을 끊는다.
  • 신뢰할 수 있는 데이터 전송 통로인 TCP
    • TCP는 HTTP에게 신뢰할 만한 통신 방식을 제공
    • TCP 커넥션의 한쪽에 있는 바이트들은 반대쪽으로 순서에 맞게 정확히 전달된다.
  • TCP 스트림은 세크먼트로 나뉘어 IP 패킷을 통해 전송한다.
    • TCP는 IP 패킷(혹은 IP 데이터그램)이라고 불리는 작은 조각을 통해 데이터를 전송
    • HTTP와 HTTPS 네트워크 프로토콜 스택
      • HTTPS는 보안계층이 추가되었다.
      • Layer HTTP 프로토콜 스택 HTTPS 프로토콜 스택
        애플리케이션 계층 HTTP HTTP
        보안 계층 - TLS or SSL
        전송 계층 TCP TCP
        네트워크 계층 IP IP
        데이터 링크 계층 Network Interface Network Interface
    • TCP는 세그먼트라는 단위로 데이터 스트림을 잘게 나누고, 세그먼트를 IP 패킷이라고 불리는 봉투에 담아서 인터넷을 통해 데이터를 전달한다.
    • IP 패킷
      • IP 패킷 헤더(보통 20바이트)
      • TCP 세그먼트 헤더(보통 20바이트)
      • TCP 데이터 조각(0 혹은 그 이상의 바이트)
    • IP 헤더는 발신자와 목적지 IP 주소, 크기, 기타 플래그를 갖는다.
    • TCP 세그먼트 헤더는 TCP 포트 번호, TCP 제어 플래그, 그리고 데이터의 순서와 무결성을 검사하기 위해 사용되는 숫자 값을 포함
  • TCP 커넥션 유지하기
    • TCP는 포트 번호를 통해서 여러 개의 TCP 커넥션을 유지한다.
    • IP 주소는 해당 컴퓨터에 연결되고 포트 번호는 해당 애플리케이션에 연결된다.
    • TCP 커넥션 <발신자 IP 주소, 발신자 포트, 수신자 IP 주소, 수신자 포트>
  • TCP Three way handshake
    • TCP Connection 과정
    • 과정
      • A클라이언트는 B서버에 접속을 요청하는 SYN 패킷을 보낸다. 이때 A클라이언트는 SYN 을 보내고 SYN/ACK 응답을 기다리는 SYN_SENT 상태가 되는 것이다.
      • B서버는 SYN요청을 받고 A클라이언트에게 요청을 수락한다는 ACK 와 SYN flag 가 설정된 패킷을 발송하고 A가 다시 ACK으로 응답하기를 기다린다. 이때 B서버는 SYN_RECEIVED 상태가 된다.
        • 이때 서버는 Listen 상태로 포트 서비스가 가능한 상태여야 한다. (Closed :닫힌상태)
      • A클라이언트는 B서버에게 ACK을 보내고 이후로부터는 연결이 이루어지고 데이터가 오가게 되는것이다. 이때의 B서버 상태가 ESTABLISHED 이다.
    • 위와 같은 방식으로 통신하는것이 신뢰성 있는 연결을 맺어 준다는 TCP의 3 Way handshake 방식이다.

4.2 TCP의 성능에 대한 고려

  • HTTP는 TCP 바로 위에 있는 계층이기 때문에 HTTP 트랜잭션의 성능은 그 아래 계층인 TCP 성능에 영향을 받는다.

  • HTTP 트랜잭션 지연
    • 최근에 방문한 적 없는 URI를 호출하는 경우 DNS 이름 분석 인프라를 사용하여 호스트명을 IP 주소로 반환하는 데 지연 발생
    • 커넥션을 연결하는 데 클라이언트는 서버에게 TCP 커넥션 요청을 보내고 서버가 커넥션 허가 응답을 회신하기를 기다리는 데, 수백개의 HTTP 트랜잭션이 발생하는 경우 소요시간이 크게 증가한다.
    • 커넥션이 만들어진 후 HTTP 요청을 새로 생성된 TCP 파이프를 통해 전송된다. 요청 메시지가 인터넷을 통해 전달되고 서버에 의해 처리되는 데 시간이 소요된다.
    • 웹 서버가 HTTP 응답을 보내는 것 역시 시간이 소요된다.
  • 성능 관련 중요 요소
    • TCP 커넥션의 핸드셰이크 설정
    • 인터넷의 혼잡을 제어하기 위한 TCP의 느린 시작(slow start)
    • 데이터를 한데 모아 한 번에 전송하기 위한 네이글(nagle) 알고리즘
    • TCP의 편승(piggyback) 확인 응답(acknowledgement)을 위한 확인 응답 지연 알고리즘
    • TIME_WAIT 지연과 포트 고갈
  • TCP 커넥션 핸드 셰이크 지연
    • TCP는 커넥션을 맺기 위한 조건을 맞추기 위해 연속으로 IP 패킷을 교환한다.
    • Handshake
      • 클라이언트는 새로운 TCP 커넥션을 생성하기 위해 SYN이라는 플래그를 갖는 TCP 패킷을 서버에게 보낸다
        • SYN의 의미는 커넥션 생성 요청이라는 뜻이다.
      • 커넥션 요청이 받아들여졌음을 의미하는 SYN + ACK 플래그를 포함한 TCP 패킷을 클라이언트에게 보낸다.
      • 클라이언트는 커넥션이 잘 맺어졌음을 알리기 위해 서버에게 다시 확인 응답 신호(ACK)를 보내며 데이터를 같이 보낼 수 있다.
  • 확인 응답 지연
    • TCP는 성공적인 데이터 전송을 보장하기 위해 자체적인 확인 쳬계를 갖는다.
      • TCP 세그먼트는 순번과 데이터 무결성 체크섬을 갖는다.
      • 각 세그먼트의 수신자는 세그먼트를 온전히 받으면 작은 확인 응답 패킷을 송신자에게 반환한다.
      • 반환받지 못하는 경우 오류가 있는 것으로 판단하고 송신자는 다시 전송한다.
    • 확인 응답은 크기가 작기 때문에, TCP는 같은 방향으로 송출되는 데이터 패킷에 확인응답을 편승(piggyback) 시킨다.
    • 네트워크에 효율성 때문에 TCP 스택은 패킷에 편승을 늘리기 위한 확인응답 지연 알고리즘을 사용
    • 하지만 요청<->응답이 대응하는 HTTP 동작 방식은 편승할 기회를 감소시킨다.
      • 편승할 패킷을 찾는 데 없는 경우 확인 응답 지연 알고리즘으로 인한 지연이 발생할 수 있다.
  • TCP 느린 시작(slow start)
    • TCP 커넥션은 시간이 지나면서 자체적으로 ‘튜닝’한다.
      • 처음에는 커넥션의 최대 속도를 제한하고 데이터가 성공적으로 전송됨에 따라 속도 제한을 높여간다.
      • 갑작스러운 부하와 혼잡을 방지하는 데 쓰인다.
    • ‘튜닝’된 커넥션이 더 빠르기 때문에 HTTP에는 이미 존재하는 커넥션을 재사용하는 기능이 있다.
  • 네이글(Naggle) 알고리즘과 TCP_NODELAY
    • 네이글 알고리즘은 네트워크 효율을 위해 패킷을 전송하기 전에 많은 양의 TCP 데이터를 한개의 덩어리로 합친다.
    • 네이글 알고리즘은 세그먼트가 최대 크기가 되지 않으면 전송하지 않는다.
    • 문제점
      • 크기가 작은 HTTP 메시지는 패킷을 채우지 못하기 때문에 지연된다.
      • 확인 응답 지연과 함께 스일 경우 형편없이 동작한다.
    • HTTP 스택에 TCP_NODELAY 파라미터 값을 설정하면 네이글 알고리즘을 비활성화한다.
      • 해당 설정을 사용한다면 너무 작은 패킷이 많아 네트워크 효율이 떨어지지 않도록 큰 크기의 데이터 덩어리를 만들어야 한다.
  • TIME_WAIT의 누적과 포트 고갈
    • 실 성능에서 일어나는 경우가 아닌 성능 측정시에 발생하는 오류
    • TCP 커넥션의 종단에서 TCP 커넥션을 끊으면 종단에서는 커넥션의 IP 주소와 포트 번호를 메모리의 작은 제어 영역(control block)에 기록한다.
      • 이 정보는 같은 IP주소와 포트 번호를 사용하는 TCP 커넥션이 다시 필요할 때 일정 시간 동안에 생성되지 않게 한다.
      • 이전 커넥션과 관련된 패킷이 커넥션과 같은 IP 주소, 포트 번호를 갖는 새로운 커넥션에 삽입되는 문제를 방지한다.
    • 성능 측정의 경우 제한된 IP 주소와 포트 번호로 많은 요청을 하다보니 TCP 커넥션의 충돌이 발생하고 TIME_WAIT 에러가 발생한다.

4.3 HTTP 커넥션 관리

  • 순차적인 트랜잭션 처리에 의한 지연
    • 만약 3개의 이미지를 갖고 있는 한개의 페이지를 요청한다면, 4개의 HTTP 트랜잭션을 만들어야 한다.
      • 각 트랜잭션 당 새로운 커넥션을 필요로 한다면 커넥션을 맺는데 발생하는 지연과 함께 느린 시작 지연이 발생
    • HTTP 커넥션의 성능을 향상시킬 수 있는 기술
      • 병렬 커넥션
      • 지속 커넥션
      • 파이프라인 커넥션
      • 다중 커넥션

4.4 병렬 커넥션

  • HTTP는 클라이언트가 여러 개의 커넥션을 맺음으로써 여러 개의 HTTP 트랜잭션을 병렬적으로 처리할 수 있다.

  • 병렬 커넥션은 페이지를 더 빠르게 내려받는다.

  • 병렬 커넥션이 항상 더 빠르지는 않다.
    • 클라이언트의 대역폭이 좁을 때, 여러개의 객체를 병렬로 내려 받는 경우, 이 제한된 대역폭 내에서 각 객체를 전송받는 것은 느리기 때문에 성능의 장점이 사라진다.
    • 다수의 커넥션은 메모리를 많이 소모하고 자체적인 성능 문제를 발생시킨다.
  • 더 빠르게 느껴질 수 있다.
    • 화면에 여러 개의 객체가 동시에 보이면서 내려받고 있는 상황을 볼 수 있기 떄문에 사용자는 더 빠르다고 느낀다.

4.5 지속 커넥션

  • 처리가 완료된 후에도 계속 연결된 상태로 있는 TCP 커넥션을 지속 커넥션이라 한다.
  • ‘튜닝’된 TCP 파이프를 사용하며, TCP 커넥션을 맺기 위한 준비 작업(handshake)에 대한 시간을 절약할 수 있다.

  • 지속 커넥션 VS 병렬커넥션
    • 병렬 커넥션 단점
      • 각 트랜잭션마다 새로운 커넥션을 맺고 끊기 때문에 시간과 대역폭이 소요된다.
      • 각각의 새로운 커넥션은 TCP 느린 시작 때문에 성능이 떨어진다.
      • 실제로 연결할 수 있는 병렬 커넥션의 수에는 제한이 있다.
    • 병렬 커넥션 대비 지속 커넥션의 장점
      • 커넥션을 맺기 위한 사전 작업과 지연을 줄여준다.
      • 튜닝된 커넥션을 유지
      • 커넥션의 수를 줄여준다.
    • 지속 커넥션과 병렬 커넥션을 함께 사용할 때 가장 효과적이다.
  • HTTP/1.0+의 Keep-Alive 커넥션
    • HTTP/1.0 브라우저와 서버들은 keep-alive 커넥션이라는 지속 커넥션을 지원하기 위해 확장
    • 커넥션을 맺고 끊는 데 필요한 작업이 없기 때문에 시간이 단축된다.
    • Connection: Keep-Alive
  • Keep-Alive 동작
    • HTTP/1.1 명세에는 keep-alive를 사용하지 않기로 결정하였기 때문에 빠졌다.
    • 하지만 아직 브라우저와 서버 간에 keep-alive handshake가 널리 사용되고 있다.
      GET /index.html HTTP/1.0
      HOST: www.joes-hardware.com
      Connection: Keep-Alive
          
      HTTP/1.0 200 OK
      Content-type: text/html
      Content-length: 3104
      Connection: Keep-Alive
      ...
      
      • 헤더에 Connection: Keep-Alive 헤더를 포함시킨다.
  • Keep-Alive 옵션
    • keep-alive의 동작은 Keep-Alive 헤더의 쉼표로 구분된 옵션들로 제어할 수 있다.
      • timeout 파라미터: 커넥션이 얼마간 유지될 것인지 의미
      • max: 커넥션이 몇개의 HTTP 트랜잭션을 처리할 때까지 유지될 것인지 의미
      • 이대로 동작한다는 것은 보장할 수 없다.
        Connection: Keep-Alive
        Keep-Alive: max=5, timeout=120
        
  • Keep-Alive 커넥션 제한과 규칙
    • HTTP/1.0에서 기본으로 사용되지 않는다.
      • 요청 헤더에 Connection: Keep-Alive를 포함해야 한다.
    • 커넥션을 유지하기 위해 모든 헤더에 Connection: Keep-Alive를 포함해야 한다.
      • 만약 요청 헤더에 없는 경우 서버는 요청을 처리한 후 커넥션을 끊는다.
    • 응답 헤더에 Connection: Keep-Alive가 없는 경우 클라이언트는 커넥션이 끊겼다는 것을 알 수 있다.
    • 엔터티 본문의 길이를 알 수 있어야 커넥션을 유지할 수 있다.
      • 엔터티 본문에 정확한 Content-Length와 Multipart 형식을 갖거나 chunked transfer encoding 으로 인코딩되어야 한다.
    • 프록시와 게이트웨이 또한 Connection 헤더의 규칙을 철저히 잘 지켜야 한다.
      • Connection 헤더를 이해 못하는 프록시 서버를 사용해서는 안된다.
    • HTTP/1.0을 따르는 기기는 모든 Connection 헤더 필드를 무시해야 한다.
      • 오래된 프록시 서버 등에서 문제가 발생할 수 있다.
    • 클라이언트는 응답 전체를 모두 받기 전에 커넥션이 끊어졌을 경우 다시 요청을 보낼 수 있도록 준비되어야 한다.
  • Keep-Alive와 멍청한 프록시
    • Connection 헤더의 무조건 전달
      • 프록시가 Connection 헤더를 이해하지 못하고 그대로 전달한다면 클라이언트가 커넥션없이 그 다음 요청을 보내더라도 프록시 서버는 연결이 끊기기만을 기다린다.
    • 프록시와 홉별 헤더
      • 프록시는 Connection 헤더와 명시된 헤더들은 절대 전달하면 안된다.
      • 다른 홉별 헤더들 역시 전달하거나 캐시하면 안된다.
  • Proxy-Connection 살펴보기
    • 해당 헤더는 멍청하지 않은 영리한 Proxy를 위한 헤더로 영리한 Proxy라면 Proxy-Connection 헤더를 Connection 헤더로 변경해 서버로 전달하고, 서버의 Connection 헤더를 받아 그대로 클라이언트에 전달한다.
    • 만약 멍청한 프락시여서 Proxy-Connection을 이해하지 못하면, 그대로 서버로 보낼 것이고, 서버는 이를 무시할 것이다.
    • 넷스케이프는 멍청한 프록시 문제를 해결하기 위해 일반적인 Connection 헤더 대신 비표준인 Proxy-Connection 확장 헤더를 프록시에게 전달한다.
      • 멍청한 프록시 문제는 Connection 헤더를 이해하지 못하고 그대로 전달하기 때문에 발생
    • 서버는 Proxy-Connection을 무시하기 때문에 문제가되지 않는다.
    • 영리한 프록시는 Proxy-Connection 헤더가 Keep-Alive를 요청하는 것임을 인식하여 자체적으로 Connection:Keep-Alive 헤더를 웹 서버로 전송한다.
    • 하지만 클라이언트 -> 영리한 Proxy -> 멍청한 Proxy -> 서버라면, Connection 헤더를 제대로 인식하지 못하는 경우가 생긴다.
    • 이 때문에 HTTP/1.1 부터는 Connection:Keep-Alive를 지원하지 않는다.
  • HTTP/1.1의 지속 커넥션
    • HTTP/1.1은 Connection:Keep-Alive 헤더를 사용하지 않은 체 지속 커넥션을 기본으로 활성화되어 있다.
    • 커넥션을 끊기 위해서 Connection: close 헤더를 명시해야 한다.
    • Connection:close 헤더가 없다면 커넥션을 유지하는 것으로 추정할 것이다.
  • 지속 커넥션의 제한과 규칙
    • 클라이언트가 요청에 Connection: close헤더를 포함하면, 그 커넥션으로 추가 요청을 보낼 수 없음
    • 커넥션에 있는 모든 메시지가 자신의 길이를 정확히 가지고 있을 때에만 커넥션 지속 가능
    • HTTP/1.1 프록시는 클라이언트와 서버 각각에 대해 별도 지속 커넥션을 맺고 관리
    • HTTP/1.1 프록시는 클라이언트의 지원 범위를 알고 있지 않은 한 지속 커넥션을 맺으면 안된다
    • 클라이언트는 전체 응답을 받기 전에 커넥션이 끊어지면, 요청을 반복해서 보내도 문제 없는 경우에는 요청을 다시 보낼 준비가 되어야함
    • N명의 사용자가 서버로 접근하려 하면, 프락시는 서버나 상위 프락시에 넉넉잡아 2N개의 커넥션을 유지해야함

4.6 파이프라인 커넥션

  • HTTP/1.1은 지속 커넥션을 통해서 요청을 파이프라이닝할 수 있도록 한다.
    • 여러 개의 요청은 응답이 오기 전에 큐에 쌓인다.
    • 첫 번째 요청이 서버로 전달되면 두, 세번째 요청이 전달될 수 있도록 한다.
    • 이는 대기 시간을 줄여 성능을 높여준다.
  • 파이프라인 제약 사항
    • HTTP 클라이언트는 커넥션이 지속 커넥션인지 확인하기 전까지는 파이프라인을 이어서 안됨
    • HTTP 응답은 요청 순서와 같게 와야 함
    • HTTP 클라이언트는 완료되 지 않은 요청이 파이프라인에 있으면 언제든지 다시 요청을 보낼 준비가 되어있어야 함
    • POST 요청같이 반복해서 보낼 경우 문제가 생기는 요청은 파이프라인을 통해 보내면 안됨(데이터 무결성)

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

  • 마음대로 커넥션 끊기
    • HTTP 애플리케이션은 언제든지 지속 커넥션을 임의로 끊을 수 있다.(Connection: close)
  • Content-Length와 Truncation
    • 클라이언트나 프록시가 커넥션이 끊어졌다는 HTTP 응답을 받은 후 해당 내용을 서버에게 물어봐야 한다.
      • 실제 전달된 엔티티의 길이와 Content-Length가 일치하는지
      • Content-Length 자체가 존재하지 않은 경우 데이터의 정확한 길이
  • 커넥션 끊기의 허용, 재시도 멱등성
    • GET 요청에 경우 데이터 변경과 같은 영향을 끼치지 않는다.
    • POST 부류의 요청은 반복될 경우 트랜잭션을 일으키기 때문에 여러 번 중복될 것이므로 반복은 피해야 한다.
    • 멱등? 여러 번 실행해도 결과가 같은 경우
      • GET, HEAD, PUT, DELETE, TRACE, OPTION은 멱등하다.
    • POST와 같이 멱등이 아닌 요청은 파이프라인을 통해 요청하면 안된다.
  • 우아한 커넥션 끊기
    • 전체 끊기와 절반 끊기
      • close(): 전체 끊기
      • shutdown(): 입력, 출력 채널 중 하나를 개별적으로 끊는다.
    • TCP 끊기와 리셋 에러
      • 예상치 못한 쓰기 에러를 발생하는 것을 예방하기 위해 shutdown()을 사용해야 한다.
      • 보통은 출력 채널을 끊는 것이 안전하다.
      • TCP는 4 way handshake 과정을 통해 연결 해제한다.
        • 클라이언트가 서버에게 FIN을 전송한다. 이 때 클라이언트는 FIN_WAIT1 상태로 FIN, ACK를 대기하고, FIN을 받은 서버는 CLOSE_WAIT 상태가 된다.
        • 서버는 클라이언트에게 ACK와 FIN를 전송한다. 이 때 서버는 다음 ACK를 받을 때까지 LAST_ACK 상태가 된다.
        • ACK를 받은 클라이언트는 다음 FIN 까지 FIN_WAIT2 상태이고, FIN을 받은 후 ACK를 전송한다. 이 때 클라이언트는 TIME_WAIT 상태가 된다.
        • ACK 전송이 완료되면 클라이언트와 서버가 CLOSED된다.
    • 우아하게 커넥션 끊기
      • 일반적으로 애플리케이션 자신의 출력 채널을 먼저 끊고 다른 쪽 기기의 출력 채널이 끊기는 것을 기다리는 것이다.
      • 하지만 상대방이 절반 끊기를 구현했다는 보장도 없고, 검사해준다는 보장도 없다.
      • 따라서, 절반 끊기를 하고 난 후에도 데이터나 스트림의 끝을 식별하기 위해 입력 채널에 대해 상태 검사를 주기적으로 해야한다.

** 참고 서적: HTTP 완벽 가이드