[HTTP 완벽 가이드] - 커넥션 관리

Lee Jeong Min·2022년 3월 5일
2

네트워크

목록 보기
11/17
post-thumbnail

4장 커넥션 관리

TCP 커넥션

전 세계 모든 HTTP 통신은 TCP/IP 를 통해 이루어진다.

→ 한번 연결이 맺어지면 클라이언트와 서버 컴퓨터 간에 주고받는 메시지들은 손실 혹은 손상되거나 순서가 바뀌지 않고 안전하게 전달(TCP특성)된다.

브라우저가 TCP 커넥션을 통해 웹 서버에 요청을 보내는 과정

호스트명 추출
⬇️
호스명에 대한 주소를 DNS를 통해 IP 주소를 찾음
⬇️
포트번호를 얻음
⬇️
브라우저가 TCP 커넥션 생성
⬇️
브라우저가 서버로 HTTP GET 요청
⬇️
서버로부터 응답메시지를 읽음
⬇️
커넥션 종료

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

인터넷을 안정적으로 연결해주고 HTTP에게 신뢰할 만한 통신 방식을 제공한다.

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

HTTPS: HTTP에 보안 기능을 추가(TLS, SSL이라는 보안 계층을 거침)

HTTP에서 메시지 전송하는 경우 연결되어있는 TCP 커넥션을 통해 TCP 세그먼트라는 단위로 데이터 스트림을 잘게 나누고, 세그먼트 IP 패킷이라는 봉투에 담아서 인터넷을 통해 데이터를 전달한다. 그리고 아래의 데이터를 포함한다.

  • IP 패킷 헤더(보통 20바이트): 발신지와 목적지의 IP주소, 크기, 기타 플래그 등등..
  • TCP 세그먼트 헤더(보통 20바이트): TCP 포트번호, TCP 제어 플래그, 데이터 순서와 무결성 검사
  • TCP 데이터 조각(0 혹은 그 이상의 바이트)

TCP 커넥션 유지하기

TCP 커넥션은 다음과 같이 4가지 값으로 식별한다. (이 4가지 값이 모두 같은 경우는 없다. 유일!!)

〈발신지 IP 주소, 발신지 포트, 수신지 IP 주소, 수신지 포트〉

어떤 커넥션들은 같은 목적지 포트 번호를 가리킬 수 있다.

TCP 소켓 프로그래밍

간단한 소켓 API의 주요 인터페이스를 알아보자.

소켓 API 호출설명
s = socket()연결이 되지 않은 익명의 새로운 소켓 생성
bind(s, )소켓에 로컬 포트 번호와 인터페이스 힐당
connect(s, )로컬의 소켓과 원격의 호스트 및 포트 사이에 TCP 커넥선 생성
listen(s,...)커넥션을 받아들이기 위해 로컬 소켓에 허용함을 표시
s2 = accept(s)누군가 로컬 포트에 커넥션을 맺기를 기다림
close(s)TCP커넥션을완전히끊음

TCP API는, 기본적인 네트워크 프로토콜의 핸드셰이킹, 그리고 TCP 데이터 스트림과 IP 패킷 간의 분할 및 재조립에 대한 모든 세부사항을 외부로부터 숨긴다.

TCP으| 성능에 대한 고려

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

HTTP 트랜잭션 지연

트랜잭션 지연의 원인

  1. 방문한적이 없는 호스트 접근시 DNS 이름 분석 인프라를 사용하여 URI에 있는 호스트 명을 IP 주소로 변환하는데 수십초의 시간
  2. 커넥션 설정시간(클라 → 서버로 TCP 커넥션 요청), HTTP 트랜잭션이 여러개일수록 시간 더 증가!
  3. 웹 서버가 데이터 요청 받고 이를 처리하는 시간
  4. 웹 서버가 (처리한 데이터) HTTP 응답을 보내는 시간

하드웨어 성능, 네트워크와 서버의 전송 속도, 요청과 응답 메시지의 크기, 클라이언트와 서버 간의 거리에 따라 크게 달라지며 TCP 프로토콜의 기술적인 복잡성도 지연에 큰 영향을 끼친다.

성능 관련 중요 요소

HTTP 프로그래머에게 영향을 주는 가장 일반적인 TCP 관련 지연들

  • TCP 커넥션의 핸드 셰이크 설정(크기가 작은 HTTP 트랜잭션은 TCP 커넥션을 구성하는 데에만 50%이상 시간을 소비)

    클라 → 서버: SYN 플래그를 보내 커넥션 생성 요청
    
    서버 → 클라: SYN + ACK를 보내 커넥션 요청이 받아들였음을 보냄
    
    클라 → 서버: ACK + get 요청(오늘날의 TCP는 확인응답 패킷과 같이 데이터를 보낼 수 있음)등 확인응답 신호
    
  • TCP의 편승(piggyback) 확인응답(acknowledgment)을 위한 확인응답 지연 알고리즘

    각 TCP 세그먼트는 순번과 데이터 무결성 체크섬을 가지고, 수신자가 이를 온전히 받으면 확인응답(ACK) 메시지를 다시 보낸다. 그러나 이 크기가 작아 같은 TCP 방향으로 송출되는 데이터 패킷에 편승(piggyback, 네트워크 효율적으로 사용하려고)하려고 하며, 확인응답 지연 알고리즘을 구현하는데 이로 인한 지연이 자주 발생하기도 한다.(요청, 응답 두 가지 형식의 HTTP 동작 방식은 편승할 기회가 많지 않음)

  • 인터넷의 혼잡을 제어하기 위한 TCP의 느린시작(slow-start)

    TCP 커넥션은 시간이 지나면서 자체적으로 ‘튜닝'되어 처음에는 보낼 수 있는 패킷 수를 제한하다가, 전송이 성공하면서 점차 늘려진다.(혼잡 윈도를 연다 = 보낼 수 있는 패킷 개수가 늘어나는 것)
    → 이러한 혼잡제어 기능 때문에 새로운 커넥션은 느리다.

  • 데이터를 한데 모아 한 번에 전송하기 위한 네이글(nagle) 알고리즘

    작은 크기의 데이터를 TCP로 보내면 오히려 배보다 배꼽이 더큼(40바이트 상당의 플래그, 헤더를 포함하여 전송하기때문)

    → 네트워크 성능이 크게 떨어짐

    이를 예방하기위해 네이글 알고리즘을 사용하는데, 세그먼트가 최대 크기가 되지 않으면 전송을 하지 않고 확인 응답을 받았을 때, 작은 패킷 전송을 허락하며 아직 패킷이 전송중이면 버퍼에 저장하고 확인응답 혹은 충분한 패킷이 쌓였을 때 버퍼에서 다시 전송

    그러나 이는 아래와 같은 문제를 발생시킨다.

    • 크기가 작은 HTTP 메시지는 패킷을 채우지 못해서 추가적인 데이터를 기다리며 지연
    • 확인응답 지연과 함께 쓰일 경우 그만큼 확인응답이 늦게오므로 시간이 더 지연됨
  • TIME_WAIT 지연과 포트 고갈

    TCP 커넥션이 종료되면 약 2분 이내에 같은 IP주소와 포트 번호를 가지는 커넥션이 생성되는 것을 막아준다. (패킷의 중복과 TCP 데이터 충돌을 막기 위해)

    → 그러나 성능시험을 하는 상황에서 문제가 될 수 있다. 약 6만(발신자 포트) 나누기 120(2분)하면 초당 500개 커넥션 제한.

    이를 다 처리할만큼 서버가 빠르지 않다면 TIME_WAIT 포트 고갈은 일어나지 않는다. 이 문제 없이도, 커넥션을 너무 많이 맺거나 대기 상태로 있는 제어 블록이 너무 많아지는 상황은 몇몇 운영체제를 느리게 만들기 때문에 주의해야 한다.

HTTP 커넥션 관리

흔히 잘못 이해하는 Connection 헤더

Connection 헤더에는 다음 세가디 종류의 토큰이 전달될 수 있다.

  • HTTP 헤더 필드 명은 이 커넥션에만 해당되는 헤더들을 나열한다.
  • 임시적인 토큰 값은 커넥션에 대한 비표준 옵션을 의미한다.
  • close 값은, 커넥션이 작업이 완료되면 종료되어야 함을 의미한다.

홉별(hop-by-hop) 헤더
두 개의 인접한 HTTP 앱이 맺고 있는 커넥션에만 적용될 옵션을 지정해야 될 때가 있고 이는 쉼표로 구분되면 다른 커넥션에 전달되서는 안된다. (Connection: close라고 명시해서)

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

3개의 이미지를 내려받는 상황을 가정.

HTML 파일 + 3개의 이미지를 받으려면 총 4번의 트랜잭션이 일어나는데, 여기서도 커넥션을 위한 설정시간(커넥션을 맺고, 느린시작 지연)이 들어가면 더 지연된다.

또한 사용자는 여러개의 이미지가 순차적보다 동시에 로드되는 것을 더 좋아하고 빠르다고 생각한다.

→ 브라우저 또한 이를 다 받기 전에 텅 빈 화면을 보여줌

이를 해결하기 위해 아래와 같은 최신 기술들이 있다.

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

병렬 커넥션

클라이언트가 여러 개의 커넥션을 맺어 HTTP 트랜잭션을 병렬로 처리한다.(여러개의 서버, 포트와 맺어질 수 있어야함)

그러나 이 병렬 커넥션이 항상 빠르진 않는데, 네트워크 대역폭이 좁은 경우 대부분의 시간을 데이터 전송하는 데만 쓰이게 된다.(여러 개의 커넥션을 생성하면서 생기는 부하또한 영향을 미침) 또한 다수의 커넥션은 메모리를 많이 소모하고 자체적인 성능 문제를 발생시킨다.

병렬 커넥션이 페이지를 항상 더 빠르게 로드하지는 않지만 화면에 여러 개의 객체가 동시에 보이면서 내려받으면 사용자는 더 빠르게 내려받고 있는 것처럼 느낄 수 있다.

지속 커넥션

웹 클라이언트는 같은 사이트에 여러 개의 커넥션을 맺고, 이를 사이트 지역성이라고 부른다. 처리가 완료된 후에도 계속 연결된 상태로 있는 TCP 커넥션을 지속 커넥션이라고 하며 이를 재사용 함으로써, 커넥션을 맺는 준비작업에 따르는 시간을 절약할 수 있다. + 느린 시작으로 인한 지연또한 피하게 됨(튜닝된 커넥션으로)

병렬 커넥션의 단점

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

지속 커넥션은 이런 문제를 해결하지만 잘못 관리하는 경우 계속 연결된 상태의 커넥션이 쌓여 클라이언트와 서버의 리소스에 불필요한 소모를 발생시킨다.

따라서 지속 + 병렬 조합이 가장 효과적이며 지속 커넥션 타입으로 HTTP/1.0+ 에서는 ‘keep-alive’ 커넥션을, HTTP/1.1 에는 ‘지속’ 커넥션이 있다.

keep-alive 커넥션을 통해 커넥션을 맺고 끊는데 필요한 작업을 없애 시간을 단축시킬 수 있다.

keep-alive는 헤더에 Connection:Keep-Alive를 포함시켜 보내고 서버도 그다음 요청도 이 커넥션을 통해 받고자하면 응답에 이를 포함시켜 보낸다. 만약 헤더가 없으면 서버 커넥션을 끊을 것이라고 클라이언트가 추정한다.

→ keep-alive를 받았다고 해서 무조건 이를 따를 필요가 없다.

옵션으로 아래와 같은 것들이 있다.(모두 이대로 동작한다는 보장은 없다.)

  • timeout: 이 커넥션이 얼마간 유지될지
  • max: 커넥션이 몇 개의 HTTP 트랜잭션을 처리할 때 까지 유지될 것인지
  • Keep-Alive 헤더는 진단이나 디버깅을 주 목적으로 하는 처리되지는 않는 임의의 속성들을 지원하기도 한다.
Connection: Keep-Alive
Keep-Alive: max=5, timeout=120

Keep-Alive 커넥션 제한과 규칙

  • HTTP/1.0에서 기본으로 사용되지 않아 요청 헤더를 따로 보내야함
  • 이를 계속 유지하려면 모든 메시지에 Connection: Keep-Alive 헤더가 포함되어 있어야 함(없으면 이를 보고 커넥션을 끊을 것임을 추론)
  • 끊어지기 전, 엔터티의 본문의 길이를 알 수 있어야 커넥션 유지 가능
  • 프락시와 게이트웨이는 Connection 헤더의 규칙을 철저히 지켜야함
  • 이 헤더를 인식 못하는 프락시 서버와 맺어지면 안된다.
  • HTTP/1.0을 따르는 기기로부터
  • 받는 모든 Connection 헤더 필드는 무시해야한다. (오래된 프락시 서버로부터 실수로 전달 될 수 있어서)
  • 클라이언트는 응답 전체를 모두 받기 전에 커넥션이 끊어졌을 경우 별다른 문제가 없으면 요청을 다시 보낼 수 있게 준비되어야한다.

Keep-Alive와 멍청한(dumb) 프락시

프락시가 Connection 헤더를 이해하지 못해 해당 헤더들을 삭제하지 않고 요청 그대로를 다음 프락시에 전달한다. 이로인한 다음과 같은 문제 상황이 발생한다.

클라이언트 중간에 프락시가 있다고 하였을 시, Connection이 Keep-Alive인 헤더를 보내면 프락시는 이를 이해못하고 바로 서버에 이를 보낸다. 서버는 이를 커넥션을 유지하는것으로 동의를하고 다시 프락시에게 응답 데이터를 준후, 프락시는 클라이언트에게 이를 다시 보낸다.

결국 클라이언트와 서버 모두 Connection이 keep-alive인 상태로 알고 있지만 정작 프락시는 이를 이해하지 못하고 서버가 커넥션을 끊기를 기다린다.

→ 이 상황에서 클라이언트가 다음 요청을 보낼 때, 프락시는 같은 커넥션 상에서 다른 요청이 오는 경우를 예상하지 못해 클라이언트나 서버가 타임아웃이 나서 커넥션이 끊길 때까지 기다린다.

따라서 프락시는 Connection 헤더 및 이 헤더에 명시된 헤더들을 전달해서는 안되며 홉별 헤더들 역시 전달하거나 캐시하면 안된다.

Proxy-Connection(비 표준)

멍청한 프락시인 경우 그냥 넘겨서 문제 발생 X

영리한 프락시인 경우 인식하여 keep-alive 커넥션을 맺을 수 있음

→ 이 방법은 한 개의 프락시만 있는 경우 작동하고 멍청한 프락시와 영리한 프락시가 섞여있다면 문제가 발생한다. 따라서 이러한 부분들을 고려하여 지속 커넥션을 명확히 구현하는 것은 중요하다.

HTTP/1.1의 지속 커넥션

이 버전에서 지속 커넥션은 기본으로 활성화되어 있다. 트랜잭션이 끝난 후 커넥션을 종료하려면 Connection: close 헤더를 명시해야한다. HTTP/1.1 클라이언트는 응답에 이 헤더가 없으면 응답 후에도 커넥션을 계속 유지하는 것으로 추정한다. 그러나 클라이언트와 서버는 언제든지 커넥션을 끊을 수 있다.

지속 커넥션의 제한과 규칙

  • 클라이언트가 해당 커넥션으로 추가적인 요청을 보내지 않을 것이라면 마지막 요청에 Connection: close 헤더를 보내고, 이를 보내면 그 커넥션으로 추가적인 요청을 보낼 수 없다.
  • 커넥션에 있는 모든 메시지가 자신의 길이 정보를 정확히 가지고 있을 때에만 커넥션을 지속시킬 수 있다.(Content-length or 청크 전송 인코딩으로 인코드 되어 있어야함)
  • HTTP/1.1의 프락시는 클라이언트와 서버 각각에 대한 별도의 지속 커넥션을 맺고 관리해야 한다.
  • 커넥션 관련 기능에 대한 클라이언트의 지우너 범위를 알고 있지 않은 한 지속 커넥션을 맺으면 안된다.(오래된 프락시가 Connection 헤더를 전달할 수 있어서)
  • HTTP/1.1 기기는 언제든 커넥션을 끊을 수 있다. (Connection 헤더의 값과 상관 없이)
  • HTTP/1.1 애플리케이션은 중간에 끊어지는 커넥션을 복구할 수 있어야만 한다.
  • 클라이언트는 전체 응답을 받기전 커넥션이 끊어지면 요청을 반복해서 보내도 문제가 없는 경우 요청을 다시 보낼 준비가 되어있어야한다.
  • 하나의 사용자 클라이언트는 서버의 과부화를 방지하기 위해 넉넉잡아 2개의 지속 커넥션만 유지해야한다.

파이프라인 커넥션

HTTP/1.1은 지속 커넥션을 통해서 요청을 파이프라이닝할 수 있다.

→ 이는 대기 시간이 긴 네트워크 상황에서 네트워크상의 왕복으로 인한 시간을 줄여서 성능을 높여준다.

파이프라인의 제약 사항

  • 지속 커넥션인지 확인하기 전까지 파이프라인을 이어서는 안됨
  • HTTP 응답은 요청 순서와 같게 와야한다.
  • HTTP 클라이언트는 커넥션이 언제 끊어지더라도, 완료되지 않은 요청이 파이프라인에 있으면 언제든 다시 요청을 보낼 준비가 되어 있어야 한다. 이 말은 예상치 못하게 끊긴 커넥션을 다시 맺고 요청을 보낼 수 있어야한다는 말이다.
  • POST 요청처럼 비멱등 요청(연산이 일어날때 서버의 데이터와 같이 결과에 변화가 생기는 것)은 에러가 발생하면 어떤 것들이 서버에서 처리되었는지 클라이언트가 알 수 없어서 파이프라인을 통해 보내면 안된다.

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

‘마음대로’ 커넥션 끊기

보통 커넥션은 메시지를 다 보낸 다음 끊지만, 에러가 있는 상황에선 끊길 수 있고, HTTP 애플리케이션은 언제든지 지속 커넥션을 임의로 끊을 수 있다.

ex) 지속 커넥션이 일정 시간 동안 요청을 전송하지 않고 유휴 상태에 있으면 서버는 그 커넥션을 끊는데, 이 시점에 서버는 클라이언트가 데이터를 전송하지 않을 것이라고 확신하지 못한다. → 이로 인한 클라이언트가 요청 메시지를 보내는 중 문제가 생긴다.

Content-Length 와 Truncation

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

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

커넥션은 에러가 없더라도 언제든 끊길 수 있는데, 이런 상황에서 HTTP 앱은 적절하게 대응할 수 있어야한다. 다시 연결을 맺으려는 재시도를 한다던가.. 등등

그러나 파이프라인 커넥션 상황에서는 좀 더 어려워지는데, 서버에서 처리되어야할 요청들을 남겨두고 커넥션이 끊어졌을 땐, 멱등과 비멱등 요청을 구분하여 이를 처리해야한다.

우아한 커넥션 끊기

TCP 커넥션은 양방향이다.

  • 전체 끊기와 절반 끊기
    애플리케이션은 TCP 입력 채널과 출력 채널 중 한 개만 끊거나 둘 다 끊을 수 있는데 close() 호출시, 모두 끊고, shutdown() 호출 시 개별적으로 끊는다.

  • TCP 끊기와 리셋 에러
    전체 끊기를 사용할 수 있지만 다른 HTTP 서버, 프락시, 클라이언트 등등과 통신하고 파이프라인 지속 커넥션을 사용할 때, 예상치 못한 에러 발생을 예방하기 위해 ‘절반 끊기'를 사용해야한다.

    보통은 커넥션의 출력채널을 끊는 것이 안전하며 입력채널을 끊고, 이 채널에 데이터를 전송하면 TCP ‘connection reset by peer’ 메시지를 클라이언트에 보내게 된다.

    → 이를 심각한 오류라고 취급하여 버퍼에 저장된, 아직 서버에서 읽히지 않은 데이터를 모두 삭제한다.

우아하게 커넥션 끊기

일반적으로 자신의 출력 채널을 먼저 끊고 다른 쪽의 출력 채널이 끊기는 것을 기다리는 것

또한 양쪽에서 더는 데이터를 전송하지 않을 것이라고 알려주면(출력 채널의 커넥션을 끊는것) 커넥션은 리셋의 위험 없이 온전히 종료된다.

또한 데이터나 스트림의 끝을 식별하기 위해 상태 검사를 주기적으로 하고, 타임아웃 시간내에 끊어지지 않으면 앱은 리소스 보호를 위해 커넥션을 강제로 끊을 수도 있다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글