Search

4장 커넥션 관리

발표자
Participant
Presentation Date
2021/03/31

TCP 커넥션

HTTP는 패킷 교환 네트워크 프로토콜의 계층화된 집합인 TCP/IP를 통해 이루어진다.
메시지가 손실, 손상되거나 순서가 바뀌지 않고 안전하게 전달된다.
주소 입력 → TCP 커넥션 생성 → 요청 → 응답 → TCP 커넥션 해제.

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

TCP 커넥션은 인터넷을 안정적으로 연결해준다.
TCP 커넥션을 통해 한 바이트씩 순서대로 전달된다.

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

HTTP는 '프로토콜 스택'의 최상위 계층이다.
HTTP + TLS(SSL) ⇒ HTTPS
TCP는 세그먼트 단위로 데이터 스트림을 잘게 나누고, IP 패킷에 담아서 전달한다.

TCP 커넥션 유지하기

TCP는 포트 번호를 통해서 여러 개의 커넥션을 유지한다.
<발신 IP, 발신 포트, 수신 IP, 수신 포트 >
네가지 구성요소가 모두 동일한 커넥션을 중복 생성할 수 없다.

TCP 소켓 프로그래밍

운영체제는 TCP 커넥션의 생성과 관련된 여러 기능을 제공 → 소켓 API
socket() : 소켓 생성
connect() : TCP 커넥션 생성
read() : 읽기
write() : 쓰기
close() : TCP 커넥션 끊음
TCP 양 끝에 데이터 구조를 생성하고 연결하여 데이터 스트림을 읽고 쓸 수 있다.
프로토콜의 세부 사항을 숨긴다.

TCP의 성능에 대한 고려

HTTP는 TCP의 상위 계층이기 때문에, HTTP 트랜잭션의 성능은 TCP의 성능에 영향을 받는다.

HTTP 트랜잭션 지연

트랜잭션이 처리되는 시간 << TCP 커넥션 설정, 요청/응답 전송 시간
HTTP의 지연은 TCP 네트워크 지연으로 인해 발생
DNS 주소 → IP주소 변환 시간 소요 ( 인프라 발전 및 캐시 사용으로 단축 )
커넥션 설정 시간 소요
요청 메시지 - 처리 - 응답 메시지 전송 시간 소요
하드웨어, 네트워크, 서버, 메시지 크기, 거리 등

성능 관련 중요 요소

TCP 커넥션 핸드셰이크 지연

작은 크기의 트랜잭션을 처리하기 위해 커넥션 생성에 50% 이상의 시간 소요
클라이언트는 커넥션을 생성을 요청하기 위해 'SYN' 패킷 전송
서버는 요청을 받아들여 응답으로 'SYN + ACK' 패킷 전송
클라이언트는 커넥션이 잘 맺어졌음을 'ACK' ( 확인응답 ) 패킷 전송

확인응답 지연

확인 응답을 보내기 위해 수신지가 같은 데이터 패킷에 'ACK' 패킷을 편승
특정 시간 동안 버퍼에 저장해 두고, 편승시킬 송출 데이터 패킷을 찾는다.
요청과 응답으로 이루어지는 HTTP 동작 방식에서 편승 기회가 낮음.
확인응답 지연 관련 기능 수정 및 비활성화

TCP 느린 시작(slow start)

TCP 커넥션 생성 후 데이터 전송 속도를 제한하고 성공적으로 데이터를 전송하면 점차 속도 제한을 높인다.
2개 → 성공 → 4개 → 성공 → ...
튜닝된 커넥션을 사용하는 '지속 커넥션' 기능

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

TCP 세그먼트의 플래그와 헤더는 대략 40바이트
작은 크기의 데이터를 포함한 패킷을 다량 보내면 성능 저하
TCP 세그먼트가 일정 크기가 되지 않으면 전송하지 않는다.
다만 다른 모든 패킷이 'ACK'를 받으면 전송한다.
크기가 작은 HTTP 메시지는 패킷을 채우지 못해, 계속 대기하여 지연 발생
확인응답 지연과 함께 사용되어 지연 발생
HTTP 스택 파라미터로 TCP_NODELAY 설정하여 네이글 알고리즘 비활성화

TIME_WAIT의 누적과 포트 고갈

TCP 커넥션을 끊으면 종단간 IP와 포트를 메모리에 기록
일정 시간동안 해당 주소를 사용하는 커넥션의 재생성 방지

HTTP 커넥션 관리

커넥션을 생성하고 최적화하는 HTTP 기술

흔히 잘못 이해하는 Connection 헤더

클라이언트와 서버 사이에 중개 서버가 존재( 프락시, 캐시 서버 )
인접한 두 개의 HTTP 어플리케이션 커넥션에만 적용될 옵션을 지정하는 경우
커넥션 토큰을 쉼표로 구분하여 갖고, 다른 커넥션에 전달되지 않아야 한다.
Connection 헤더에는 세 가지 종류의 토큰이 전달될 수 있다.
HTTP 헤더 필드명은 이 커넥션에만 해당되는 헤더를 나열
임시적인 토큰 값은, 커넥션에 대한 비표준 옵션을 의미
close 값은, 커넥션 작업이 완료되면 종료되어야 함을 의미
커넥션 토큰이 HTTP 헤더 필드명을 갖고 있으면 현재 커넥션만을 위한 정보이므로 다음 커넥션에 전달해서는 안된다.
Connection 헤더에 있는 모든 헤더 필드는 다음 커넥션에 전달되는 시점에 삭제되어야 한다.
홉별(hop-by-hop) 헤더 명을 기술 → '헤더 보호하기'

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

HTML과 리소스를 받기 위해서 각기 다른 커넥션 생성 → 지연 발생
사용자는 빈 화면에 하나씩 나타나는 것을 보고있다.
HTTP 커넥션의 성능을 향상시키는 기술 네 가지에 대해 알아보자.

병렬 커넥션

여러 개의 TCP 커넥션을 통한 동시 HTTP 요청

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

단일 커넥션의 대역폭 제한과 커넥션이 동작하지 않는 시간을 활용
각 커넥션의 지연시간을 겹치게 하여 총 지연 시간을 감소
클라이언트의 인터넷 대역폭을 여러 커넥션이 나누어 사용

병렬 커넥션이 항상 더 빠르지는 않다

대역폭이 작은 경우에는 오히려 성능 저하
다수의 커넥션 생성 과정의 부하와 메모리 소모
브라우저는 적은 수의 병렬 커넥션을 허용
서버는 여러 사용자를 고려하여 과도한 수의 커넥션 제한

병렬 커넥션은 더 빠르게 '느껴질 수' 있다

화면에 객체가 동시에 보여 사용자는 심리적으로 더 빠르다고 느낀다.

지속 커넥션

동일한 서버에 HTTP 요청을 여러번 전송 → 사이트 지역성
HTTP 트랜잭션 처리 완료 후 TCP 커넥션을 유지하여 재사용
새로운 커넥션 설정 및 느린 시작으로 인한 지연 감소

지속 커넥션 vs 병렬 커넥션

병렬 커넥션의 단점
각 트랜잭션 마다 새로운 커넥션을 생성하고 끊음 → 시간, 대역폭 소모
느린 시작으로 인한 성능 저하
실제 연결 가능한 병렬 커넥션 수의 제한
지속 커넥션의 단점
지속 커넥션이 쌓여 리소스의 불필요한 소모 발생
지속 커넥션과 병렬 커넥션을 함께 사용하는 것이 효과적

HTTP/1.0+의 Keep-Alive 커넥션

HTTP/1.0 에서 시작된 keep-alive 커넥션 → HTTP/1.1에서 수정 및 보완
새로운 커넥션 생성에 소요되는 시간을 절약

Keep-Alive 동작

HTTP/1.1 명세에서 제외되었지만 여전히 사용중
클라이언트는 요청에 'Connection:Keep-Alive' 헤더를 포함하여 전송
서버는 응답 메시지에 같은 헤더를 포함하여 전송
응답에 헤더가 없으면 클라이언트는 서버가 커넥션을 끊을 것이라 추정

Keep-Alive 옵션

keep-alive의 동작은 Keep-Alive 헤더의 쉼표로 구분된 옵션을 추가하여 제어
timeout : 커넥션을 유지할 시간 ( 응답 )
max : 커넥션을 유지할 트랜잭션 처리 개수 ( 응답 )

Keep-Alive 커넥션 제한과 규칙

클라이언트는 keep-alive 커넥션을 사용하기 위해 Connection : Keep-Alive 요청 헤더를 전송한다.
커넥션을 유지하기 위해 Connection : Keep-Alive 헤더를 포함해 전송한다.
클라이언트는 Connection : Keep-Alive 헤더가 없으면 서버가 응답 후에 커넥션을 끊을 것을 알 수 있다.
엔터티 본문의 길이를 명확하게 표시해야 한다. (Content-Length)
프락시와 게이트웨이는 메시지를 전달하거나 캐시에 저장하기 전에 Connection 헤더에 명시된 모든 헤더 필드와 Connection 헤더를 제거한다.
keep-alive 커넥션은 Connection 헤더를 인식하지 못하는 멍청한 프락시(dumb proxy)와 맺어져서는 안된다.
HTTP/1.0을 따르는 기기로부터 받는 모든 Connection 헤더 필드는 무시해야하 한다.
클라이언트는 응답 전체를 모두 받기 전에 커넥션이 끊어진 경우, 다시 요청을 보낼 수 있는 준비를 해야한다.

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

클라이언트의 요청 메시지에 Connection:Keep-Alive 헤더가 포함된 경우, 처리가 끝난 뒤에 이 커넥션을 유지하려는 것
서버가 keep-alive를 지원한다면 응답에 Connection:Keep-Alive 헤더를 포함하여 전송한다.

Connection 헤더의 무조건 전달

프락시는 Connection 헤더를 이해하지 못해서 해당 헤더들을 삭제하지 않고 요청 그대로를 다음 프락시에 전달
클라이언트는 프락시에 'Connection:Keep-Alive' 헤더가 포함된 요청 메시지를 전송. 클라이언트는 프락시의 응답을 대기하고 있다.
멍청한 프락시는 'Connection:Keep-Alive' 헤더를 이해하지 못해 서버에 그대로 전달한다. Connection 헤더는 홉별 헤더이므로 인접한 애플리케이션간에 적용하고 삭제한 뒤에 전달해야 한다.
서버는 프락시로부터 'Connection:Keep-Alive' 헤더가 포함된 메시지를 받아 프락시가 커넥션을 유지하자고 요청하는 것으로 판단하여 해당 헤더를 포함한 응답 메시지를 전송.
프락시는 여전히 해당 헤더를 이해하지 못해 클라이언트에게 그대로 전달.
클라이언트는 프락시로부터 'Connection:Keep-Alive'헤더가 포함된 메시지를 응답받아 커넥션을 유지한다.
프락시는 서버로부터 응답 메시지를 받았기 때문에 커넥션이 끊어지기를 기다림.
이후 클라이언트가 요청 메시지를 전송하고 프락시는 같은 커넥션상에서 다른 요청이 오는 경우를 예상하지 못하여 모든 요청 메시지를 무시한다.
브라우저는 아무런 응답 메시지를 받지 못하고 이후 타임아웃이 발생하여 커넥션이 끊어질 때 까지 기다린다.

프락시와 홉별 헤더

위와 같은 문제를 회피하기 위해, 프락시는 Connection 헤더와 여기에 명시된 헤더를 전달하면 안된다.
홉별 헤더들 역시 전달하거나 캐시해서는 안된다.

Proxy-Connection 살펴보기

모든 헤더를 무조건 전달하는 문제를 해결하기 위한 차선책
Connection 헤더 대신에 비표준인 Proxy-Connection 확장 헤더를 프락시에게 전달한다.
프락시가 무조건 전달하더라도 서버는 해당 헤더를 무시하여 문제가 발생하지 않는다.
영리한 프락시는 Connection 헤더로 변환하여 올바르게 처리한다.
클라이언트와 서버사이에 한 개의 프락시가 있는 경우에 성립된다.
멍청한 프락시 옆에 영리한 프락시가 있는 경우 또다른 문제가 발생함.

HTTP/1.1의 지속 커넥션

HTTP/1.1 명세에서 제외된 keep-alive 대신 '지속 커넥션'을 지원
HTTP/1.1의 커넥션은 기본적으로 지속 커넥션이 활성화 되어있다.
트랜잭션이 끝나면 Connection:close 헤더를 포함한 응답을 전송한다.

지속 커넥션의 제한과 규칙

클라이언트가 요청에 Connection:close 헤더를 포함하여 보내면 해당 커넥션은 더이상 사용할 수 없다.
클라이언트가 해당 커넥션으로 추가적인 요청을 보내지 않을 것이라면 Connection:close 헤더를 포함한 메시지를 보내야 한다.
커넥션에 있는 모든 메시지가 자신의 길이 정보를 정확하게 갖고 있어야 커넥션을 유지할 수 있다.
프락시는 클라이언트와 서버 각각에 대해 별도의 지속 커넥션을 맺고 관리해야 한다.
프락시 서버는 클라이언트가 커넥션 관련 기능에 대한 클라이언트의 지원 범위를 알고있지 않다면 지속 커넥션을 맺어서는 안된다.
지속 커넥션도 언제든지 끊길 수 있다.
중간에 끊어지는 커넥션을 복구할 수 있어야 한다. 클라이언트는 다시 보내도 문제가 없는 요청은 가능한 한 다시 보내야 한다.
클라이언트는 전체 응답을 받기 전에 커넥션이 끊어지면, 요청을 반복해서 보내도 문제가 없는 경우에는 요청을 다시 보낼 준비가 되어 있어야 한다.
클라이언트 사용자는 서버의 과부하 방지를 위해 두개의 지속 커넥션만을 유지해야 한다.

파이프라인 커넥션

HTTP/1.1은 지속 커넥션을 통해서 요청을 파이프라이닝 할 수 있다.
여러 개의 요청은 응답이 도착하기 전까지 큐에 쌓인다.
파이프라인의 제약 사항
클라이언트는 지속 커넥션 요청에 대한 응답을 받기 전까지 파이프라인을 이어서는 안된다.
응답은 요청을 보낸 순서와 같게 와야한다.
클라이언트는 커넥션이 끊어지더라도 큐에 쌓인 요청을 언제든 다시 보낼 준비를 해야한다.
POST 요청과 같이 반복해서 보낼 경우 문제가 생기는 요청은 파이프라인을 통해 보내서는 안된다 → 비멱등 요청

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

커넥션 관리에 대한 명확한 기준은 없다.

'마음대로' 커넥션 끊기

누구든지 아무때나 커넥션을 끊을 수 있다.
파이프라인의 경우, 일정 시간 동안 아무런 요청 메시지를 전송하지 않으면 서버는 그 커넥션을 끊을 수 있다.
하지만 서버가 커넥션을 끊는 시점에 클라이언트가 요청 메시지를 전송하였다고 확신할 수는 없다.

Content-Length와 Truncation

HTTP 응답은 본문의 정확한 크기 값을 갖는 Content-Length 헤더를 가지고 있어야 한다.
서버가 잘못된 길이 정보로 응답 메시지를 보내고 커넥션을 끊은 경우, 클라이언트는 데이터의 정확한 길이를 물어봐야 하는데 물어볼 수 가 없다.

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

커넥션은 에러가 없어도 언제든 끊을 수 있다.
예상치 못하게 커넥션이 끊기게 되면 재전송 할 수 있는 준비를 해야한다.
파이프라인의 경우, 응답이 오기전에 커넥션이 끊어지면 요청한 메시지가 얼마나 처리되었는지 알 수 없다.
POST와 같이 비멱등한 메소드를 재전송하면 잘못된 결과를 초래한다.
비멱등인 요청을 재전송해야 하는 경우, 이전 요청에 대한 응답을 기다린다.

우아한 커넥션 끊기

TCP 커넥션의 양쪽 끝에는 입력 큐와 출력 큐가 있다.
한쪽의 출력 큐에있는 데이터는 다른 한쪽의 입력 큐로 보내진다.

전체 끊기와 절반 끊기

HTTP 애플리케이션은 입력 채널과 출력 채널 중 하나 이상을 끊을 수 있다.
전체 끊기 : 입출력 채널의 커넥션을 모두 끊는다. → close()
절반 끊기 : 입력 채널이나 출력 채널중 하나만 끊는다. → shutdown()

TCP 끊기와 리셋 에러

단순한 HTTP 애플리케이션은 전체 끊기만 사용
각기 다른 대상과 통신할 때, 파이프라인 지속 커넥션 사용, 에러 방지를 위해 절반 끊기 사용
보통 커넥션의 출력 채널을 끊는 것이 안전하다.
클라이언트가 더이상 데이터를 보내지 않는다고 확신할 수 없는 이상 입력 채널을 끊는 것은 위험하다.
클라이언트가 이미 끊긴 입력 채널에 요청 메시지를 보내는 경우 서버의 운영체제는 'connection reset by peer' 메시지를 클라이언트에게 보낸다.
운영체제는 심각한 에러로 취급하여 버퍼에 아직 읽지 않은 데이터를 모두 삭제한다.
파이프라인 지속 커넥션에서 더욱 악화된 상황으로 발생
클라이언트가 요청에 대한 응답을 읽지 않아 버퍼에 저장되어 있다.
서버가 커넥션을 끊는다.
클라이언트는 새로운 요청을 보내지만 이미 커넥션이 끊겨 connection reset by peer 메시지를 수신한다.
버퍼에 있는 응답 데이터는 모두 삭제된다.

우아하게 커넥션 끊기

HTTP 명세에서 언급되지만 방법에 대해 설명하지 않음.
일반적인 '우아하게 커넥션 끊기'는 자신의 출력 채널을 먼저 끊고 다른 쪽의 출력 채널이 끊기는 것을 기다리며 더이상 데이터를 전송하지 않을 것을 확신한 뒤에 커넥션을 끊는 것.
하지만 상대방이 절반 끊기 기능이 구현되지 않거나 절반 끊기를 했는지 보장할 수 없다.
출력 채널에 절반 끊기를 하고 난 뒤에도 입력 채널에 대해 주기적으로 데이터나 스트림의 끝을 식별하는 검사를 수행한다.